1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Effective seismological trace viewer.
7'''
8from __future__ import absolute_import
10import os
11import sys
12import signal
13import logging
14import time
15import re
16import zlib
17import struct
18import pickle
21from pyrocko.streaming import serial_hamster
22from pyrocko.streaming import slink
23from pyrocko.streaming import edl
25from pyrocko import pile # noqa
26from pyrocko import util # noqa
27from pyrocko import model # noqa
28from pyrocko import config # noqa
29from pyrocko import io # noqa
31from . import pile_viewer # noqa
33from .qt_compat import qc, qg, qw, qn
35logger = logging.getLogger('pyrocko.gui.snuffler_app')
38class AcquisitionThread(qc.QThread):
39 def __init__(self, post_process_sleep=0.0):
40 qc.QThread.__init__(self)
41 self.mutex = qc.QMutex()
42 self.queue = []
43 self.post_process_sleep = post_process_sleep
44 self._sun_is_shining = True
46 def run(self):
47 while True:
48 try:
49 self.acquisition_start()
50 while self._sun_is_shining:
51 t0 = time.time()
52 self.process()
53 t1 = time.time()
54 if self.post_process_sleep != 0.0:
55 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
57 self.acquisition_stop()
58 break
60 except (
61 edl.ReadError,
62 serial_hamster.SerialHamsterError,
63 slink.SlowSlinkError) as e:
65 logger.error(str(e))
66 logger.error('Acquistion terminated, restart in 5 s')
67 self.acquisition_stop()
68 time.sleep(5)
69 if not self._sun_is_shining:
70 break
72 def stop(self):
73 self._sun_is_shining = False
75 logger.debug("Waiting for thread to terminate...")
76 self.wait()
77 logger.debug("Thread has terminated.")
79 def got_trace(self, tr):
80 self.mutex.lock()
81 self.queue.append(tr)
82 self.mutex.unlock()
84 def poll(self):
85 self.mutex.lock()
86 items = self.queue[:]
87 self.queue[:] = []
88 self.mutex.unlock()
89 return items
92class SlinkAcquisition(
93 slink.SlowSlink, AcquisitionThread):
95 def __init__(self, *args, **kwargs):
96 slink.SlowSlink.__init__(self, *args, **kwargs)
97 AcquisitionThread.__init__(self)
99 def got_trace(self, tr):
100 AcquisitionThread.got_trace(self, tr)
103class CamAcquisition(
104 serial_hamster.CamSerialHamster, AcquisitionThread):
106 def __init__(self, *args, **kwargs):
107 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
108 AcquisitionThread.__init__(self, post_process_sleep=0.1)
110 def got_trace(self, tr):
111 AcquisitionThread.got_trace(self, tr)
114class USBHB628Acquisition(
115 serial_hamster.USBHB628Hamster, AcquisitionThread):
117 def __init__(self, deltat=0.02, *args, **kwargs):
118 serial_hamster.USBHB628Hamster.__init__(
119 self, deltat=deltat, *args, **kwargs)
120 AcquisitionThread.__init__(self)
122 def got_trace(self, tr):
123 AcquisitionThread.got_trace(self, tr)
126class SchoolSeismometerAcquisition(
127 serial_hamster.SerialHamster, AcquisitionThread):
129 def __init__(self, *args, **kwargs):
130 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
131 AcquisitionThread.__init__(self, post_process_sleep=0.01)
133 def got_trace(self, tr):
134 AcquisitionThread.got_trace(self, tr)
137class EDLAcquisition(
138 edl.EDLHamster, AcquisitionThread):
140 def __init__(self, *args, **kwargs):
141 edl.EDLHamster.__init__(self, *args, **kwargs)
142 AcquisitionThread.__init__(self)
144 def got_trace(self, tr):
145 AcquisitionThread.got_trace(self, tr)
148def setup_acquisition_sources(args):
150 sources = []
151 iarg = 0
152 while iarg < len(args):
153 arg = args[iarg]
155 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
156 mca = re.match(r'cam://([^:]+)', arg)
157 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
158 msc = re.match(r'school://([^:]+)', arg)
159 med = re.match(r'edl://([^:]+)', arg)
160 if msl:
161 host = msl.group(1)
162 port = msl.group(3)
163 if not port:
164 port = '18000'
166 sl = SlinkAcquisition(host=host, port=port)
167 if msl.group(5):
168 stream_patterns = msl.group(5).split(',')
170 if '_' not in msl.group(5):
171 try:
172 streams = sl.query_streams()
173 except slink.SlowSlinkError as e:
174 logger.fatal(str(e))
175 sys.exit(1)
177 streams = list(set(
178 util.match_nslcs(stream_patterns, streams)))
180 for stream in streams:
181 sl.add_stream(*stream)
182 else:
183 for stream in stream_patterns:
184 sl.add_raw_stream_selector(stream)
186 sources.append(sl)
187 elif mca:
188 port = mca.group(1)
189 cam = CamAcquisition(port=port, deltat=0.0314504)
190 sources.append(cam)
191 elif mus:
192 port = mus.group(1)
193 try:
194 d = {}
195 if mus.group(3):
196 d = dict(urlparse.parse_qsl(mus.group(3))) # noqa
198 deltat = 1.0/float(d.get('rate', '50'))
199 channels = [(int(c), c) for c in d.get('channels', '01234567')]
200 hb628 = USBHB628Acquisition(
201 port=port,
202 deltat=deltat,
203 channels=channels,
204 buffersize=16,
205 lookback=50)
207 sources.append(hb628)
208 except Exception:
209 raise
210 sys.exit('invalid acquisition source: %s' % arg)
212 elif msc:
213 port = msc.group(1)
214 sco = SchoolSeismometerAcquisition(port=port)
215 sources.append(sco)
216 elif med:
217 port = med.group(1)
218 edl = EDLAcquisition(port=port)
219 sources.append(edl)
221 if msl or mca or mus or msc or med:
222 args.pop(iarg)
223 else:
224 iarg += 1
226 return sources
229class PollInjector(qc.QObject):
231 def __init__(self, *args, **kwargs):
232 qc.QObject.__init__(self)
233 self._injector = pile.Injector(*args, **kwargs)
234 self._sources = []
235 self.startTimer(1000.)
237 def add_source(self, source):
238 self._sources.append(source)
240 def remove_source(self, source):
241 self._sources.remove(source)
243 def timerEvent(self, ev):
244 for source in self._sources:
245 trs = source.poll()
246 for tr in trs:
247 self._injector.inject(tr)
249 # following methods needed because mulitple inheritance does not seem
250 # to work anymore with QObject in Python3 or PyQt5
252 def set_fixation_length(self, length):
253 return self._injector.set_fixation_length(length)
255 def set_save_path(
256 self,
257 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
258 '%(tmin)s_%(tmax)s.mseed'):
260 return self._injector.set_save_path(path)
262 def fixate_all(self):
263 return self._injector.fixate_all()
265 def free(self):
266 return self._injector.free()
269class Connection(qc.QObject):
271 received = qc.pyqtSignal(object, object)
272 disconnected = qc.pyqtSignal(object)
274 def __init__(self, parent, sock):
275 qc.QObject.__init__(self, parent)
276 self.socket = sock
277 self.readyRead.connect(
278 self.handle_read)
279 self.disconnected.connect(
280 self.handle_disconnected)
281 self.nwanted = 8
282 self.reading_size = True
283 self.handler = None
284 self.nbytes_received = 0
285 self.nbytes_sent = 0
286 self.compressor = zlib.compressobj()
287 self.decompressor = zlib.decompressobj()
289 def handle_read(self):
290 while True:
291 navail = self.socket.bytesAvailable()
292 if navail < self.nwanted:
293 return
295 data = self.socket.read(self.nwanted)
296 self.nbytes_received += len(data)
297 if self.reading_size:
298 self.nwanted = struct.unpack('>Q', data)[0]
299 self.reading_size = False
300 else:
301 obj = pickle.loads(self.decompressor.decompress(data))
302 if obj is None:
303 self.socket.disconnectFromHost()
304 else:
305 self.handle_received(obj)
306 self.nwanted = 8
307 self.reading_size = True
309 def handle_received(self, obj):
310 self.received.emit(self, obj)
312 def ship(self, obj):
313 data = self.compressor.compress(pickle.dumps(obj))
314 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
315 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
316 self.socket.write(data)
317 self.socket.write(data_end)
318 self.nbytes_sent += len(data)+len(data_end) + 8
320 def handle_disconnected(self):
321 self.disconnected.emit(self)
323 def close(self):
324 self.socket.close()
327class ConnectionHandler(qc.QObject):
328 def __init__(self, parent):
329 qc.QObject.__init__(self, parent)
330 self.queue = []
331 self.connection = None
333 def connected(self):
334 return self.connection is None
336 def set_connection(self, connection):
337 self.connection = connection
338 connection.received.connect(
339 self._handle_received)
341 connection.connect(
342 self.handle_disconnected)
344 for obj in self.queue:
345 self.connection.ship(obj)
347 self.queue = []
349 def _handle_received(self, conn, obj):
350 self.handle_received(obj)
352 def handle_received(self, obj):
353 pass
355 def handle_disconnected(self):
356 self.connection = None
358 def ship(self, obj):
359 if self.connection:
360 self.connection.ship(obj)
361 else:
362 self.queue.append(obj)
365class SimpleConnectionHandler(ConnectionHandler):
366 def __init__(self, parent, **mapping):
367 ConnectionHandler.__init__(self, parent)
368 self.mapping = mapping
370 def handle_received(self, obj):
371 command = obj[0]
372 args = obj[1:]
373 self.mapping[command](*args)
376class MyMainWindow(qw.QMainWindow):
378 def __init__(self, app, *args):
379 qg.QMainWindow.__init__(self, *args)
380 self.app = app
382 def keyPressEvent(self, ev):
383 self.app.pile_viewer.get_view().keyPressEvent(ev)
386class SnufflerTabs(qw.QTabWidget):
387 def __init__(self, parent):
388 qw.QTabWidget.__init__(self, parent)
389 if hasattr(self, 'setTabsClosable'):
390 self.setTabsClosable(True)
392 self.tabCloseRequested.connect(
393 self.removeTab)
395 if hasattr(self, 'setDocumentMode'):
396 self.setDocumentMode(True)
398 def hide_close_button_on_first_tab(self):
399 tbar = self.tabBar()
400 if hasattr(tbar, 'setTabButton'):
401 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
402 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
404 def append_tab(self, widget, name):
405 widget.setParent(self)
406 self.insertTab(self.count(), widget, name)
407 self.setCurrentIndex(self.count()-1)
409 def remove_tab(self, widget):
410 self.removeTab(self.indexOf(widget))
412 def tabInserted(self, index):
413 if index == 0:
414 self.hide_close_button_on_first_tab()
416 self.tabbar_visibility()
417 self.setFocus()
419 def removeTab(self, index):
420 w = self.widget(index)
421 w.close()
422 qw.QTabWidget.removeTab(self, index)
424 def tabRemoved(self, index):
425 self.tabbar_visibility()
427 def tabbar_visibility(self):
428 if self.count() <= 1:
429 self.tabBar().hide()
430 elif self.count() > 1:
431 self.tabBar().show()
433 def keyPressEvent(self, event):
434 if event.text() == 'd':
435 i = self.currentIndex()
436 if i != 0:
437 self.tabCloseRequested.emit(i)
438 else:
439 self.parent().keyPressEvent(event)
442class SnufflerStartWizard(qw.QWizard):
444 def __init__(self, parent):
445 qw.QWizard.__init__(self, parent)
447 self.setOption(self.NoBackButtonOnStartPage)
448 self.setOption(self.NoBackButtonOnLastPage)
449 self.setOption(self.NoCancelButton)
450 self.addPageSurvey()
451 self.addPageHelp()
452 self.setWindowTitle('Welcome to Pyrocko')
454 def getSystemInfo(self):
455 import numpy
456 import scipy
457 import pyrocko
458 import platform
459 import uuid
460 data = {
461 'node-uuid': uuid.getnode(),
462 'platform.architecture': platform.architecture(),
463 'platform.system': platform.system(),
464 'platform.release': platform.release(),
465 'python': platform.python_version(),
466 'pyrocko': pyrocko.__version__,
467 'numpy': numpy.__version__,
468 'scipy': scipy.__version__,
469 'qt': qc.PYQT_VERSION_STR,
470 }
471 return data
473 def addPageSurvey(self):
474 import pprint
475 webtk = 'DSFGK234ADF4ASDF'
476 sys_info = self.getSystemInfo()
478 p = qw.QWizardPage()
479 p.setCommitPage(True)
480 p.setTitle('Thank you for installing Pyrocko!')
482 lyt = qw.QVBoxLayout()
483 lyt.addWidget(qw.QLabel(
484 '<p>Your feedback is important for'
485 ' the development and improvement of Pyrocko.</p>'
486 '<p>Do you want to send this system information anon'
487 'ymously to <a href="https://pyrocko.org">'
488 'https://pyrocko.org</a>?</p>'))
490 text_data = qw.QLabel(
491 '<code style="font-size: small;">%s</code>' %
492 pprint.pformat(
493 sys_info,
494 indent=1).replace('\n', '<br>')
495 )
496 text_data.setStyleSheet('padding: 10px;')
497 lyt.addWidget(text_data)
499 lyt.addWidget(qw.QLabel(
500 'This message won\'t be shown again.\n\n'
501 'We appreciate your contribution!\n- The Pyrocko Developers'
502 ))
504 p.setLayout(lyt)
505 p.setButtonText(self.CommitButton, 'No')
507 yes_btn = qw.QPushButton(p)
508 yes_btn.setText('Yes')
510 @qc.pyqtSlot()
511 def send_data():
512 import requests
513 import json
514 try:
515 requests.post('https://pyrocko.org/%s' % webtk,
516 data=json.dumps(sys_info))
517 except Exception as e:
518 print(e)
519 self.button(self.NextButton).clicked.emit(True)
521 self.customButtonClicked.connect(send_data)
523 self.setButton(self.CustomButton1, yes_btn)
524 self.setOption(self.HaveCustomButton1, True)
526 self.addPage(p)
527 return p
529 def addPageHelp(self):
530 p = qw.QWizardPage()
531 p.setTitle('Welcome to Snuffler!')
533 text = qw.QLabel('''<html>
534<h3>- <i>The Seismogram browser and workbench.</i></h3>
535<p>Looks like you are starting the Snuffler for the first time.<br>
536It allows you to browse and process large archives of waveform data.</p>
537<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
538<ul>
539 <li><b>Download seismograms</b> from Geofon, IRIS and others</li>
540 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
541 <li><b>Cake</b>, Calculate synthetic arrival times</li>
542 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
543 <li>
544 <b>Map</b>, swiftly inspect stations and events on interactive maps
545 </li>
546</ul>
547<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
548<p><b>NOTE:</b><br>If you installed snufflings from the
549<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
550snufflings repository</a><br>you also have to pull an update from there.
551</p>
552<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
553 align="center">
554 <b>You can always press <code>?</code> for help!</b>
555</p>
556</html>''')
558 lyt = qw.QVBoxLayout()
559 lyt.addWidget(text)
561 def remove_custom_button():
562 self.setOption(self.HaveCustomButton1, False)
564 p.initializePage = remove_custom_button
566 p.setLayout(lyt)
567 self.addPage(p)
568 return p
571class SnufflerWindow(qw.QMainWindow):
573 def __init__(
574 self, pile, stations=None, events=None, markers=None, ntracks=12,
575 marker_editor_sortable=True, follow=None, controls=True,
576 opengl=None, instant_close=False):
578 qw.QMainWindow.__init__(self)
580 self.instant_close = instant_close
582 self.dockwidget_to_toggler = {}
583 self.dockwidgets = []
585 self.setWindowTitle("Snuffler")
587 self.pile_viewer = pile_viewer.PileViewer(
588 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
589 marker_editor_sortable=marker_editor_sortable,
590 panel_parent=self)
592 self.marker_editor = self.pile_viewer.marker_editor()
593 self.add_panel(
594 'Markers', self.marker_editor, visible=False,
595 where=qc.Qt.RightDockWidgetArea)
596 if stations:
597 self.get_view().add_stations(stations)
599 if events:
600 self.get_view().add_events(events)
602 if len(events) == 1:
603 self.get_view().set_active_event(events[0])
605 if markers:
606 self.get_view().add_markers(markers)
607 self.get_view().associate_phases_to_events()
609 self.tabs = SnufflerTabs(self)
610 self.setCentralWidget(self.tabs)
611 self.add_tab('Main', self.pile_viewer)
613 self.pile_viewer.setup_snufflings()
614 self.setMenuBar(self.pile_viewer.menu)
616 self.main_controls = self.pile_viewer.controls()
617 self.add_panel('Main Controls', self.main_controls, visible=controls)
618 self.show()
620 self.get_view().setFocus(qc.Qt.OtherFocusReason)
622 sb = self.statusBar()
623 sb.clearMessage()
624 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
626 snuffler_config = self.pile_viewer.viewer.config
628 if snuffler_config.first_start:
629 wizard = SnufflerStartWizard(self)
631 @qc.pyqtSlot()
632 def wizard_finished(result):
633 if result == wizard.Accepted:
634 snuffler_config.first_start = False
635 config.write_config(snuffler_config, 'snuffler')
637 wizard.finished.connect(wizard_finished)
639 wizard.show()
641 if follow:
642 self.get_view().follow(float(follow))
644 self.closing = False
646 def sizeHint(self):
647 return qc.QSize(1024, 768)
648 # return qc.QSize(800, 600) # used for screen shots in tutorial
650 def keyPressEvent(self, ev):
651 self.get_view().keyPressEvent(ev)
653 def get_view(self):
654 return self.pile_viewer.get_view()
656 def get_panel_parent_widget(self):
657 return self
659 def add_tab(self, name, widget):
660 self.tabs.append_tab(widget, name)
662 def remove_tab(self, widget):
663 self.tabs.remove_tab(widget)
665 def add_panel(self, name, panel, visible=False, volatile=False,
666 where=qc.Qt.BottomDockWidgetArea):
668 if not self.dockwidgets:
669 self.dockwidgets = []
671 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
673 dockwidget = qw.QDockWidget(name, self)
674 self.dockwidgets.append(dockwidget)
675 dockwidget.setWidget(panel)
676 panel.setParent(dockwidget)
677 self.addDockWidget(where, dockwidget)
679 if dws:
680 self.tabifyDockWidget(dws[-1], dockwidget)
682 self.toggle_panel(dockwidget, visible)
684 mitem = qw.QAction(name, None)
686 def toggle_panel(checked):
687 self.toggle_panel(dockwidget, True)
689 mitem.triggered.connect(toggle_panel)
691 if volatile:
692 def visibility(visible):
693 if not visible:
694 self.remove_panel(panel)
696 dockwidget.visibilityChanged.connect(
697 visibility)
699 self.get_view().add_panel_toggler(mitem)
700 self.dockwidget_to_toggler[dockwidget] = mitem
702 def toggle_panel(self, dockwidget, visible):
703 if visible is None:
704 visible = not dockwidget.isVisible()
706 dockwidget.setVisible(visible)
707 if visible:
708 w = dockwidget.widget()
709 minsize = w.minimumSize()
710 w.setMinimumHeight(w.sizeHint().height() + 5)
712 def reset_minimum_size():
713 import sip
714 if not sip.isdeleted(w):
715 w.setMinimumSize(minsize)
717 qc.QTimer.singleShot(200, reset_minimum_size)
719 dockwidget.setFocus()
720 dockwidget.raise_()
722 def toggle_marker_editor(self):
723 self.toggle_panel(self.marker_editor.parent(), None)
725 def toggle_main_controls(self):
726 self.toggle_panel(self.main_controls.parent(), None)
728 def remove_panel(self, panel):
729 dockwidget = panel.parent()
730 self.removeDockWidget(dockwidget)
731 dockwidget.setParent(None)
732 mitem = self.dockwidget_to_toggler[dockwidget]
733 self.get_view().remove_panel_toggler(mitem)
735 def return_tag(self):
736 return self.get_view().return_tag
738 def confirm_close(self):
739 ret = qw.QMessageBox.question(
740 self,
741 'Snuffler',
742 'Close Snuffler window?',
743 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
744 qw.QMessageBox.Ok)
746 return ret == qw.QMessageBox.Ok
748 def closeEvent(self, event):
749 if self.instant_close or self.confirm_close():
750 self.closing = True
751 self.pile_viewer.cleanup()
752 event.accept()
753 else:
754 event.ignore()
756 def is_closing(self):
757 return self.closing
760class Snuffler(qw.QApplication):
762 def __init__(self):
763 qw.QApplication.__init__(self, sys.argv)
764 self.lastWindowClosed.connect(self.myQuit)
765 self.server = None
766 self.loader = None
768 def install_sigint_handler(self):
769 self._old_signal_handler = signal.signal(
770 signal.SIGINT,
771 self.myCloseAllWindows)
773 def uninstall_sigint_handler(self):
774 signal.signal(signal.SIGINT, self._old_signal_handler)
776 def start_server(self):
777 self.connections = []
778 s = qn.QTcpServer(self)
779 s.listen(qn.QHostAddress.LocalHost)
780 s.newConnection.connect(
781 self.handle_accept)
782 self.server = s
784 def start_loader(self):
785 self.loader = SimpleConnectionHandler(
786 self,
787 add_files=self.add_files,
788 update_progress=self.update_progress)
789 ticket = os.urandom(32)
790 self.forker.spawn('loader', self.server.serverPort(), ticket)
791 self.connection_handlers[ticket] = self.loader
793 def handle_accept(self):
794 sock = self.server.nextPendingConnection()
795 con = Connection(self, sock)
796 self.connections.append(con)
798 con.disconnected.connect(
799 self.handle_disconnected)
801 con.received.connect(
802 self.handle_received_ticket)
804 def handle_disconnected(self, connection):
805 self.connections.remove(connection)
806 connection.close()
807 del connection
809 def handle_received_ticket(self, connection, object):
810 if not isinstance(object, str):
811 self.handle_disconnected(connection)
813 ticket = object
814 if ticket in self.connection_handlers:
815 h = self.connection_handlers[ticket]
816 connection.received.disconnect(
817 self.handle_received_ticket)
819 h.set_connection(connection)
820 else:
821 self.handle_disconnected(connection)
823 def snuffler_windows(self):
824 return [w for w in self.topLevelWidgets()
825 if isinstance(w, SnufflerWindow) and not w.is_closing()]
827 def event(self, e):
828 if isinstance(e, qg.QFileOpenEvent):
829 paths = [str(e.file())]
830 wins = self.snuffler_windows()
831 if wins:
832 wins[0].get_view().load_soon(paths)
834 return True
835 else:
836 return qw.QApplication.event(self, e)
838 def load(self, pathes, cachedirname, pattern, format):
839 if not self.loader:
840 self.start_loader()
842 self.loader.ship(
843 ('load', pathes, cachedirname, pattern, format))
845 def add_files(self, files):
846 p = self.pile_viewer.get_pile()
847 p.add_files(files)
848 self.pile_viewer.update_contents()
850 def update_progress(self, task, percent):
851 self.pile_viewer.progressbars.set_status(task, percent)
853 def myCloseAllWindows(self, *args):
854 self.closeAllWindows()
856 def myQuit(self, *args):
857 self.quit()