1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import, print_function
7import sys
8import os
9import time
10import calendar
11import datetime
12import re
13import math
14import logging
15import operator
16import copy
17import enum
18from itertools import groupby
20import numpy as num
21import pyrocko.model
22import pyrocko.pile
23import pyrocko.trace
24import pyrocko.response
25import pyrocko.util
26import pyrocko.plot
27import pyrocko.gui.snuffling
28import pyrocko.gui.snufflings
29import pyrocko.gui.marker_editor
31from pyrocko.util import hpfloat, gmtime_x, mystrftime
33from .marker import associate_phases_to_events, MarkerOneNSLCRequired
35from .util import (ValControl, LinValControl, Marker, EventMarker,
36 PhaseMarker, make_QPolygonF, draw_label, Label,
37 Progressbars, ColorbarControl)
39from .qt_compat import qc, qg, qw, qgl, qsvg, use_pyqt5
41from .pile_viewer_waterfall import TraceWaterfall
43import scipy.stats as sstats
44import platform
46MIN_LABEL_SIZE_PT = 6
48try:
49 newstr = unicode
50except NameError:
51 newstr = str
54def fnpatch(x):
55 if use_pyqt5:
56 return x
57 else:
58 return x, None
61if sys.version_info[0] >= 3:
62 qc.QString = str
64qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
65 qw.QFileDialog.DontUseSheet
67if platform.mac_ver() != ('', ('', '', ''), ''):
68 macosx = True
69else:
70 macosx = False
72logger = logging.getLogger('pyrocko.gui.pile_viewer')
75def detrend(x, y):
76 slope, offset, _, _, _ = sstats.linregress(x, y)
77 y_detrended = y - slope * x - offset
78 return y_detrended, slope, offset
81def retrend(x, y_detrended, slope, offset):
82 return x * slope + y_detrended + offset
85class Global(object):
86 appOnDemand = None
89class NSLC(object):
90 def __init__(self, n, s, l=None, c=None): # noqa
91 self.network = n
92 self.station = s
93 self.location = l
94 self.channel = c
97class m_float(float):
99 def __str__(self):
100 if abs(self) >= 10000.:
101 return '%g km' % round(self/1000., 0)
102 elif abs(self) >= 1000.:
103 return '%g km' % round(self/1000., 1)
104 else:
105 return '%.5g m' % self
107 def __lt__(self, other):
108 if other is None:
109 return True
110 return float(self) < float(other)
112 def __gt__(self, other):
113 if other is None:
114 return False
115 return float(self) > float(other)
118def m_float_or_none(x):
119 if x is None:
120 return None
121 else:
122 return m_float(x)
125def make_chunks(items):
126 '''
127 Split a list of integers into sublists of consecutive elements.
128 '''
129 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
130 enumerate(items), (lambda x: x[1]-x[0]))]
133class deg_float(float):
135 def __str__(self):
136 return '%4.0f' % self
139def deg_float_or_none(x):
140 if x is None:
141 return None
142 else:
143 return deg_float(x)
146class sector_int(int):
148 def __str__(self):
149 return '[%i]' % self
152def num_to_html(num):
153 snum = '%g' % num
154 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
155 if m:
156 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
158 return snum
161gap_lap_tolerance = 5.
164class ViewMode(enum.Enum):
165 Wiggle = 1
166 Waterfall = 2
169class Timer(object):
170 def __init__(self):
171 self._start = None
172 self._stop = None
174 def start(self):
175 self._start = os.times()
177 def stop(self):
178 self._stop = os.times()
180 def get(self):
181 a = self._start
182 b = self._stop
183 if a is not None and b is not None:
184 return tuple([b[i] - a[i] for i in range(5)])
185 else:
186 return tuple([0.] * 5)
188 def __sub__(self, other):
189 a = self.get()
190 b = other.get()
191 return tuple([a[i] - b[i] for i in range(5)])
194class ObjectStyle(object):
195 def __init__(self, frame_pen, fill_brush):
196 self.frame_pen = frame_pen
197 self.fill_brush = fill_brush
200box_styles = []
201box_alpha = 100
202for color in 'orange skyblue butter chameleon chocolate plum ' \
203 'scarletred'.split():
205 box_styles.append(ObjectStyle(
206 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
207 qg.QBrush(qg.QColor(
208 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
209 ))
211box_styles_coverage = [
212 ObjectStyle(
213 qg.QPen(
214 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
215 1, qc.Qt.DashLine),
216 qg.QBrush(qg.QColor(
217 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
218 ),
219 ObjectStyle(
220 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
221 qg.QBrush(qg.QColor(
222 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
223 ),
224 ObjectStyle(
225 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
226 qg.QBrush(qg.QColor(
227 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
228 )]
230sday = 60*60*24. # \
231smonth = 60*60*24*30. # | only used as approx. intervals...
232syear = 60*60*24*365. # /
234acceptable_tincs = num.array([
235 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
236 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
239working_system_time_range = \
240 pyrocko.util.working_system_time_range()
242initial_time_range = []
244try:
245 initial_time_range.append(
246 calendar.timegm((1950, 1, 1, 0, 0, 0)))
247except Exception:
248 initial_time_range.append(working_system_time_range[0])
250try:
251 initial_time_range.append(
252 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
253except Exception:
254 initial_time_range.append(working_system_time_range[1])
257def is_working_time(t):
258 return working_system_time_range[0] <= t and \
259 t <= working_system_time_range[1]
262def fancy_time_ax_format(inc):
263 l0_fmt_brief = ''
264 l2_fmt = ''
265 l2_trig = 0
266 if inc < 0.000001:
267 l0_fmt = '.%n'
268 l0_center = False
269 l1_fmt = '%H:%M:%S'
270 l1_trig = 6
271 l2_fmt = '%b %d, %Y'
272 l2_trig = 3
273 elif inc < 0.001:
274 l0_fmt = '.%u'
275 l0_center = False
276 l1_fmt = '%H:%M:%S'
277 l1_trig = 6
278 l2_fmt = '%b %d, %Y'
279 l2_trig = 3
280 elif inc < 1:
281 l0_fmt = '.%r'
282 l0_center = False
283 l1_fmt = '%H:%M:%S'
284 l1_trig = 6
285 l2_fmt = '%b %d, %Y'
286 l2_trig = 3
287 elif inc < 60:
288 l0_fmt = '%H:%M:%S'
289 l0_center = False
290 l1_fmt = '%b %d, %Y'
291 l1_trig = 3
292 elif inc < 3600:
293 l0_fmt = '%H:%M'
294 l0_center = False
295 l1_fmt = '%b %d, %Y'
296 l1_trig = 3
297 elif inc < sday:
298 l0_fmt = '%H:%M'
299 l0_center = False
300 l1_fmt = '%b %d, %Y'
301 l1_trig = 3
302 elif inc < smonth:
303 l0_fmt = '%a %d'
304 l0_fmt_brief = '%d'
305 l0_center = True
306 l1_fmt = '%b, %Y'
307 l1_trig = 2
308 elif inc < syear:
309 l0_fmt = '%b'
310 l0_center = True
311 l1_fmt = '%Y'
312 l1_trig = 1
313 else:
314 l0_fmt = '%Y'
315 l0_center = False
316 l1_fmt = ''
317 l1_trig = 0
319 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
322def day_start(timestamp):
323 tt = time.gmtime(int(timestamp))
324 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
325 return calendar.timegm(tts)
328def month_start(timestamp):
329 tt = time.gmtime(int(timestamp))
330 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
331 return calendar.timegm(tts)
334def year_start(timestamp):
335 tt = time.gmtime(int(timestamp))
336 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
337 return calendar.timegm(tts)
340def time_nice_value(inc0):
341 if inc0 < acceptable_tincs[0]:
342 return pyrocko.plot.nice_value(inc0)
343 elif inc0 > acceptable_tincs[-1]:
344 return pyrocko.plot.nice_value(inc0/syear)*syear
345 else:
346 i = num.argmin(num.abs(acceptable_tincs-inc0))
347 return acceptable_tincs[i]
350class TimeScaler(pyrocko.plot.AutoScaler):
351 def __init__(self):
352 pyrocko.plot.AutoScaler.__init__(self)
353 self.mode = 'min-max'
355 def make_scale(self, data_range):
356 assert self.mode in ('min-max', 'off'), \
357 'mode must be "min-max" or "off" for TimeScaler'
359 data_min = min(data_range)
360 data_max = max(data_range)
361 is_reverse = (data_range[0] > data_range[1])
363 mi, ma = data_min, data_max
364 nmi = mi
365 if self.mode != 'off':
366 nmi = mi - self.space*(ma-mi)
368 nma = ma
369 if self.mode != 'off':
370 nma = ma + self.space*(ma-mi)
372 mi, ma = nmi, nma
374 if mi == ma and self.mode != 'off':
375 mi -= 1.0
376 ma += 1.0
378 mi = max(working_system_time_range[0], mi)
379 ma = min(working_system_time_range[1], ma)
381 # make nice tick increment
382 if self.inc is not None:
383 inc = self.inc
384 else:
385 if self.approx_ticks > 0.:
386 inc = time_nice_value((ma-mi)/self.approx_ticks)
387 else:
388 inc = time_nice_value((ma-mi)*10.)
390 if inc == 0.0:
391 inc = 1.0
393 if is_reverse:
394 return ma, mi, -inc
395 else:
396 return mi, ma, inc
398 def make_ticks(self, data_range):
399 mi, ma, inc = self.make_scale(data_range)
401 is_reverse = False
402 if inc < 0:
403 mi, ma, inc = ma, mi, -inc
404 is_reverse = True
406 ticks = []
408 if inc < sday:
409 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
410 if inc < 0.001:
411 mi_day = hpfloat(mi_day)
413 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
414 if inc < 0.001:
415 base = hpfloat(base)
417 base_day = mi_day
418 i = 0
419 while True:
420 tick = base+i*inc
421 if tick > ma:
422 break
424 tick_day = day_start(tick)
425 if tick_day > base_day:
426 base_day = tick_day
427 base = base_day
428 i = 0
429 else:
430 ticks.append(tick)
431 i += 1
433 elif inc < smonth:
434 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
435 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
436 delta = datetime.timedelta(days=int(round(inc/sday)))
437 if mi_day == mi:
438 dt_base += delta
439 i = 0
440 while True:
441 current = dt_base + i*delta
442 tick = calendar.timegm(current.timetuple())
443 if tick > ma:
444 break
445 ticks.append(tick)
446 i += 1
448 elif inc < syear:
449 mi_month = month_start(max(
450 mi, working_system_time_range[0]+smonth*1.5))
452 y, m = time.gmtime(mi_month)[:2]
453 while True:
454 tick = calendar.timegm((y, m, 1, 0, 0, 0))
455 m += 1
456 if m > 12:
457 y, m = y+1, 1
459 if tick > ma:
460 break
462 if tick >= mi:
463 ticks.append(tick)
465 else:
466 mi_year = year_start(max(
467 mi, working_system_time_range[0]+syear*1.5))
469 incy = int(round(inc/syear))
470 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
472 while True:
473 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
474 y += incy
475 if tick > ma:
476 break
477 if tick >= mi:
478 ticks.append(tick)
480 if is_reverse:
481 ticks.reverse()
483 return ticks, inc
486def need_l1_tick(tt, ms, l1_trig):
487 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
490def tick_to_labels(tick, inc):
491 tt, ms = gmtime_x(tick)
492 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
493 fancy_time_ax_format(inc)
495 l0 = mystrftime(l0_fmt, tt, ms)
496 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
497 l1, l2 = None, None
498 if need_l1_tick(tt, ms, l1_trig):
499 l1 = mystrftime(l1_fmt, tt, ms)
500 if need_l1_tick(tt, ms, l2_trig):
501 l2 = mystrftime(l2_fmt, tt, ms)
503 return l0, l0_brief, l0_center, l1, l2
506def l1_l2_tick(tick, inc):
507 tt, ms = gmtime_x(tick)
508 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
509 fancy_time_ax_format(inc)
511 l1 = mystrftime(l1_fmt, tt, ms)
512 l2 = mystrftime(l2_fmt, tt, ms)
513 return l1, l2
516class TimeAx(TimeScaler):
517 def __init__(self, *args):
518 TimeScaler.__init__(self, *args)
520 def drawit(self, p, xprojection, yprojection):
521 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
522 p.setPen(pen)
523 font = qg.QFont()
524 font.setBold(True)
525 p.setFont(font)
526 fm = p.fontMetrics()
527 ticklen = 10
528 pad = 10
529 tmin, tmax = xprojection.get_in_range()
530 ticks, inc = self.make_ticks((tmin, tmax))
531 l1_hits = 0
532 l2_hits = 0
534 vmin, vmax = yprojection(0), yprojection(ticklen)
535 uumin, uumax = xprojection.get_out_range()
536 first_tick_with_label = None
537 for tick in ticks:
538 umin = xprojection(tick)
540 umin_approx_next = xprojection(tick+inc)
541 umax = xprojection(tick)
543 pinc_approx = umin_approx_next - umin
545 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
546 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
548 if tick == 0.0 and tmax - tmin < 3600*24:
549 # hide year at epoch (we assume that synthetic data is shown)
550 if l2:
551 l2 = None
552 elif l1:
553 l1 = None
555 if l0_center:
556 ushift = (umin_approx_next-umin)/2.
557 else:
558 ushift = 0.
560 for l0x in (l0, l0_brief, ''):
561 label0 = l0x
562 rect0 = fm.boundingRect(label0)
563 if rect0.width() <= pinc_approx*0.9:
564 break
566 if uumin+pad < umin-rect0.width()/2.+ushift and \
567 umin+rect0.width()/2.+ushift < uumax-pad:
569 if first_tick_with_label is None:
570 first_tick_with_label = tick
571 p.drawText(qc.QPointF(
572 umin-rect0.width()/2.+ushift,
573 vmin+rect0.height()+ticklen), label0)
575 if l1:
576 label1 = l1
577 rect1 = fm.boundingRect(label1)
578 if uumin+pad < umin-rect1.width()/2. and \
579 umin+rect1.width()/2. < uumax-pad:
581 p.drawText(qc.QPointF(
582 umin-rect1.width()/2.,
583 vmin+rect0.height()+rect1.height()+ticklen),
584 label1)
586 l1_hits += 1
588 if l2:
589 label2 = l2
590 rect2 = fm.boundingRect(label2)
591 if uumin+pad < umin-rect2.width()/2. and \
592 umin+rect2.width()/2. < uumax-pad:
594 p.drawText(qc.QPointF(
595 umin-rect2.width()/2.,
596 vmin+rect0.height()+rect1.height()+rect2.height() +
597 ticklen), label2)
599 l2_hits += 1
601 if first_tick_with_label is None:
602 first_tick_with_label = tmin
604 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
606 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
607 tmax - tmin < 3600*24:
609 # hide year at epoch (we assume that synthetic data is shown)
610 if l2:
611 l2 = None
612 elif l1:
613 l1 = None
615 if l1_hits == 0 and l1:
616 label1 = l1
617 rect1 = fm.boundingRect(label1)
618 p.drawText(qc.QPointF(
619 uumin+pad,
620 vmin+rect0.height()+rect1.height()+ticklen),
621 label1)
623 l1_hits += 1
625 if l2_hits == 0 and l2:
626 label2 = l2
627 rect2 = fm.boundingRect(label2)
628 p.drawText(qc.QPointF(
629 uumin+pad,
630 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
631 label2)
633 v = yprojection(0)
634 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
637class Projection(object):
638 def __init__(self):
639 self.xr = 0., 1.
640 self.ur = 0., 1.
642 def set_in_range(self, xmin, xmax):
643 if xmax == xmin:
644 xmax = xmin + 1.
646 self.xr = xmin, xmax
648 def get_in_range(self):
649 return self.xr
651 def set_out_range(self, umin, umax):
652 if umax == umin:
653 umax = umin + 1.
655 self.ur = umin, umax
657 def get_out_range(self):
658 return self.ur
660 def __call__(self, x):
661 umin, umax = self.ur
662 xmin, xmax = self.xr
663 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
665 def clipped(self, x):
666 umin, umax = self.ur
667 xmin, xmax = self.xr
668 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
670 def rev(self, u):
671 umin, umax = self.ur
672 xmin, xmax = self.xr
673 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
675 def copy(self):
676 return copy.copy(self)
679def add_radiobuttongroup(menu, menudef, target, default=None):
680 group = qw.QActionGroup(menu)
681 group.setExclusive(True)
682 menuitems = []
684 for name, value, *shortcut in menudef:
685 action = menu.addAction(name)
686 action.setCheckable(True)
687 action.setActionGroup(group)
688 if shortcut:
689 action.setShortcut(shortcut[0])
691 menuitems.append((action, value))
692 if default is not None and (
693 name.lower().replace(' ', '_') == default or
694 value == default):
695 action.setChecked(True)
697 group.triggered.connect(target)
699 if default is None:
700 menuitems[0][0].setChecked(True)
702 return menuitems
705def sort_actions(menu):
706 actions = [act for act in menu.actions() if not act.menu()]
707 for action in actions:
708 menu.removeAction(action)
709 actions.sort(key=lambda x: newstr(x.text()))
711 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
712 if help_action:
713 actions.insert(0, actions.pop(actions.index(help_action[0])))
714 for action in actions:
715 menu.addAction(action)
718fkey_map = dict(zip(
719 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
720 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10,
721 qc.Qt.Key_F11, qc.Qt.Key_F12),
722 range(12)))
725class PileViewerMainException(Exception):
726 pass
729class PileViewerMenuBar(qw.QMenuBar):
730 ...
733class PileViewerMenu(qw.QMenu):
734 ...
737def MakePileViewerMainClass(base):
739 class PileViewerMain(base):
741 want_input = qc.pyqtSignal()
742 about_to_close = qc.pyqtSignal()
743 pile_has_changed_signal = qc.pyqtSignal()
744 tracks_range_changed = qc.pyqtSignal(int, int, int)
746 begin_markers_add = qc.pyqtSignal(int, int)
747 end_markers_add = qc.pyqtSignal()
748 begin_markers_remove = qc.pyqtSignal(int, int)
749 end_markers_remove = qc.pyqtSignal()
751 marker_selection_changed = qc.pyqtSignal(list)
752 active_event_marker_changed = qc.pyqtSignal()
754 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
755 menu=None):
756 if base == qgl.QGLWidget:
757 from OpenGL import GL # noqa
759 base.__init__(
760 self, qgl.QGLFormat(qgl.QGL.SampleBuffers), *args)
761 else:
762 base.__init__(self, *args)
764 self.pile = pile
765 self.ax_height = 80
766 self.panel_parent = panel_parent
768 self.click_tolerance = 5
770 self.ntracks_shown_max = ntracks_shown_max
771 self.initial_ntracks_shown_max = ntracks_shown_max
772 self.ntracks = 0
773 self.show_all = True
774 self.shown_tracks_range = None
775 self.track_start = None
776 self.track_trange = None
778 self.lowpass = None
779 self.highpass = None
780 self.gain = 1.0
781 self.rotate = 0.0
782 self.picking_down = None
783 self.picking = None
784 self.floating_marker = None
785 self.markers = pyrocko.pile.Sorted([], 'tmin')
786 self.markers_deltat_max = 0.
787 self.n_selected_markers = 0
788 self.all_marker_kinds = (0, 1, 2, 3, 4, 5)
789 self.visible_marker_kinds = self.all_marker_kinds
790 self.active_event_marker = None
791 self.ignore_releases = 0
792 self.message = None
793 self.reloaded = False
794 self.pile_has_changed = False
795 self.config = pyrocko.config.config('snuffler')
797 self.tax = TimeAx()
798 self.setBackgroundRole(qg.QPalette.Base)
799 self.setAutoFillBackground(True)
800 poli = qw.QSizePolicy(
801 qw.QSizePolicy.Expanding,
802 qw.QSizePolicy.Expanding)
804 self.setSizePolicy(poli)
805 self.setMinimumSize(300, 200)
806 self.setFocusPolicy(qc.Qt.ClickFocus)
808 self.menu = menu or PileViewerMenu(self)
810 file_menu = self.menu.addMenu('&File')
811 view_menu = self.menu.addMenu('&View')
812 options_menu = self.menu.addMenu('&Options')
813 scale_menu = self.menu.addMenu('&Scaling')
814 sort_menu = self.menu.addMenu('Sor&ting')
815 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
817 help_menu = self.menu.addMenu('&Help')
819 self.snufflings_menu = self.toggle_panel_menu.addMenu(
820 'Run Snuffling')
821 self.toggle_panel_menu.addSeparator()
822 self.snuffling_help = help_menu.addMenu('Snuffling Help')
823 help_menu.addSeparator()
825 file_menu.addAction(
826 qg.QIcon.fromTheme('document-open'),
827 'Open waveform files...',
828 self.open_waveforms,
829 qg.QKeySequence.Open)
831 file_menu.addAction(
832 qg.QIcon.fromTheme('document-open'),
833 'Open waveform directory...',
834 self.open_waveform_directory)
836 file_menu.addAction(
837 'Open station files...',
838 self.open_stations)
840 file_menu.addAction(
841 'Open StationXML files...',
842 self.open_stations_xml)
844 file_menu.addAction(
845 'Open event file...',
846 self.read_events)
848 file_menu.addSeparator()
849 file_menu.addAction(
850 'Open marker file...',
851 self.read_markers)
853 file_menu.addAction(
854 qg.QIcon.fromTheme('document-save'),
855 'Save markers...',
856 self.write_markers,
857 qg.QKeySequence.Save)
859 file_menu.addAction(
860 qg.QIcon.fromTheme('document-save-as'),
861 'Save selected markers...',
862 self.write_selected_markers,
863 qg.QKeySequence.SaveAs)
865 file_menu.addSeparator()
866 file_menu.addAction(
867 qg.QIcon.fromTheme('document-print'),
868 'Print',
869 self.printit,
870 qg.QKeySequence.Print)
872 file_menu.addAction(
873 qg.QIcon.fromTheme('insert-image'),
874 'Save as SVG or PNG',
875 self.savesvg,
876 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
878 file_menu.addSeparator()
879 close = file_menu.addAction(
880 qg.QIcon.fromTheme('window-close'),
881 'Close',
882 self.myclose)
883 close.setShortcuts(
884 (qg.QKeySequence(qc.Qt.Key_Q),
885 qg.QKeySequence(qc.Qt.Key_X)))
887 # Scale Menu
888 menudef = [
889 ('Individual Scale',
890 lambda tr: tr.nslc_id,
891 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
892 ('Common Scale',
893 lambda tr: None,
894 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
895 ('Common Scale per Station',
896 lambda tr: (tr.network, tr.station),
897 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
898 ('Common Scale per Station Location',
899 lambda tr: (tr.network, tr.station, tr.location)),
900 ('Common Scale per Component',
901 lambda tr: (tr.channel)),
902 ]
904 self.menuitems_scaling = add_radiobuttongroup(
905 scale_menu, menudef, self.scalingmode_change,
906 default=self.config.trace_scale)
907 scale_menu.addSeparator()
909 self.scaling_key = self.menuitems_scaling[0][1]
910 self.scaling_hooks = {}
911 self.scalingmode_change()
913 menudef = [
914 ('Scaling based on Minimum and Maximum', 'minmax'),
915 ('Scaling based on Mean ± 2x Std. Deviation', 2),
916 ('Scaling based on Mean ± 4x Std. Deviation', 4),
917 ]
919 self.menuitems_scaling_base = add_radiobuttongroup(
920 scale_menu, menudef, self.scaling_base_change)
922 self.scaling_base = self.menuitems_scaling_base[0][1]
923 scale_menu.addSeparator()
925 self.menuitem_fixscalerange = scale_menu.addAction(
926 'Fix Scale Ranges')
927 self.menuitem_fixscalerange.setCheckable(True)
929 # Sort Menu
930 def sector_dist(sta):
931 if sta.dist_m is None:
932 return None, None
933 else:
934 return (
935 sector_int(round((sta.azimuth+15.)/30.)),
936 m_float(sta.dist_m))
938 menudef = [
939 ('Sort by Names',
940 lambda tr: (),
941 qg.QKeySequence(qc.Qt.Key_N)),
942 ('Sort by Distance',
943 lambda tr: self.station_attrib(
944 tr,
945 lambda sta: (m_float_or_none(sta.dist_m),),
946 lambda tr: (None,)),
947 qg.QKeySequence(qc.Qt.Key_D)),
948 ('Sort by Azimuth',
949 lambda tr: self.station_attrib(
950 tr,
951 lambda sta: (deg_float_or_none(sta.azimuth),),
952 lambda tr: (None,))),
953 ('Sort by Distance in 12 Azimuthal Blocks',
954 lambda tr: self.station_attrib(
955 tr,
956 sector_dist,
957 lambda tr: (None, None))),
958 ('Sort by Backazimuth',
959 lambda tr: self.station_attrib(
960 tr,
961 lambda sta: (deg_float_or_none(sta.backazimuth),),
962 lambda tr: (None,))),
963 ]
964 self.menuitems_ssorting = add_radiobuttongroup(
965 sort_menu, menudef, self.s_sortingmode_change)
966 sort_menu.addSeparator()
968 self._ssort = lambda tr: ()
970 self.menu.addSeparator()
972 menudef = [
973 ('Subsort by Network, Station, Location, Channel',
974 ((0, 1, 2, 3), # gathering
975 lambda tr: tr.location)), # coloring
976 ('Subsort by Network, Station, Channel, Location',
977 ((0, 1, 3, 2),
978 lambda tr: tr.channel)),
979 ('Subsort by Station, Network, Channel, Location',
980 ((1, 0, 3, 2),
981 lambda tr: tr.channel)),
982 ('Subsort by Location, Network, Station, Channel',
983 ((2, 0, 1, 3),
984 lambda tr: tr.channel)),
985 ('Subsort by Channel, Network, Station, Location',
986 ((3, 0, 1, 2),
987 lambda tr: (tr.network, tr.station, tr.location))),
988 ('Subsort by Network, Station, Channel (Grouped by Location)',
989 ((0, 1, 3),
990 lambda tr: tr.location)),
991 ('Subsort by Station, Network, Channel (Grouped by Location)',
992 ((1, 0, 3),
993 lambda tr: tr.location)),
994 ]
996 self.menuitems_sorting = add_radiobuttongroup(
997 sort_menu, menudef, self.sortingmode_change)
999 menudef = [(x.key, x.value) for x in
1000 self.config.visible_length_setting]
1002 # View menu
1003 self.menuitems_visible_length = add_radiobuttongroup(
1004 view_menu, menudef,
1005 self.visible_length_change)
1006 view_menu.addSeparator()
1008 view_modes = [
1009 ('Wiggle Plot', ViewMode.Wiggle),
1010 ('Waterfall', ViewMode.Waterfall)
1011 ]
1013 self.menuitems_viewmode = add_radiobuttongroup(
1014 view_menu, view_modes,
1015 self.viewmode_change, default=ViewMode.Wiggle)
1016 view_menu.addSeparator()
1018 self.menuitem_cliptraces = view_menu.addAction(
1019 'Clip Traces')
1020 self.menuitem_cliptraces.setCheckable(True)
1021 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1023 self.menuitem_showboxes = view_menu.addAction(
1024 'Show Boxes')
1025 self.menuitem_showboxes.setCheckable(True)
1026 self.menuitem_showboxes.setChecked(
1027 self.config.show_boxes)
1029 self.menuitem_colortraces = view_menu.addAction(
1030 'Color Traces')
1031 self.menuitem_colortraces.setCheckable(True)
1032 self.menuitem_antialias = view_menu.addAction(
1033 'Antialiasing')
1034 self.menuitem_antialias.setCheckable(True)
1036 view_menu.addSeparator()
1037 self.menuitem_showscalerange = view_menu.addAction(
1038 'Show Scale Ranges')
1039 self.menuitem_showscalerange.setCheckable(True)
1040 self.menuitem_showscalerange.setChecked(
1041 self.config.show_scale_ranges)
1043 self.menuitem_showscaleaxis = view_menu.addAction(
1044 'Show Scale Axes')
1045 self.menuitem_showscaleaxis.setCheckable(True)
1046 self.menuitem_showscaleaxis.setChecked(
1047 self.config.show_scale_axes)
1049 self.menuitem_showzeroline = view_menu.addAction(
1050 'Show Zero Lines')
1051 self.menuitem_showzeroline.setCheckable(True)
1053 view_menu.addSeparator()
1054 view_menu.addAction(
1055 qg.QIcon.fromTheme('view-fullscreen'),
1056 'Fullscreen',
1057 self.toggle_fullscreen,
1058 qg.QKeySequence(qc.Qt.Key_F11))
1060 # Options Menu
1061 self.menuitem_demean = options_menu.addAction('Demean')
1062 self.menuitem_demean.setCheckable(True)
1063 self.menuitem_demean.setChecked(self.config.demean)
1064 self.menuitem_demean.setShortcut(
1065 qg.QKeySequence(qc.Qt.Key_Underscore))
1067 self.menuitem_distances_3d = options_menu.addAction(
1068 '3D distances',
1069 self.distances_3d_changed)
1070 self.menuitem_distances_3d.setCheckable(True)
1072 self.menuitem_allowdownsampling = options_menu.addAction(
1073 'Allow Downsampling')
1074 self.menuitem_allowdownsampling.setCheckable(True)
1075 self.menuitem_allowdownsampling.setChecked(True)
1077 self.menuitem_degap = options_menu.addAction(
1078 'Allow Degapping')
1079 self.menuitem_degap.setCheckable(True)
1080 self.menuitem_degap.setChecked(True)
1082 options_menu.addSeparator()
1084 self.menuitem_fft_filtering = options_menu.addAction(
1085 'FFT Filtering')
1086 self.menuitem_fft_filtering.setCheckable(True)
1088 self.menuitem_lphp = options_menu.addAction(
1089 'Bandpass is Low- + Highpass')
1090 self.menuitem_lphp.setCheckable(True)
1091 self.menuitem_lphp.setChecked(True)
1093 options_menu.addSeparator()
1094 self.menuitem_watch = options_menu.addAction(
1095 'Watch Files')
1096 self.menuitem_watch.setCheckable(True)
1098 self.menuitem_liberal_fetch = options_menu.addAction(
1099 'Liberal Fetch Optimization')
1100 self.menuitem_liberal_fetch.setCheckable(True)
1102 self.visible_length = menudef[0][1]
1104 self.snufflings_menu.addAction(
1105 'Reload Snufflings',
1106 self.setup_snufflings)
1108 # Disable ShadowPileTest
1109 if False:
1110 test_action = self.menu.addAction(
1111 'Test',
1112 self.toggletest)
1113 test_action.setCheckable(True)
1115 help_menu.addAction(
1116 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1117 'Snuffler Controls',
1118 self.help,
1119 qg.QKeySequence(qc.Qt.Key_Question))
1121 help_menu.addAction(
1122 'About',
1123 self.about)
1125 self.time_projection = Projection()
1126 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1127 self.time_projection.set_out_range(0., self.width())
1129 self.gather = None
1131 self.trace_filter = None
1132 self.quick_filter = None
1133 self.quick_filter_patterns = None, None
1134 self.blacklist = []
1136 self.track_to_screen = Projection()
1137 self.track_to_nslc_ids = {}
1139 self.cached_vec = None
1140 self.cached_processed_traces = None
1141 self.cached_chopped_traces = {}
1143 self.timer = qc.QTimer(self)
1144 self.timer.timeout.connect(self.periodical)
1145 self.timer.setInterval(1000)
1146 self.timer.start()
1147 self.pile.add_listener(self)
1148 self.trace_styles = {}
1149 if self.get_squirrel() is None:
1150 self.determine_box_styles()
1152 self.setMouseTracking(True)
1154 user_home_dir = os.path.expanduser('~')
1155 self.snuffling_modules = {}
1156 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1157 self.default_snufflings = None
1158 self.snufflings = []
1160 self.stations = {}
1162 self.timer_draw = Timer()
1163 self.timer_cutout = Timer()
1164 self.time_spent_painting = 0.0
1165 self.time_last_painted = time.time()
1167 self.interactive_range_change_time = 0.0
1168 self.interactive_range_change_delay_time = 10.0
1169 self.follow_timer = None
1171 self.sortingmode_change_time = 0.0
1172 self.sortingmode_change_delay_time = None
1174 self.old_data_ranges = {}
1176 self.error_messages = {}
1177 self.return_tag = None
1178 self.wheel_pos = 60
1180 self.setAcceptDrops(True)
1181 self._paths_to_load = []
1183 self.tf_cache = {}
1185 self.waterfall = TraceWaterfall()
1186 self.waterfall_cmap = 'viridis'
1187 self.waterfall_clip_min = 0.
1188 self.waterfall_clip_max = 1.
1189 self.waterfall_show_absolute = False
1190 self.waterfall_integrate = False
1191 self.view_mode = ViewMode.Wiggle
1193 self.automatic_updates = True
1195 self.closing = False
1196 self.paint_timer = qc.QTimer(self)
1197 self.paint_timer.timeout.connect(self.reset_updates)
1198 self.paint_timer.setInterval(20)
1199 self.paint_timer.start()
1201 @qc.pyqtSlot()
1202 def reset_updates(self):
1203 if not self.updatesEnabled():
1204 self.setUpdatesEnabled(True)
1206 def fail(self, reason):
1207 box = qw.QMessageBox(self)
1208 box.setText(reason)
1209 box.exec_()
1211 def set_trace_filter(self, filter_func):
1212 self.trace_filter = filter_func
1213 self.sortingmode_change()
1215 def update_trace_filter(self):
1216 if self.blacklist:
1218 def blacklist_func(tr):
1219 return not pyrocko.util.match_nslc(
1220 self.blacklist, tr.nslc_id)
1222 else:
1223 blacklist_func = None
1225 if self.quick_filter is None and blacklist_func is None:
1226 self.set_trace_filter(None)
1227 elif self.quick_filter is None:
1228 self.set_trace_filter(blacklist_func)
1229 elif blacklist_func is None:
1230 self.set_trace_filter(self.quick_filter)
1231 else:
1232 self.set_trace_filter(
1233 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1235 def set_quick_filter(self, filter_func):
1236 self.quick_filter = filter_func
1237 self.update_trace_filter()
1239 def set_quick_filter_patterns(self, patterns, inputline=None):
1240 if patterns is not None:
1241 self.set_quick_filter(
1242 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1243 else:
1244 self.set_quick_filter(None)
1246 self.quick_filter_patterns = patterns, inputline
1248 def get_quick_filter_patterns(self):
1249 return self.quick_filter_patterns
1251 def add_blacklist_pattern(self, pattern):
1252 if pattern == 'empty':
1253 keys = set(self.pile.nslc_ids)
1254 trs = self.pile.all(
1255 tmin=self.tmin,
1256 tmax=self.tmax,
1257 load_data=False,
1258 degap=False)
1260 for tr in trs:
1261 if tr.nslc_id in keys:
1262 keys.remove(tr.nslc_id)
1264 for key in keys:
1265 xpattern = '.'.join(key)
1266 if xpattern not in self.blacklist:
1267 self.blacklist.append(xpattern)
1269 else:
1270 if pattern in self.blacklist:
1271 self.blacklist.remove(pattern)
1273 self.blacklist.append(pattern)
1275 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1276 self.update_trace_filter()
1278 def remove_blacklist_pattern(self, pattern):
1279 if pattern in self.blacklist:
1280 self.blacklist.remove(pattern)
1281 else:
1282 raise PileViewerMainException(
1283 'Pattern not found in blacklist.')
1285 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1286 self.update_trace_filter()
1288 def clear_blacklist(self):
1289 self.blacklist = []
1290 self.update_trace_filter()
1292 def ssort(self, tr):
1293 return self._ssort(tr)
1295 def station_key(self, x):
1296 return x.network, x.station
1298 def station_keys(self, x):
1299 return [
1300 (x.network, x.station, x.location),
1301 (x.network, x.station)]
1303 def station_attrib(self, tr, getter, default_getter):
1304 for sk in self.station_keys(tr):
1305 if sk in self.stations:
1306 station = self.stations[sk]
1307 return getter(station)
1309 return default_getter(tr)
1311 def get_station(self, sk):
1312 return self.stations[sk]
1314 def has_station(self, station):
1315 for sk in self.station_keys(station):
1316 if sk in self.stations:
1317 return True
1319 return False
1321 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1322 return self.station_attrib(
1323 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1325 def set_stations(self, stations):
1326 self.stations = {}
1327 self.add_stations(stations)
1329 def add_stations(self, stations):
1330 for station in stations:
1331 for sk in self.station_keys(station):
1332 self.stations[sk] = station
1334 ev = self.get_active_event()
1335 if ev:
1336 self.set_origin(ev)
1338 def add_event(self, event):
1339 marker = EventMarker(event)
1340 self.add_marker(marker)
1342 def add_events(self, events):
1343 markers = [EventMarker(e) for e in events]
1344 self.add_markers(markers)
1346 def set_event_marker_as_origin(self, ignore=None):
1347 selected = self.selected_markers()
1348 if not selected:
1349 self.fail('An event marker must be selected.')
1350 return
1352 m = selected[0]
1353 if not isinstance(m, EventMarker):
1354 self.fail('Selected marker is not an event.')
1355 return
1357 self.set_active_event_marker(m)
1359 def deactivate_event_marker(self):
1360 if self.active_event_marker:
1361 self.active_event_marker.active = False
1363 self.active_event_marker_changed.emit()
1364 self.active_event_marker = None
1366 def set_active_event_marker(self, event_marker):
1367 if self.active_event_marker:
1368 self.active_event_marker.active = False
1370 self.active_event_marker = event_marker
1371 event_marker.active = True
1372 event = event_marker.get_event()
1373 self.set_origin(event)
1374 self.active_event_marker_changed.emit()
1376 def set_active_event(self, event):
1377 for marker in self.markers:
1378 if isinstance(marker, EventMarker):
1379 if marker.get_event() is event:
1380 self.set_active_event_marker(marker)
1382 def get_active_event_marker(self):
1383 return self.active_event_marker
1385 def get_active_event(self):
1386 m = self.get_active_event_marker()
1387 if m is not None:
1388 return m.get_event()
1389 else:
1390 return None
1392 def get_active_markers(self):
1393 emarker = self.get_active_event_marker()
1394 if emarker is None:
1395 return None, []
1397 else:
1398 ev = emarker.get_event()
1399 pmarkers = [
1400 m for m in self.markers
1401 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1403 return emarker, pmarkers
1405 def set_origin(self, location):
1406 for station in self.stations.values():
1407 station.set_event_relative_data(
1408 location,
1409 distance_3d=self.menuitem_distances_3d.isChecked())
1411 self.sortingmode_change()
1413 def distances_3d_changed(self):
1414 ignore = self.menuitem_distances_3d.isChecked()
1415 self.set_event_marker_as_origin(ignore)
1417 def iter_snuffling_modules(self):
1418 pjoin = os.path.join
1419 for path in self.snuffling_paths:
1421 if not os.path.isdir(path):
1422 os.mkdir(path)
1424 for entry in os.listdir(path):
1425 directory = path
1426 fn = entry
1427 d = pjoin(path, entry)
1428 if os.path.isdir(d):
1429 directory = d
1430 if os.path.isfile(
1431 os.path.join(directory, 'snuffling.py')):
1432 fn = 'snuffling.py'
1434 if not fn.endswith('.py'):
1435 continue
1437 name = fn[:-3]
1439 if (directory, name) not in self.snuffling_modules:
1440 self.snuffling_modules[directory, name] = \
1441 pyrocko.gui.snuffling.SnufflingModule(
1442 directory, name, self)
1444 yield self.snuffling_modules[directory, name]
1446 def setup_snufflings(self):
1447 # user snufflings
1448 for mod in self.iter_snuffling_modules():
1449 try:
1450 mod.load_if_needed()
1451 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1452 logger.warning('Snuffling module "%s" is broken' % e)
1454 # load the default snufflings on first run
1455 if self.default_snufflings is None:
1456 self.default_snufflings = pyrocko.gui\
1457 .snufflings.__snufflings__()
1458 for snuffling in self.default_snufflings:
1459 self.add_snuffling(snuffling)
1461 def set_panel_parent(self, panel_parent):
1462 self.panel_parent = panel_parent
1464 def get_panel_parent(self):
1465 return self.panel_parent
1467 def add_snuffling(self, snuffling, reloaded=False):
1468 logger.debug('Adding snuffling %s' % snuffling.get_name())
1469 snuffling.init_gui(
1470 self, self.get_panel_parent(), self, reloaded=reloaded)
1471 self.snufflings.append(snuffling)
1472 self.update()
1474 def remove_snuffling(self, snuffling):
1475 snuffling.delete_gui()
1476 self.update()
1477 self.snufflings.remove(snuffling)
1478 snuffling.pre_destroy()
1480 def add_snuffling_menuitem(self, item):
1481 self.snufflings_menu.addAction(item)
1482 item.setParent(self.snufflings_menu)
1483 sort_actions(self.snufflings_menu)
1485 def remove_snuffling_menuitem(self, item):
1486 self.snufflings_menu.removeAction(item)
1488 def add_snuffling_help_menuitem(self, item):
1489 self.snuffling_help.addAction(item)
1490 item.setParent(self.snuffling_help)
1491 sort_actions(self.snuffling_help)
1493 def remove_snuffling_help_menuitem(self, item):
1494 self.snuffling_help.removeAction(item)
1496 def add_panel_toggler(self, item):
1497 self.toggle_panel_menu.addAction(item)
1498 item.setParent(self.toggle_panel_menu)
1499 sort_actions(self.toggle_panel_menu)
1501 def remove_panel_toggler(self, item):
1502 self.toggle_panel_menu.removeAction(item)
1504 def load(self, paths, regex=None, format='from_extension',
1505 cache_dir=None, force_cache=False):
1507 if cache_dir is None:
1508 cache_dir = pyrocko.config.config().cache_dir
1509 if isinstance(paths, str):
1510 paths = [paths]
1512 fns = pyrocko.util.select_files(
1513 paths, selector=None, include=regex, show_progress=False)
1515 if not fns:
1516 return
1518 cache = pyrocko.pile.get_cache(cache_dir)
1520 t = [time.time()]
1522 def update_bar(label, value):
1523 pbs = self.parent().get_progressbars()
1524 if label.lower() == 'looking at files':
1525 label = 'Looking at %i files' % len(fns)
1526 else:
1527 label = 'Scanning %i files' % len(fns)
1529 return pbs.set_status(label, value)
1531 def update_progress(label, i, n):
1532 abort = False
1534 qw.qApp.processEvents()
1535 if n != 0:
1536 perc = i*100/n
1537 else:
1538 perc = 100
1539 abort |= update_bar(label, perc)
1540 abort |= self.window().is_closing()
1542 tnow = time.time()
1543 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1544 self.update()
1545 t[0] = tnow
1547 return abort
1549 self.automatic_updates = False
1551 self.pile.load_files(
1552 sorted(fns),
1553 filename_attributes=regex,
1554 cache=cache,
1555 fileformat=format,
1556 show_progress=False,
1557 update_progress=update_progress)
1559 self.automatic_updates = True
1560 self.update()
1562 def load_queued(self):
1563 if not self._paths_to_load:
1564 return
1565 paths = self._paths_to_load
1566 self._paths_to_load = []
1567 self.load(paths)
1569 def load_soon(self, paths):
1570 self._paths_to_load.extend(paths)
1571 qc.QTimer.singleShot(200, self.load_queued)
1573 def open_waveforms(self):
1574 caption = 'Select one or more files to open'
1576 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1577 self, caption, options=qfiledialog_options))
1579 if fns:
1580 self.load(list(str(fn) for fn in fns))
1582 def open_waveform_directory(self):
1583 caption = 'Select directory to scan for waveform files'
1585 dn = qw.QFileDialog.getExistingDirectory(
1586 self, caption, options=qfiledialog_options)
1588 if dn:
1589 self.load([str(dn)])
1591 def open_stations(self, fns=None):
1592 caption = 'Select one or more Pyrocko station files to open'
1594 if not fns:
1595 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1596 self, caption, options=qfiledialog_options))
1598 try:
1599 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1600 for stat in stations:
1601 self.add_stations(stat)
1603 except Exception as e:
1604 self.fail('Failed to read station file: %s' % str(e))
1606 def open_stations_xml(self, fns=None):
1607 from pyrocko.io import stationxml
1609 caption = 'Select one or more StationXML files'
1610 if not fns:
1611 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1612 self, caption, options=qfiledialog_options,
1613 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1614 ';;All files (*)'))
1616 try:
1617 stations = [
1618 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1619 for x in fns]
1621 for stat in stations:
1622 self.add_stations(stat)
1624 except Exception as e:
1625 self.fail('Failed to read StationXML file: %s' % str(e))
1627 def add_traces(self, traces):
1628 if traces:
1629 mtf = pyrocko.pile.MemTracesFile(None, traces)
1630 self.pile.add_file(mtf)
1631 ticket = (self.pile, mtf)
1632 return ticket
1633 else:
1634 return (None, None)
1636 def release_data(self, tickets):
1637 for ticket in tickets:
1638 pile, mtf = ticket
1639 if pile is not None:
1640 pile.remove_file(mtf)
1642 def periodical(self):
1643 if self.menuitem_watch.isChecked():
1644 if self.pile.reload_modified():
1645 self.update()
1647 def get_pile(self):
1648 return self.pile
1650 def pile_changed(self, what):
1651 self.pile_has_changed = True
1652 self.pile_has_changed_signal.emit()
1653 if self.automatic_updates:
1654 self.update()
1656 def set_gathering(self, gather=None, color=None):
1658 if gather is None:
1659 def gather_func(tr):
1660 return tr.nslc_id
1662 gather = (0, 1, 2, 3)
1664 else:
1665 def gather_func(tr):
1666 return (
1667 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1669 if color is None:
1670 def color(tr):
1671 return tr.location
1673 self.gather = gather_func
1674 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1676 self.color_gather = color
1677 self.color_keys = self.pile.gather_keys(color)
1678 previous_ntracks = self.ntracks
1679 self.set_ntracks(len(keys))
1681 if self.shown_tracks_range is None or \
1682 previous_ntracks == 0 or \
1683 self.show_all:
1685 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1686 key_at_top = None
1687 n = high-low
1689 else:
1690 low, high = self.shown_tracks_range
1691 key_at_top = self.track_keys[low]
1692 n = high-low
1694 self.track_keys = sorted(keys)
1696 track_patterns = []
1697 for k in self.track_keys:
1698 pat = ['*', '*', '*', '*']
1699 for i, j in enumerate(gather):
1700 pat[j] = k[-len(gather)+i]
1702 track_patterns.append(pat)
1704 self.track_patterns = track_patterns
1706 if key_at_top is not None:
1707 try:
1708 ind = self.track_keys.index(key_at_top)
1709 low = ind
1710 high = low+n
1711 except Exception:
1712 pass
1714 self.set_tracks_range((low, high))
1716 self.key_to_row = dict(
1717 [(key, i) for (i, key) in enumerate(self.track_keys)])
1719 def inrange(x, r):
1720 return r[0] <= x and x < r[1]
1722 def trace_selector(trace):
1723 gt = self.gather(trace)
1724 return (
1725 gt in self.key_to_row and
1726 inrange(self.key_to_row[gt], self.shown_tracks_range))
1728 if self.trace_filter is not None:
1729 self.trace_selector = lambda x: \
1730 self.trace_filter(x) and trace_selector(x)
1731 else:
1732 self.trace_selector = trace_selector
1734 if self.tmin == working_system_time_range[0] and \
1735 self.tmax == working_system_time_range[1] or \
1736 self.show_all:
1738 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1739 if tmin is not None and tmax is not None:
1740 tlen = (tmax - tmin)
1741 tpad = tlen * 5./self.width()
1742 self.set_time_range(tmin-tpad, tmax+tpad)
1744 def set_time_range(self, tmin, tmax):
1745 if tmin is None:
1746 tmin = initial_time_range[0]
1748 if tmax is None:
1749 tmax = initial_time_range[1]
1751 if tmin > tmax:
1752 tmin, tmax = tmax, tmin
1754 if tmin == tmax:
1755 tmin -= 1.
1756 tmax += 1.
1758 tmin = max(working_system_time_range[0], tmin)
1759 tmax = min(working_system_time_range[1], tmax)
1761 min_deltat = self.content_deltat_range()[0]
1762 if (tmax - tmin < min_deltat):
1763 m = (tmin + tmax) / 2.
1764 tmin = m - min_deltat/2.
1765 tmax = m + min_deltat/2.
1767 self.time_projection.set_in_range(tmin, tmax)
1768 self.tmin, self.tmax = tmin, tmax
1770 def get_time_range(self):
1771 return self.tmin, self.tmax
1773 def ypart(self, y):
1774 if y < self.ax_height:
1775 return -1
1776 elif y > self.height()-self.ax_height:
1777 return 1
1778 else:
1779 return 0
1781 def time_fractional_digits(self):
1782 min_deltat = self.content_deltat_range()[0]
1783 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1785 def write_markers(self, fn=None):
1786 caption = "Choose a file name to write markers"
1787 if not fn:
1788 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1789 self, caption, options=qfiledialog_options))
1790 if fn:
1791 try:
1792 Marker.save_markers(
1793 self.markers, fn,
1794 fdigits=self.time_fractional_digits())
1796 except Exception as e:
1797 self.fail('Failed to write marker file: %s' % str(e))
1799 def write_selected_markers(self, fn=None):
1800 caption = "Choose a file name to write selected markers"
1801 if not fn:
1802 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1803 self, caption, options=qfiledialog_options))
1804 if fn:
1805 try:
1806 Marker.save_markers(
1807 self.iter_selected_markers(),
1808 fn,
1809 fdigits=self.time_fractional_digits())
1811 except Exception as e:
1812 self.fail('Failed to write marker file: %s' % str(e))
1814 def read_events(self, fn=None):
1815 '''
1816 Open QFileDialog to open, read and add
1817 :py:class:`pyrocko.model.Event` instances and their marker
1818 representation to the pile viewer.
1819 '''
1820 caption = "Selet one or more files to open"
1821 if not fn:
1822 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1823 self, caption, options=qfiledialog_options))
1824 if fn:
1825 try:
1826 self.add_events(pyrocko.model.load_events(fn))
1827 self.associate_phases_to_events()
1829 except Exception as e:
1830 self.fail('Failed to read event file: %s' % str(e))
1832 def read_markers(self, fn=None):
1833 '''
1834 Open QFileDialog to open, read and add markers to the pile viewer.
1835 '''
1836 caption = "Selet one or more marker files to open"
1837 if not fn:
1838 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1839 self, caption, options=qfiledialog_options))
1840 if fn:
1841 try:
1842 self.add_markers(Marker.load_markers(fn))
1843 self.associate_phases_to_events()
1845 except Exception as e:
1846 self.fail('Failed to read marker file: %s' % str(e))
1848 def associate_phases_to_events(self):
1849 associate_phases_to_events(self.markers)
1851 def add_marker(self, marker):
1852 # need index to inform QAbstactTableModel about upcoming change,
1853 # but have to restore current state in order to not cause problems
1854 self.markers.insert(marker)
1855 i = self.markers.remove(marker)
1857 self.begin_markers_add.emit(i, i)
1858 self.markers.insert(marker)
1859 self.end_markers_add.emit()
1860 self.markers_deltat_max = max(
1861 self.markers_deltat_max, marker.tmax - marker.tmin)
1863 def add_markers(self, markers):
1864 if not self.markers:
1865 self.begin_markers_add.emit(0, len(markers) - 1)
1866 self.markers.insert_many(markers)
1867 self.end_markers_add.emit()
1868 self.update_markers_deltat_max()
1869 else:
1870 for marker in markers:
1871 self.add_marker(marker)
1873 def update_markers_deltat_max(self):
1874 if self.markers:
1875 self.markers_deltat_max = max(
1876 marker.tmax - marker.tmin for marker in self.markers)
1878 def remove_marker(self, marker):
1879 '''
1880 Remove a ``marker`` from the :py:class:`PileViewer`.
1882 :param marker: :py:class:`Marker` (or subclass) instance
1883 '''
1885 if marker is self.active_event_marker:
1886 self.deactivate_event_marker()
1888 try:
1889 i = self.markers.index(marker)
1890 self.begin_markers_remove.emit(i, i)
1891 self.markers.remove_at(i)
1892 self.end_markers_remove.emit()
1893 except ValueError:
1894 pass
1896 def remove_markers(self, markers):
1897 '''
1898 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1900 :param markers: list of :py:class:`Marker` (or subclass)
1901 instances
1902 '''
1904 if markers is self.markers:
1905 markers = list(markers)
1907 for marker in markers:
1908 self.remove_marker(marker)
1910 self.update_markers_deltat_max()
1912 def remove_selected_markers(self):
1913 def delete_segment(istart, iend):
1914 self.begin_markers_remove.emit(istart, iend-1)
1915 for _ in range(iend - istart):
1916 self.markers.remove_at(istart)
1918 self.end_markers_remove.emit()
1920 istart = None
1921 ipos = 0
1922 markers = self.markers
1923 nmarkers = len(self.markers)
1924 while ipos < nmarkers:
1925 marker = markers[ipos]
1926 if marker.is_selected():
1927 if marker is self.active_event_marker:
1928 self.deactivate_event_marker()
1930 if istart is None:
1931 istart = ipos
1932 else:
1933 if istart is not None:
1934 delete_segment(istart, ipos)
1935 nmarkers -= ipos - istart
1936 ipos = istart - 1
1937 istart = None
1939 ipos += 1
1941 if istart is not None:
1942 delete_segment(istart, ipos)
1944 self.update_markers_deltat_max()
1946 def selected_markers(self):
1947 return [marker for marker in self.markers if marker.is_selected()]
1949 def iter_selected_markers(self):
1950 for marker in self.markers:
1951 if marker.is_selected():
1952 yield marker
1954 def get_markers(self):
1955 return self.markers
1957 def mousePressEvent(self, mouse_ev):
1958 self.show_all = False
1959 point = self.mapFromGlobal(mouse_ev.globalPos())
1961 if mouse_ev.button() == qc.Qt.LeftButton:
1962 marker = self.marker_under_cursor(point.x(), point.y())
1963 if self.picking:
1964 if self.picking_down is None:
1965 self.picking_down = (
1966 self.time_projection.rev(mouse_ev.x()),
1967 mouse_ev.y())
1969 elif marker is not None:
1970 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1971 self.deselect_all()
1972 marker.selected = True
1973 self.emit_selected_markers()
1974 self.update()
1975 else:
1976 self.track_start = mouse_ev.x(), mouse_ev.y()
1977 self.track_trange = self.tmin, self.tmax
1979 if mouse_ev.button() == qc.Qt.RightButton \
1980 and isinstance(self.menu, qw.QMenu):
1981 self.menu.exec_(qg.QCursor.pos())
1982 self.update_status()
1984 def mouseReleaseEvent(self, mouse_ev):
1985 if self.ignore_releases:
1986 self.ignore_releases -= 1
1987 return
1989 if self.picking:
1990 self.stop_picking(mouse_ev.x(), mouse_ev.y())
1991 self.emit_selected_markers()
1993 if self.track_start:
1994 self.update()
1996 self.track_start = None
1997 self.track_trange = None
1998 self.update_status()
2000 def mouseDoubleClickEvent(self, mouse_ev):
2001 self.show_all = False
2002 self.start_picking(None)
2003 self.ignore_releases = 1
2005 def mouseMoveEvent(self, mouse_ev):
2006 self.setUpdatesEnabled(False)
2007 point = self.mapFromGlobal(mouse_ev.globalPos())
2009 if self.picking:
2010 self.update_picking(point.x(), point.y())
2012 elif self.track_start is not None:
2013 x0, y0 = self.track_start
2014 dx = (point.x() - x0)/float(self.width())
2015 dy = (point.y() - y0)/float(self.height())
2016 if self.ypart(y0) == 1:
2017 dy = 0
2019 tmin0, tmax0 = self.track_trange
2021 scale = math.exp(-dy*5.)
2022 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2023 frac = x0/float(self.width())
2024 dt = dx*(tmax0-tmin0)*scale
2026 self.interrupt_following()
2027 self.set_time_range(
2028 tmin0 - dt - dtr*frac,
2029 tmax0 - dt + dtr*(1.-frac))
2031 self.update()
2032 else:
2033 self.hoovering(point.x(), point.y())
2035 self.update_status()
2037 def nslc_ids_under_cursor(self, x, y):
2038 ftrack = self.track_to_screen.rev(y)
2039 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2040 return nslc_ids
2042 def marker_under_cursor(self, x, y):
2043 mouset = self.time_projection.rev(x)
2044 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2045 relevant_nslc_ids = None
2046 for marker in self.markers:
2047 if marker.kind not in self.visible_marker_kinds:
2048 continue
2050 if (abs(mouset-marker.tmin) < deltat or
2051 abs(mouset-marker.tmax) < deltat):
2053 if relevant_nslc_ids is None:
2054 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2056 marker_nslc_ids = marker.get_nslc_ids()
2057 if not marker_nslc_ids:
2058 return marker
2060 for nslc_id in marker_nslc_ids:
2061 if nslc_id in relevant_nslc_ids:
2062 return marker
2064 def hoovering(self, x, y):
2065 mouset = self.time_projection.rev(x)
2066 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2067 needupdate = False
2068 haveone = False
2069 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2070 for marker in self.markers:
2071 if marker.kind not in self.visible_marker_kinds:
2072 continue
2074 state = abs(mouset-marker.tmin) < deltat or \
2075 abs(mouset-marker.tmax) < deltat and not haveone
2077 if state:
2078 xstate = False
2080 marker_nslc_ids = marker.get_nslc_ids()
2081 if not marker_nslc_ids:
2082 xstate = True
2084 for nslc in relevant_nslc_ids:
2085 if marker.match_nslc(nslc):
2086 xstate = True
2088 state = xstate
2090 if state:
2091 haveone = True
2092 oldstate = marker.is_alerted()
2093 if oldstate != state:
2094 needupdate = True
2095 marker.set_alerted(state)
2096 if state:
2097 self.message = marker.hoover_message()
2099 if not haveone:
2100 self.message = None
2102 if needupdate:
2103 self.update()
2105 def event(self, event):
2106 if event.type() == qc.QEvent.KeyPress:
2107 self.keyPressEvent(event)
2108 return True
2109 else:
2110 return base.event(self, event)
2112 def keyPressEvent(self, key_event):
2113 self.show_all = False
2114 dt = self.tmax - self.tmin
2115 tmid = (self.tmin + self.tmax) / 2.
2117 key = key_event.key()
2118 try:
2119 keytext = str(key_event.text())
2120 except UnicodeEncodeError:
2121 return
2123 if key == qc.Qt.Key_Space:
2124 self.interrupt_following()
2125 self.set_time_range(self.tmin+dt, self.tmax+dt)
2127 elif key == qc.Qt.Key_Up:
2128 for m in self.selected_markers():
2129 if isinstance(m, PhaseMarker):
2130 if key_event.modifiers() & qc.Qt.ShiftModifier:
2131 p = 0
2132 else:
2133 p = 1 if m.get_polarity() != 1 else None
2134 m.set_polarity(p)
2136 elif key == qc.Qt.Key_Down:
2137 for m in self.selected_markers():
2138 if isinstance(m, PhaseMarker):
2139 if key_event.modifiers() & qc.Qt.ShiftModifier:
2140 p = 0
2141 else:
2142 p = -1 if m.get_polarity() != -1 else None
2143 m.set_polarity(p)
2145 elif key == qc.Qt.Key_B:
2146 dt = self.tmax - self.tmin
2147 self.interrupt_following()
2148 self.set_time_range(self.tmin-dt, self.tmax-dt)
2150 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2151 self.interrupt_following()
2153 tgo = None
2155 class TraceDummy(object):
2156 def __init__(self, marker):
2157 self._marker = marker
2159 @property
2160 def nslc_id(self):
2161 return self._marker.one_nslc()
2163 def marker_to_itrack(marker):
2164 try:
2165 return self.key_to_row.get(
2166 self.gather(TraceDummy(marker)), -1)
2168 except MarkerOneNSLCRequired:
2169 return -1
2171 emarker, pmarkers = self.get_active_markers()
2172 pmarkers = [
2173 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2174 pmarkers.sort(key=lambda m: (
2175 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2177 if key == qc.Qt.Key_Backtab:
2178 pmarkers.reverse()
2180 smarkers = self.selected_markers()
2181 iselected = []
2182 for sm in smarkers:
2183 try:
2184 iselected.append(pmarkers.index(sm))
2185 except ValueError:
2186 pass
2188 if iselected:
2189 icurrent = max(iselected) + 1
2190 else:
2191 icurrent = 0
2193 if icurrent < len(pmarkers):
2194 self.deselect_all()
2195 cmarker = pmarkers[icurrent]
2196 cmarker.selected = True
2197 tgo = cmarker.tmin
2198 if not self.tmin < tgo < self.tmax:
2199 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2201 itrack = marker_to_itrack(cmarker)
2202 if itrack != -1:
2203 if itrack < self.shown_tracks_range[0]:
2204 self.scroll_tracks(
2205 - (self.shown_tracks_range[0] - itrack))
2206 elif self.shown_tracks_range[1] <= itrack:
2207 self.scroll_tracks(
2208 itrack - self.shown_tracks_range[1]+1)
2210 if itrack not in self.track_to_nslc_ids:
2211 self.go_to_selection()
2213 elif keytext in ('p', 'n', 'P', 'N'):
2214 smarkers = self.selected_markers()
2215 tgo = None
2216 dir = str(keytext)
2217 if smarkers:
2218 tmid = smarkers[0].tmin
2219 for smarker in smarkers:
2220 if dir == 'n':
2221 tmid = max(smarker.tmin, tmid)
2222 else:
2223 tmid = min(smarker.tmin, tmid)
2225 tgo = tmid
2227 if dir.lower() == 'n':
2228 for marker in sorted(
2229 self.markers,
2230 key=operator.attrgetter('tmin')):
2232 t = marker.tmin
2233 if t > tmid and \
2234 marker.kind in self.visible_marker_kinds and \
2235 (dir == 'n' or
2236 isinstance(marker, EventMarker)):
2238 self.deselect_all()
2239 marker.selected = True
2240 tgo = t
2241 break
2242 else:
2243 for marker in sorted(
2244 self.markers,
2245 key=operator.attrgetter('tmin'),
2246 reverse=True):
2248 t = marker.tmin
2249 if t < tmid and \
2250 marker.kind in self.visible_marker_kinds and \
2251 (dir == 'p' or
2252 isinstance(marker, EventMarker)):
2253 self.deselect_all()
2254 marker.selected = True
2255 tgo = t
2256 break
2258 if tgo is not None:
2259 self.interrupt_following()
2260 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2262 elif keytext == 'r':
2263 if self.pile.reload_modified():
2264 self.reloaded = True
2266 elif keytext == 'R':
2267 self.setup_snufflings()
2269 elif key == qc.Qt.Key_Backspace:
2270 self.remove_selected_markers()
2272 elif keytext == 'a':
2273 for marker in self.markers:
2274 if ((self.tmin <= marker.tmin <= self.tmax or
2275 self.tmin <= marker.tmax <= self.tmax) and
2276 marker.kind in self.visible_marker_kinds):
2277 marker.selected = True
2278 else:
2279 marker.selected = False
2281 elif keytext == 'A':
2282 for marker in self.markers:
2283 if marker.kind in self.visible_marker_kinds:
2284 marker.selected = True
2286 elif keytext == 'd':
2287 self.deselect_all()
2289 elif keytext == 'E':
2290 self.deactivate_event_marker()
2292 elif keytext == 'e':
2293 markers = self.selected_markers()
2294 event_markers_in_spe = [
2295 marker for marker in markers
2296 if not isinstance(marker, PhaseMarker)]
2298 phase_markers = [
2299 marker for marker in markers
2300 if isinstance(marker, PhaseMarker)]
2302 if len(event_markers_in_spe) == 1:
2303 event_marker = event_markers_in_spe[0]
2304 if not isinstance(event_marker, EventMarker):
2305 nslcs = list(event_marker.nslc_ids)
2306 lat, lon = 0.0, 0.0
2307 old = self.get_active_event()
2308 if len(nslcs) == 1:
2309 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2310 elif old is not None:
2311 lat, lon = old.lat, old.lon
2313 event_marker.convert_to_event_marker(lat, lon)
2315 self.set_active_event_marker(event_marker)
2316 event = event_marker.get_event()
2317 for marker in phase_markers:
2318 marker.set_event(event)
2320 else:
2321 for marker in event_markers_in_spe:
2322 marker.convert_to_event_marker()
2324 elif keytext in ('0', '1', '2', '3', '4', '5'):
2325 for marker in self.selected_markers():
2326 marker.set_kind(int(keytext))
2327 self.emit_selected_markers()
2329 elif key in fkey_map:
2330 self.handle_fkeys(key)
2332 elif key == qc.Qt.Key_Escape:
2333 if self.picking:
2334 self.stop_picking(0, 0, abort=True)
2336 elif key == qc.Qt.Key_PageDown:
2337 self.scroll_tracks(
2338 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2340 elif key == qc.Qt.Key_PageUp:
2341 self.scroll_tracks(
2342 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2344 elif key == qc.Qt.Key_Plus:
2345 self.zoom_tracks(0., 1.)
2347 elif key == qc.Qt.Key_Minus:
2348 self.zoom_tracks(0., -1.)
2350 elif key == qc.Qt.Key_Equal:
2351 ntracks_shown = self.shown_tracks_range[1] - \
2352 self.shown_tracks_range[0]
2353 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2354 self.zoom_tracks(0., dtracks)
2356 elif key == qc.Qt.Key_Colon:
2357 self.want_input.emit()
2359 elif keytext == 'g':
2360 self.go_to_selection()
2362 elif keytext == 'G':
2363 self.go_to_selection(tight=True)
2365 elif keytext == 'm':
2366 self.toggle_marker_editor()
2368 elif keytext == 'c':
2369 self.toggle_main_controls()
2371 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2372 dir = 1
2373 amount = 1
2374 if key_event.key() == qc.Qt.Key_Left:
2375 dir = -1
2376 if key_event.modifiers() & qc.Qt.ShiftModifier:
2377 amount = 10
2378 self.nudge_selected_markers(dir*amount)
2379 else:
2380 super().keyPressEvent(key_event)
2382 if keytext != '' and keytext in 'degaApPnN':
2383 self.emit_selected_markers()
2385 self.update()
2386 self.update_status()
2388 def handle_fkeys(self, key):
2389 self.set_phase_kind(
2390 self.selected_markers(),
2391 fkey_map[key] + 1)
2392 self.emit_selected_markers()
2394 def emit_selected_markers(self):
2395 ibounds = []
2396 last_selected = False
2397 for imarker, marker in enumerate(self.markers):
2398 this_selected = marker.is_selected()
2399 if this_selected != last_selected:
2400 ibounds.append(imarker)
2402 last_selected = this_selected
2404 if last_selected:
2405 ibounds.append(len(self.markers))
2407 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2408 self.n_selected_markers = sum(
2409 chunk[1] - chunk[0] for chunk in chunks)
2410 self.marker_selection_changed.emit(chunks)
2412 def toggle_marker_editor(self):
2413 self.panel_parent.toggle_marker_editor()
2415 def toggle_main_controls(self):
2416 self.panel_parent.toggle_main_controls()
2418 def nudge_selected_markers(self, npixels):
2419 a, b = self.time_projection.ur
2420 c, d = self.time_projection.xr
2421 for marker in self.selected_markers():
2422 if not isinstance(marker, EventMarker):
2423 marker.tmin += npixels * (d-c)/b
2424 marker.tmax += npixels * (d-c)/b
2426 def toggle_fullscreen(self):
2427 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2428 self.window().windowState() & qc.Qt.WindowMaximized:
2429 self.window().showNormal()
2430 else:
2431 if macosx:
2432 self.window().showMaximized()
2433 else:
2434 self.window().showFullScreen()
2436 def about(self):
2437 fn = pyrocko.util.data_file('snuffler.png')
2438 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2439 txt = f.read()
2440 label = qw.QLabel(txt % {'logo': fn})
2441 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2442 self.show_doc('About', [label], target='tab')
2444 def help(self):
2445 class MyScrollArea(qw.QScrollArea):
2447 def sizeHint(self):
2448 s = qc.QSize()
2449 s.setWidth(self.widget().sizeHint().width())
2450 s.setHeight(self.widget().sizeHint().height())
2451 return s
2453 with open(pyrocko.util.data_file(
2454 'snuffler_help.html')) as f:
2455 hcheat = qw.QLabel(f.read())
2457 with open(pyrocko.util.data_file(
2458 'snuffler_help_epilog.html')) as f:
2459 hepilog = qw.QLabel(f.read())
2461 for h in [hcheat, hepilog]:
2462 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2463 h.setWordWrap(True)
2465 self.show_doc('Help', [hcheat, hepilog], target='panel')
2467 def show_doc(self, name, labels, target='panel'):
2468 scroller = qw.QScrollArea()
2469 frame = qw.QFrame(scroller)
2470 frame.setLineWidth(0)
2471 layout = qw.QVBoxLayout()
2472 layout.setContentsMargins(0, 0, 0, 0)
2473 layout.setSpacing(0)
2474 frame.setLayout(layout)
2475 scroller.setWidget(frame)
2476 scroller.setWidgetResizable(True)
2477 frame.setBackgroundRole(qg.QPalette.Base)
2478 for h in labels:
2479 h.setParent(frame)
2480 h.setMargin(3)
2481 h.setTextInteractionFlags(
2482 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2483 h.setBackgroundRole(qg.QPalette.Base)
2484 layout.addWidget(h)
2485 h.linkActivated.connect(
2486 self.open_link)
2488 if self.panel_parent is not None:
2489 if target == 'panel':
2490 self.panel_parent.add_panel(
2491 name, scroller, True, volatile=False)
2492 else:
2493 self.panel_parent.add_tab(name, scroller)
2495 def open_link(self, link):
2496 qg.QDesktopServices.openUrl(qc.QUrl(link))
2498 def wheelEvent(self, wheel_event):
2499 if use_pyqt5:
2500 self.wheel_pos += wheel_event.angleDelta().y()
2501 else:
2502 self.wheel_pos += wheel_event.delta()
2504 n = self.wheel_pos // 120
2505 self.wheel_pos = self.wheel_pos % 120
2506 if n == 0:
2507 return
2509 amount = max(
2510 1.,
2511 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2512 wdelta = amount * n
2514 trmin, trmax = self.track_to_screen.get_in_range()
2515 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2516 / (trmax-trmin)
2518 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2519 self.zoom_tracks(anchor, wdelta)
2520 else:
2521 self.scroll_tracks(-wdelta)
2523 def dragEnterEvent(self, event):
2524 if event.mimeData().hasUrls():
2525 if any(url.toLocalFile() for url in event.mimeData().urls()):
2526 event.setDropAction(qc.Qt.LinkAction)
2527 event.accept()
2529 def dropEvent(self, event):
2530 if event.mimeData().hasUrls():
2531 paths = list(
2532 str(url.toLocalFile()) for url in event.mimeData().urls())
2533 event.acceptProposedAction()
2534 self.load(paths)
2536 def get_phase_name(self, kind):
2537 return self.config.get_phase_name(kind)
2539 def set_phase_kind(self, markers, kind):
2540 phasename = self.get_phase_name(kind)
2542 for marker in markers:
2543 if isinstance(marker, PhaseMarker):
2544 if kind == 10:
2545 marker.convert_to_marker()
2546 else:
2547 marker.set_phasename(phasename)
2548 marker.set_event(self.get_active_event())
2550 elif isinstance(marker, EventMarker):
2551 pass
2553 else:
2554 if kind != 10:
2555 event = self.get_active_event()
2556 marker.convert_to_phase_marker(
2557 event, phasename, None, False)
2559 def set_ntracks(self, ntracks):
2560 if self.ntracks != ntracks:
2561 self.ntracks = ntracks
2562 if self.shown_tracks_range is not None:
2563 l, h = self.shown_tracks_range
2564 else:
2565 l, h = 0, self.ntracks
2567 self.tracks_range_changed.emit(self.ntracks, l, h)
2569 def set_tracks_range(self, range, start=None):
2571 low, high = range
2572 low = min(self.ntracks-1, low)
2573 high = min(self.ntracks, high)
2574 low = max(0, low)
2575 high = max(1, high)
2577 if start is None:
2578 start = float(low)
2580 if self.shown_tracks_range != (low, high):
2581 self.shown_tracks_range = low, high
2582 self.shown_tracks_start = start
2584 self.tracks_range_changed.emit(self.ntracks, low, high)
2586 def scroll_tracks(self, shift):
2587 shown = self.shown_tracks_range
2588 shiftmin = -shown[0]
2589 shiftmax = self.ntracks-shown[1]
2590 shift = max(shiftmin, shift)
2591 shift = min(shiftmax, shift)
2592 shown = shown[0] + shift, shown[1] + shift
2594 self.set_tracks_range((int(shown[0]), int(shown[1])))
2596 self.update()
2598 def zoom_tracks(self, anchor, delta):
2599 ntracks_shown = self.shown_tracks_range[1] \
2600 - self.shown_tracks_range[0]
2602 if (ntracks_shown == 1 and delta <= 0) or \
2603 (ntracks_shown == self.ntracks and delta >= 0):
2604 return
2606 ntracks_shown += int(round(delta))
2607 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2609 u = self.shown_tracks_start
2610 nu = max(0., u-anchor*delta)
2611 nv = nu + ntracks_shown
2612 if nv > self.ntracks:
2613 nu -= nv - self.ntracks
2614 nv -= nv - self.ntracks
2616 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2618 self.ntracks_shown_max = self.shown_tracks_range[1] \
2619 - self.shown_tracks_range[0]
2621 self.update()
2623 def content_time_range(self):
2624 pile = self.get_pile()
2625 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2626 if tmin is None:
2627 tmin = initial_time_range[0]
2628 if tmax is None:
2629 tmax = initial_time_range[1]
2631 return tmin, tmax
2633 def content_deltat_range(self):
2634 pile = self.get_pile()
2636 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2638 if deltatmin is None:
2639 deltatmin = 0.001
2641 if deltatmax is None:
2642 deltatmax = 1000.0
2644 return deltatmin, deltatmax
2646 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2647 if tmax < tmin:
2648 tmin, tmax = tmax, tmin
2650 deltatmin = self.content_deltat_range()[0]
2651 dt = deltatmin * self.visible_length * 0.95
2653 if dt == 0.0:
2654 dt = 1.0
2656 if tight:
2657 if tmax != tmin:
2658 dtm = tmax - tmin
2659 tmin -= dtm*0.1
2660 tmax += dtm*0.1
2661 return tmin, tmax
2662 else:
2663 tcenter = (tmin + tmax) / 2.
2664 tmin = tcenter - 0.5*dt
2665 tmax = tcenter + 0.5*dt
2666 return tmin, tmax
2668 if tmax-tmin < dt:
2669 vmin, vmax = self.get_time_range()
2670 dt = min(vmax - vmin, dt)
2672 tcenter = (tmin+tmax)/2.
2673 etmin, etmax = tmin, tmax
2674 tmin = min(etmin, tcenter - 0.5*dt)
2675 tmax = max(etmax, tcenter + 0.5*dt)
2676 dtm = tmax-tmin
2677 if etmin == tmin:
2678 tmin -= dtm*0.1
2679 if etmax == tmax:
2680 tmax += dtm*0.1
2682 else:
2683 dtm = tmax-tmin
2684 tmin -= dtm*0.1
2685 tmax += dtm*0.1
2687 return tmin, tmax
2689 def go_to_selection(self, tight=False):
2690 markers = self.selected_markers()
2691 if markers:
2692 tmax, tmin = self.content_time_range()
2693 for marker in markers:
2694 tmin = min(tmin, marker.tmin)
2695 tmax = max(tmax, marker.tmax)
2697 else:
2698 if tight:
2699 vmin, vmax = self.get_time_range()
2700 tmin = tmax = (vmin + vmax) / 2.
2701 else:
2702 tmin, tmax = self.content_time_range()
2704 tmin, tmax = self.make_good_looking_time_range(
2705 tmin, tmax, tight=tight)
2707 self.interrupt_following()
2708 self.set_time_range(tmin, tmax)
2709 self.update()
2711 def go_to_time(self, t, tlen=None):
2712 tmax = t
2713 if tlen is not None:
2714 tmax = t+tlen
2715 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2716 self.interrupt_following()
2717 self.set_time_range(tmin, tmax)
2718 self.update()
2720 def go_to_event_by_name(self, name):
2721 for marker in self.markers:
2722 if isinstance(marker, EventMarker):
2723 event = marker.get_event()
2724 if event.name and event.name.lower() == name.lower():
2725 tmin, tmax = self.make_good_looking_time_range(
2726 event.time, event.time)
2728 self.interrupt_following()
2729 self.set_time_range(tmin, tmax)
2731 def printit(self):
2732 from .qt_compat import qprint
2733 printer = qprint.QPrinter()
2734 printer.setOrientation(qprint.QPrinter.Landscape)
2736 dialog = qprint.QPrintDialog(printer, self)
2737 dialog.setWindowTitle('Print')
2739 if dialog.exec_() != qw.QDialog.Accepted:
2740 return
2742 painter = qg.QPainter()
2743 painter.begin(printer)
2744 page = printer.pageRect()
2745 self.drawit(
2746 painter, printmode=False, w=page.width(), h=page.height())
2748 painter.end()
2750 def savesvg(self, fn=None):
2752 if not fn:
2753 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
2754 self,
2755 'Save as SVG|PNG',
2756 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2757 'SVG|PNG (*.svg *.png)',
2758 options=qfiledialog_options))
2760 if fn == '':
2761 return
2763 fn = str(fn)
2765 if fn.lower().endswith('.svg'):
2766 try:
2767 w, h = 842, 595
2768 margin = 0.025
2769 m = max(w, h)*margin
2771 generator = qsvg.QSvgGenerator()
2772 generator.setFileName(fn)
2773 generator.setSize(qc.QSize(w, h))
2774 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2776 painter = qg.QPainter()
2777 painter.begin(generator)
2778 self.drawit(painter, printmode=False, w=w, h=h)
2779 painter.end()
2781 except Exception as e:
2782 self.fail('Failed to write SVG file: %s' % str(e))
2784 elif fn.lower().endswith('.png'):
2785 if use_pyqt5:
2786 pixmap = self.grab()
2787 else:
2788 pixmap = qg.QPixmap().grabWidget(self)
2790 try:
2791 pixmap.save(fn)
2793 except Exception as e:
2794 self.fail('Failed to write PNG file: %s' % str(e))
2796 else:
2797 self.fail(
2798 'Unsupported file type: filename must end with ".svg" or '
2799 '".png".')
2801 def paintEvent(self, paint_ev):
2802 '''
2803 Called by QT whenever widget needs to be painted.
2804 '''
2805 painter = qg.QPainter(self)
2807 if self.menuitem_antialias.isChecked():
2808 painter.setRenderHint(qg.QPainter.Antialiasing)
2810 self.drawit(painter)
2812 logger.debug(
2813 'Time spent drawing: '
2814 ' user:%.3f sys:%.3f children_user:%.3f'
2815 ' childred_sys:%.3f elapsed:%.3f' %
2816 (self.timer_draw - self.timer_cutout))
2818 logger.debug(
2819 'Time spent processing:'
2820 ' user:%.3f sys:%.3f children_user:%.3f'
2821 ' childred_sys:%.3f elapsed:%.3f' %
2822 self.timer_cutout.get())
2824 self.time_spent_painting = self.timer_draw.get()[-1]
2825 self.time_last_painted = time.time()
2827 def determine_box_styles(self):
2829 traces = list(self.pile.iter_traces())
2830 traces.sort(key=operator.attrgetter('full_id'))
2831 istyle = 0
2832 trace_styles = {}
2833 for itr, tr in enumerate(traces):
2834 if itr > 0:
2835 other = traces[itr-1]
2836 if not (
2837 other.nslc_id == tr.nslc_id
2838 and other.deltat == tr.deltat
2839 and abs(other.tmax - tr.tmin)
2840 < gap_lap_tolerance*tr.deltat):
2842 istyle += 1
2844 trace_styles[tr.full_id, tr.deltat] = istyle
2846 self.trace_styles = trace_styles
2848 def draw_trace_boxes(self, p, time_projection, track_projections):
2850 for v_projection in track_projections.values():
2851 v_projection.set_in_range(0., 1.)
2853 def selector(x):
2854 return x.overlaps(*time_projection.get_in_range())
2856 if self.trace_filter is not None:
2857 def tselector(x):
2858 return selector(x) and self.trace_filter(x)
2860 else:
2861 tselector = selector
2863 traces = list(self.pile.iter_traces(
2864 group_selector=selector, trace_selector=tselector))
2866 traces.sort(key=operator.attrgetter('full_id'))
2868 def drawbox(itrack, istyle, traces):
2869 v_projection = track_projections[itrack]
2870 dvmin = v_projection(0.)
2871 dvmax = v_projection(1.)
2872 dtmin = time_projection.clipped(traces[0].tmin)
2873 dtmax = time_projection.clipped(traces[-1].tmax)
2875 style = box_styles[istyle % len(box_styles)]
2876 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2877 p.fillRect(rect, style.fill_brush)
2878 p.setPen(style.frame_pen)
2879 p.drawRect(rect)
2881 traces_by_style = {}
2882 for itr, tr in enumerate(traces):
2883 gt = self.gather(tr)
2884 if gt not in self.key_to_row:
2885 continue
2887 itrack = self.key_to_row[gt]
2888 if itrack not in track_projections:
2889 continue
2891 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2893 if len(traces) < 500:
2894 drawbox(itrack, istyle, [tr])
2895 else:
2896 if (itrack, istyle) not in traces_by_style:
2897 traces_by_style[itrack, istyle] = []
2898 traces_by_style[itrack, istyle].append(tr)
2900 for (itrack, istyle), traces in traces_by_style.items():
2901 drawbox(itrack, istyle, traces)
2903 def draw_visible_markers(
2904 self, p, vcenter_projection, primary_pen):
2906 try:
2907 markers = self.markers.with_key_in_limited(
2908 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2910 except pyrocko.pile.TooMany:
2911 tmin = self.markers[0].tmin
2912 tmax = self.markers[-1].tmax
2913 umin_view, umax_view = self.time_projection.get_out_range()
2914 umin = max(umin_view, self.time_projection(tmin))
2915 umax = min(umax_view, self.time_projection(tmax))
2916 v0, _ = vcenter_projection.get_out_range()
2917 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2919 p.save()
2921 pen = qg.QPen(primary_pen)
2922 pen.setWidth(2)
2923 pen.setStyle(qc.Qt.DotLine)
2924 # pat = [5., 3.]
2925 # pen.setDashPattern(pat)
2926 p.setPen(pen)
2928 if self.n_selected_markers == len(self.markers):
2929 s_selected = ' (all selected)'
2930 elif self.n_selected_markers > 0:
2931 s_selected = ' (%i selected)' % self.n_selected_markers
2932 else:
2933 s_selected = ''
2935 draw_label(
2936 p, umin+10., v0-10.,
2937 '%i Markers' % len(self.markers) + s_selected,
2938 label_bg, 'LB')
2940 line = qc.QLineF(umin, v0, umax, v0)
2941 p.drawLine(line)
2942 p.restore()
2944 return
2946 for marker in markers:
2947 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2948 and marker.kind in self.visible_marker_kinds:
2950 marker.draw(
2951 p, self.time_projection, vcenter_projection,
2952 with_label=True)
2954 def get_squirrel(self):
2955 try:
2956 return self.pile._squirrel
2957 except AttributeError:
2958 return None
2960 def draw_coverage(self, p, time_projection, track_projections):
2961 sq = self.get_squirrel()
2962 if sq is None:
2963 return
2965 def drawbox(itrack, tmin, tmax, style):
2966 v_projection = track_projections[itrack]
2967 dvmin = v_projection(0.)
2968 dvmax = v_projection(1.)
2969 dtmin = time_projection.clipped(tmin)
2970 dtmax = time_projection.clipped(tmax)
2972 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2973 p.fillRect(rect, style.fill_brush)
2974 p.setPen(style.frame_pen)
2975 p.drawRect(rect)
2977 pattern_list = []
2978 pattern_to_itrack = {}
2979 for key in self.track_keys:
2980 itrack = self.key_to_row[key]
2981 if itrack not in track_projections:
2982 continue
2984 pattern = self.track_patterns[itrack]
2985 pattern_to_itrack[tuple(pattern)] = itrack
2986 pattern_list.append(pattern)
2988 vmin, vmax = self.get_time_range()
2989 for entry in sq.get_coverage(
2990 'waveform', vmin, vmax, pattern_list, limit=500):
2991 pattern, codes, deltat, tmin, tmax, cover_data = entry
2992 itrack = pattern_to_itrack[tuple(pattern)]
2994 if cover_data is None:
2995 drawbox(itrack, tmin, tmax, box_styles_coverage[0])
2996 else:
2997 t = None
2998 pcount = 0
2999 for tb, count in cover_data:
3000 if t is not None and tb > t:
3001 if pcount > 0:
3002 drawbox(
3003 itrack, t, tb,
3004 box_styles_coverage[
3005 min(len(box_styles_coverage)-1,
3006 pcount)])
3008 t = tb
3009 pcount = count
3011 def drawit(self, p, printmode=False, w=None, h=None):
3012 '''
3013 This performs the actual drawing.
3014 '''
3016 self.timer_draw.start()
3017 show_boxes = self.menuitem_showboxes.isChecked()
3018 sq = self.get_squirrel()
3020 if self.gather is None:
3021 self.set_gathering()
3023 if self.pile_has_changed:
3025 if not self.sortingmode_change_delayed():
3026 self.sortingmode_change()
3028 if show_boxes and sq is None:
3029 self.determine_box_styles()
3031 self.pile_has_changed = False
3033 if h is None:
3034 h = float(self.height())
3035 if w is None:
3036 w = float(self.width())
3038 if printmode:
3039 primary_color = (0, 0, 0)
3040 else:
3041 primary_color = pyrocko.plot.tango_colors['aluminium5']
3043 primary_pen = qg.QPen(qg.QColor(*primary_color))
3045 ax_h = float(self.ax_height)
3047 vbottom_ax_projection = Projection()
3048 vtop_ax_projection = Projection()
3049 vcenter_projection = Projection()
3051 self.time_projection.set_out_range(0., w)
3052 vbottom_ax_projection.set_out_range(h-ax_h, h)
3053 vtop_ax_projection.set_out_range(0., ax_h)
3054 vcenter_projection.set_out_range(ax_h, h-ax_h)
3055 vcenter_projection.set_in_range(0., 1.)
3056 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3058 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3059 track_projections = {}
3060 for i in range(*self.shown_tracks_range):
3061 proj = Projection()
3062 proj.set_out_range(
3063 self.track_to_screen(i+0.05),
3064 self.track_to_screen(i+1.-0.05))
3066 track_projections[i] = proj
3068 if self.tmin > self.tmax:
3069 return
3071 self.time_projection.set_in_range(self.tmin, self.tmax)
3072 vbottom_ax_projection.set_in_range(0, ax_h)
3074 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3076 yscaler = pyrocko.plot.AutoScaler()
3078 p.setPen(primary_pen)
3080 font = qg.QFont()
3081 font.setBold(True)
3083 axannotfont = qg.QFont()
3084 axannotfont.setBold(True)
3085 axannotfont.setPointSize(8)
3087 processed_traces = self.prepare_cutout2(
3088 self.tmin, self.tmax,
3089 trace_selector=self.trace_selector,
3090 degap=self.menuitem_degap.isChecked(),
3091 demean=self.menuitem_demean.isChecked())
3093 if not printmode and show_boxes:
3094 if (self.view_mode is ViewMode.Wiggle) \
3095 or (self.view_mode is ViewMode.Waterfall
3096 and not processed_traces):
3098 if sq is None:
3099 self.draw_trace_boxes(
3100 p, self.time_projection, track_projections)
3102 else:
3103 self.draw_coverage(
3104 p, self.time_projection, track_projections)
3106 p.setFont(font)
3107 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3109 color_lookup = dict(
3110 [(k, i) for (i, k) in enumerate(self.color_keys)])
3112 self.track_to_nslc_ids = {}
3113 nticks = 0
3114 annot_labels = []
3116 if self.view_mode is ViewMode.Waterfall and processed_traces:
3117 waterfall = self.waterfall
3118 waterfall.set_time_range(self.tmin, self.tmax)
3119 waterfall.set_traces(processed_traces)
3120 waterfall.set_cmap(self.waterfall_cmap)
3121 waterfall.set_integrate(self.waterfall_integrate)
3122 waterfall.set_clip(
3123 self.waterfall_clip_min, self.waterfall_clip_max)
3124 waterfall.show_absolute_values(
3125 self.waterfall_show_absolute)
3127 rect = qc.QRectF(
3128 0, self.ax_height,
3129 self.width(), self.height() - self.ax_height*2
3130 )
3131 waterfall.draw_waterfall(p, rect=rect)
3133 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3134 show_scales = self.menuitem_showscalerange.isChecked() \
3135 or self.menuitem_showscaleaxis.isChecked()
3137 fm = qg.QFontMetrics(axannotfont, p.device())
3138 trackheight = self.track_to_screen(1.-0.05) \
3139 - self.track_to_screen(0.05)
3141 nlinesavail = trackheight/float(fm.lineSpacing())
3143 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3144 if self.menuitem_showscaleaxis.isChecked() \
3145 else 15
3147 yscaler = pyrocko.plot.AutoScaler(
3148 no_exp_interval=(-3, 2), approx_ticks=nticks,
3149 snap=show_scales
3150 and not self.menuitem_showscaleaxis.isChecked())
3152 data_ranges = pyrocko.trace.minmax(
3153 processed_traces,
3154 key=self.scaling_key,
3155 mode=self.scaling_base)
3157 if not self.menuitem_fixscalerange.isChecked():
3158 self.old_data_ranges = data_ranges
3159 else:
3160 data_ranges.update(self.old_data_ranges)
3162 self.apply_scaling_hooks(data_ranges)
3164 trace_to_itrack = {}
3165 track_scaling_keys = {}
3166 track_scaling_colors = {}
3167 for trace in processed_traces:
3168 gt = self.gather(trace)
3169 if gt not in self.key_to_row:
3170 continue
3172 itrack = self.key_to_row[gt]
3173 if itrack not in track_projections:
3174 continue
3176 trace_to_itrack[trace] = itrack
3178 if itrack not in self.track_to_nslc_ids:
3179 self.track_to_nslc_ids[itrack] = set()
3181 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3183 if itrack not in track_scaling_keys:
3184 track_scaling_keys[itrack] = set()
3186 scaling_key = self.scaling_key(trace)
3187 track_scaling_keys[itrack].add(scaling_key)
3189 color = pyrocko.plot.color(
3190 color_lookup[self.color_gather(trace)])
3192 k = itrack, scaling_key
3193 if k not in track_scaling_colors \
3194 and self.menuitem_colortraces.isChecked():
3195 track_scaling_colors[k] = color
3196 else:
3197 track_scaling_colors[k] = primary_color
3199 # y axes, zero lines
3200 trace_projections = {}
3201 for itrack in list(track_projections.keys()):
3202 if itrack not in track_scaling_keys:
3203 continue
3204 uoff = 0
3205 for scaling_key in track_scaling_keys[itrack]:
3206 data_range = data_ranges[scaling_key]
3207 dymin, dymax = data_range
3208 ymin, ymax, yinc = yscaler.make_scale(
3209 (dymin/self.gain, dymax/self.gain))
3210 iexp = yscaler.make_exp(yinc)
3211 factor = 10**iexp
3212 trace_projection = track_projections[itrack].copy()
3213 trace_projection.set_in_range(ymax, ymin)
3214 trace_projections[itrack, scaling_key] = \
3215 trace_projection
3216 umin, umax = self.time_projection.get_out_range()
3217 vmin, vmax = trace_projection.get_out_range()
3218 umax_zeroline = umax
3219 uoffnext = uoff
3221 if show_scales:
3222 pen = qg.QPen(primary_pen)
3223 k = itrack, scaling_key
3224 if k in track_scaling_colors:
3225 c = qg.QColor(*track_scaling_colors[
3226 itrack, scaling_key])
3228 pen.setColor(c)
3230 p.setPen(pen)
3231 if nlinesavail > 3:
3232 if self.menuitem_showscaleaxis.isChecked():
3233 ymin_annot = math.ceil(ymin/yinc)*yinc
3234 ny_annot = int(
3235 math.floor(ymax/yinc)
3236 - math.ceil(ymin/yinc)) + 1
3238 for iy_annot in range(ny_annot):
3239 y = ymin_annot + iy_annot*yinc
3240 v = trace_projection(y)
3241 line = qc.QLineF(
3242 umax-10-uoff, v, umax-uoff, v)
3244 p.drawLine(line)
3245 if iy_annot == ny_annot - 1 \
3246 and iexp != 0:
3247 sexp = ' × ' \
3248 '10<sup>%i</sup>' % iexp
3249 else:
3250 sexp = ''
3252 snum = num_to_html(y/factor)
3253 lab = Label(
3254 p,
3255 umax-20-uoff,
3256 v, '%s%s' % (snum, sexp),
3257 label_bg=None,
3258 anchor='MR',
3259 font=axannotfont,
3260 color=c)
3262 uoffnext = max(
3263 lab.rect.width()+30., uoffnext)
3265 annot_labels.append(lab)
3266 if y == 0.:
3267 umax_zeroline = \
3268 umax - 20 \
3269 - lab.rect.width() - 10 \
3270 - uoff
3271 else:
3272 if not show_boxes:
3273 qpoints = make_QPolygonF(
3274 [umax-20-uoff,
3275 umax-10-uoff,
3276 umax-10-uoff,
3277 umax-20-uoff],
3278 [vmax, vmax, vmin, vmin])
3279 p.drawPolyline(qpoints)
3281 snum = num_to_html(ymin)
3282 labmin = Label(
3283 p, umax-15-uoff, vmax, snum,
3284 label_bg=None,
3285 anchor='BR',
3286 font=axannotfont,
3287 color=c)
3289 annot_labels.append(labmin)
3290 snum = num_to_html(ymax)
3291 labmax = Label(
3292 p, umax-15-uoff, vmin, snum,
3293 label_bg=None,
3294 anchor='TR',
3295 font=axannotfont,
3296 color=c)
3298 annot_labels.append(labmax)
3300 for lab in (labmin, labmax):
3301 uoffnext = max(
3302 lab.rect.width()+10., uoffnext)
3304 if self.menuitem_showzeroline.isChecked():
3305 v = trace_projection(0.)
3306 if vmin <= v <= vmax:
3307 line = qc.QLineF(umin, v, umax_zeroline, v)
3308 p.drawLine(line)
3310 uoff = uoffnext
3312 p.setFont(font)
3313 p.setPen(primary_pen)
3314 for trace in processed_traces:
3315 if self.view_mode is not ViewMode.Wiggle:
3316 break
3318 if trace not in trace_to_itrack:
3319 continue
3321 itrack = trace_to_itrack[trace]
3322 scaling_key = self.scaling_key(trace)
3323 trace_projection = trace_projections[
3324 itrack, scaling_key]
3326 vdata = trace_projection(trace.get_ydata())
3328 udata_min = float(self.time_projection(trace.tmin))
3329 udata_max = float(self.time_projection(
3330 trace.tmin+trace.deltat*(vdata.size-1)))
3331 udata = num.linspace(udata_min, udata_max, vdata.size)
3333 qpoints = make_QPolygonF(udata, vdata)
3335 umin, umax = self.time_projection.get_out_range()
3336 vmin, vmax = trace_projection.get_out_range()
3338 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3340 if self.menuitem_cliptraces.isChecked():
3341 p.setClipRect(trackrect)
3343 if self.menuitem_colortraces.isChecked():
3344 color = pyrocko.plot.color(
3345 color_lookup[self.color_gather(trace)])
3346 pen = qg.QPen(qg.QColor(*color), 1)
3347 p.setPen(pen)
3349 p.drawPolyline(qpoints)
3351 if self.floating_marker:
3352 self.floating_marker.draw_trace(
3353 self, p, trace,
3354 self.time_projection, trace_projection, 1.0)
3356 for marker in self.markers.with_key_in(
3357 self.tmin - self.markers_deltat_max,
3358 self.tmax):
3360 if marker.tmin < self.tmax \
3361 and self.tmin < marker.tmax \
3362 and marker.kind \
3363 in self.visible_marker_kinds:
3364 marker.draw_trace(
3365 self, p, trace, self.time_projection,
3366 trace_projection, 1.0)
3368 p.setPen(primary_pen)
3370 if self.menuitem_cliptraces.isChecked():
3371 p.setClipRect(0, 0, w, h)
3373 if self.floating_marker:
3374 self.floating_marker.draw(
3375 p, self.time_projection, vcenter_projection)
3377 self.draw_visible_markers(
3378 p, vcenter_projection, primary_pen)
3380 p.setPen(primary_pen)
3381 while font.pointSize() > 2:
3382 fm = qg.QFontMetrics(font, p.device())
3383 trackheight = self.track_to_screen(1.-0.05) \
3384 - self.track_to_screen(0.05)
3385 nlinesavail = trackheight/float(fm.lineSpacing())
3386 if nlinesavail > 1:
3387 break
3389 font.setPointSize(font.pointSize()-1)
3391 p.setFont(font)
3392 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3394 for key in self.track_keys:
3395 itrack = self.key_to_row[key]
3396 if itrack in track_projections:
3397 plabel = ' '.join(
3398 [str(x) for x in key if x is not None])
3399 lx = 10
3400 ly = self.track_to_screen(itrack+0.5)
3402 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3403 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3404 continue
3406 contains_cursor = \
3407 self.track_to_screen(itrack) \
3408 < mouse_pos.y() \
3409 < self.track_to_screen(itrack+1)
3411 if not contains_cursor:
3412 continue
3414 font_large = p.font()
3415 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3416 p.setFont(font_large)
3417 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3418 p.setFont(font)
3420 for lab in annot_labels:
3421 lab.draw()
3423 self.timer_draw.stop()
3425 def see_data_params(self):
3427 min_deltat = self.content_deltat_range()[0]
3429 # determine padding and downampling requirements
3430 if self.lowpass is not None:
3431 deltat_target = 1./self.lowpass * 0.25
3432 ndecimate = min(
3433 50,
3434 max(1, int(round(deltat_target / min_deltat))))
3435 tpad = 1./self.lowpass * 2.
3436 else:
3437 ndecimate = 1
3438 tpad = min_deltat*5.
3440 if self.highpass is not None:
3441 tpad = max(1./self.highpass * 2., tpad)
3443 nsee_points_per_trace = 5000*10
3444 tsee = ndecimate*nsee_points_per_trace*min_deltat
3446 return ndecimate, tpad, tsee
3448 def clean_update(self):
3449 self.cached_processed_traces = None
3450 self.cached_chopped_traces = {}
3451 self.update()
3453 def get_adequate_tpad(self):
3454 tpad = 0.
3455 for f in [self.highpass, self.lowpass]:
3456 if f is not None:
3457 tpad = max(tpad, 1.0/f)
3459 for snuffling in self.snufflings:
3460 if snuffling._post_process_hook_enabled \
3461 or snuffling._pre_process_hook_enabled:
3463 tpad = max(tpad, snuffling.get_tpad())
3465 return tpad
3467 def prepare_cutout2(
3468 self, tmin, tmax, trace_selector=None, degap=True,
3469 demean=True, nmax=6000):
3471 if self.pile.is_empty():
3472 return []
3474 nmax = self.visible_length
3476 self.timer_cutout.start()
3478 tsee = tmax-tmin
3479 min_deltat_wo_decimate = tsee/nmax
3480 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3482 min_deltat_allow = min_deltat_wo_decimate
3483 if self.lowpass is not None:
3484 target_deltat_lp = 0.25/self.lowpass
3485 if target_deltat_lp > min_deltat_wo_decimate:
3486 min_deltat_allow = min_deltat_w_decimate
3488 min_deltat_allow = math.exp(
3489 int(math.floor(math.log(min_deltat_allow))))
3491 tmin_ = tmin
3492 tmax_ = tmax
3494 # fetch more than needed?
3495 if self.menuitem_liberal_fetch.isChecked():
3496 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3497 tmin = math.floor(tmin/tlen) * tlen
3498 tmax = math.ceil(tmax/tlen) * tlen
3500 fft_filtering = self.menuitem_fft_filtering.isChecked()
3501 lphp = self.menuitem_lphp.isChecked()
3502 ads = self.menuitem_allowdownsampling.isChecked()
3504 tpad = self.get_adequate_tpad()
3505 tpad = max(tpad, tsee)
3507 # state vector to decide if cached traces can be used
3508 vec = (
3509 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3510 self.highpass, fft_filtering, lphp,
3511 min_deltat_allow, self.rotate, self.shown_tracks_range,
3512 ads, self.pile.get_update_count())
3514 dtmin = 0. if not self.cached_vec else tmin_ - self.cached_vec[0]
3515 dtmax = 0. if not self.cached_vec else tmax_ - self.cached_vec[1]
3517 if (self.cached_vec
3518 and self.cached_vec[0] <= vec[0]
3519 and vec[1] <= self.cached_vec[1]
3520 and vec[2:] == self.cached_vec[2:]
3521 and not (self.reloaded or self.menuitem_watch.isChecked())
3522 and self.cached_processed_traces is not None):
3524 logger.debug('Using cached traces')
3525 processed_traces = self.cached_processed_traces
3527 else:
3528 processed_traces = []
3529 if self.pile.deltatmax >= min_deltat_allow:
3531 def group_selector(gr):
3532 return gr.deltatmax >= min_deltat_allow
3534 if trace_selector is not None:
3535 def trace_selectorx(tr):
3536 return tr.deltat >= min_deltat_allow \
3537 and trace_selector(tr)
3538 else:
3539 def trace_selectorx(tr):
3540 return tr.deltat >= min_deltat_allow
3542 for traces in self.pile.chopper(
3543 tmin=tmin, tmax=tmax, tpad=tpad,
3544 want_incomplete=True,
3545 degap=degap,
3546 maxgap=gap_lap_tolerance,
3547 maxlap=gap_lap_tolerance,
3548 keep_current_files_open=True,
3549 group_selector=group_selector,
3550 trace_selector=trace_selectorx,
3551 accessor_id=id(self),
3552 snap=(math.floor, math.ceil),
3553 include_last=True):
3555 if demean:
3556 for tr in traces:
3557 if (tr.meta and tr.meta.get('tabu', False)):
3558 continue
3559 y = tr.get_ydata()
3560 tr.set_ydata(y - num.mean(y))
3562 traces = self.pre_process_hooks(traces)
3564 for trace in traces:
3566 if not (trace.meta
3567 and trace.meta.get('tabu', False)):
3569 if fft_filtering:
3570 but = pyrocko.response.ButterworthResponse
3571 multres = pyrocko.response.MultiplyResponse
3572 if self.lowpass is not None \
3573 or self.highpass is not None:
3575 it = num.arange(
3576 trace.data_len(), dtype=float)
3577 detr_data, m, b = detrend(
3578 it, trace.get_ydata())
3580 trace.set_ydata(detr_data)
3582 freqs, fdata = trace.spectrum(
3583 pad_to_pow2=True, tfade=None)
3585 nfreqs = fdata.size
3587 key = (trace.deltat, nfreqs)
3589 if key not in self.tf_cache:
3590 resps = []
3591 if self.lowpass is not None:
3592 resps.append(but(
3593 order=4,
3594 corner=self.lowpass,
3595 type='low'))
3597 if self.highpass is not None:
3598 resps.append(but(
3599 order=4,
3600 corner=self.highpass,
3601 type='high'))
3603 resp = multres(resps)
3604 self.tf_cache[key] = \
3605 resp.evaluate(freqs)
3607 filtered_data = num.fft.irfft(
3608 fdata*self.tf_cache[key]
3609 )[:trace.data_len()]
3611 retrended_data = retrend(
3612 it, filtered_data, m, b)
3614 trace.set_ydata(retrended_data)
3616 else:
3618 if ads and self.lowpass is not None:
3619 while trace.deltat \
3620 < min_deltat_wo_decimate:
3622 trace.downsample(2, demean=False)
3624 fmax = 0.5/trace.deltat
3625 if not lphp and (
3626 self.lowpass is not None
3627 and self.highpass is not None
3628 and self.lowpass < fmax
3629 and self.highpass < fmax
3630 and self.highpass < self.lowpass):
3632 trace.bandpass(
3633 2, self.highpass, self.lowpass)
3634 else:
3635 if self.lowpass is not None:
3636 if self.lowpass < 0.5/trace.deltat:
3637 trace.lowpass(
3638 4, self.lowpass,
3639 demean=False)
3641 if self.highpass is not None:
3642 if self.lowpass is None \
3643 or self.highpass \
3644 < self.lowpass:
3646 if self.highpass < \
3647 0.5/trace.deltat:
3648 trace.highpass(
3649 4, self.highpass,
3650 demean=False)
3652 processed_traces.append(trace)
3654 if self.rotate != 0.0:
3655 phi = self.rotate/180.*math.pi
3656 cphi = math.cos(phi)
3657 sphi = math.sin(phi)
3658 for a in processed_traces:
3659 for b in processed_traces:
3660 if (a.network == b.network
3661 and a.station == b.station
3662 and a.location == b.location
3663 and ((a.channel.lower().endswith('n')
3664 and b.channel.lower().endswith('e'))
3665 or (a.channel.endswith('1')
3666 and b.channel.endswith('2')))
3667 and abs(a.deltat-b.deltat) < a.deltat*0.001
3668 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3669 len(a.get_ydata()) == len(b.get_ydata())):
3671 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3672 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3673 a.set_ydata(aydata)
3674 b.set_ydata(bydata)
3676 processed_traces = self.post_process_hooks(processed_traces)
3678 self.cached_processed_traces = processed_traces
3679 self.cached_vec = vec
3681 chopped_traces = []
3682 for trace in processed_traces:
3683 chop_tmin = tmin_ - trace.deltat*4
3684 chop_tmax = tmax_ + trace.deltat*4
3685 trace_hash = trace.hash(unsafe=True)
3687 # Use cache if tmin and tmax have not changed
3688 if dtmin == 0. and dtmax == 0. \
3689 and trace_hash in self.cached_chopped_traces:
3690 ctrace = self.cached_chopped_traces[trace_hash]
3692 else:
3693 try:
3694 ctrace = trace.chop(
3695 chop_tmin, chop_tmax,
3696 inplace=False)
3698 except pyrocko.trace.NoData:
3699 continue
3701 if ctrace.data_len() < 2:
3702 continue
3704 self.cached_chopped_traces[trace_hash] = ctrace
3705 chopped_traces.append(ctrace)
3707 self.timer_cutout.stop()
3708 return chopped_traces
3710 def pre_process_hooks(self, traces):
3711 for snuffling in self.snufflings:
3712 if snuffling._pre_process_hook_enabled:
3713 traces = snuffling.pre_process_hook(traces)
3715 return traces
3717 def post_process_hooks(self, traces):
3718 for snuffling in self.snufflings:
3719 if snuffling._post_process_hook_enabled:
3720 traces = snuffling.post_process_hook(traces)
3722 return traces
3724 def visible_length_change(self, ignore=None):
3725 for menuitem, vlen in self.menuitems_visible_length:
3726 if menuitem.isChecked():
3727 self.visible_length = vlen
3729 def scaling_base_change(self, ignore=None):
3730 for menuitem, scaling_base in self.menuitems_scaling_base:
3731 if menuitem.isChecked():
3732 self.scaling_base = scaling_base
3734 def scalingmode_change(self, ignore=None):
3735 for menuitem, scaling_key in self.menuitems_scaling:
3736 if menuitem.isChecked():
3737 self.scaling_key = scaling_key
3738 self.update()
3740 def apply_scaling_hooks(self, data_ranges):
3741 for k in sorted(self.scaling_hooks.keys()):
3742 hook = self.scaling_hooks[k]
3743 hook(data_ranges)
3745 def viewmode_change(self, ignore=True):
3746 for item, mode in self.menuitems_viewmode:
3747 if item.isChecked():
3748 self.view_mode = mode
3749 break
3750 else:
3751 raise AttributeError('unknown view mode')
3753 items_waterfall_disabled = (
3754 self.menuitem_showscaleaxis,
3755 self.menuitem_showscalerange,
3756 self.menuitem_showzeroline,
3757 self.menuitem_colortraces,
3758 self.menuitem_cliptraces,
3759 *(itm[0] for itm in self.menuitems_visible_length)
3760 )
3762 if self.view_mode is ViewMode.Waterfall:
3763 self.parent().show_colorbar_ctrl(True)
3764 self.parent().show_gain_ctrl(False)
3766 for item in items_waterfall_disabled:
3767 item.setDisabled(True)
3769 self.visible_length = 180.
3770 else:
3771 self.parent().show_colorbar_ctrl(False)
3772 self.parent().show_gain_ctrl(True)
3774 for item in items_waterfall_disabled:
3775 item.setDisabled(False)
3777 self.visible_length_change()
3778 self.update()
3780 def set_scaling_hook(self, k, hook):
3781 self.scaling_hooks[k] = hook
3783 def remove_scaling_hook(self, k):
3784 del self.scaling_hooks[k]
3786 def remove_scaling_hooks(self):
3787 self.scaling_hooks = {}
3789 def s_sortingmode_change(self, ignore=None):
3790 for menuitem, valfunc in self.menuitems_ssorting:
3791 if menuitem.isChecked():
3792 self._ssort = valfunc
3794 self.sortingmode_change()
3796 def sortingmode_change(self, ignore=None):
3797 for menuitem, (gather, color) in self.menuitems_sorting:
3798 if menuitem.isChecked():
3799 self.set_gathering(gather, color)
3801 self.sortingmode_change_time = time.time()
3803 def lowpass_change(self, value, ignore=None):
3804 self.lowpass = value
3805 self.passband_check()
3806 self.tf_cache = {}
3807 self.update()
3809 def highpass_change(self, value, ignore=None):
3810 self.highpass = value
3811 self.passband_check()
3812 self.tf_cache = {}
3813 self.update()
3815 def passband_check(self):
3816 if self.highpass and self.lowpass \
3817 and self.highpass >= self.lowpass:
3819 self.message = 'Corner frequency of highpass larger than ' \
3820 'corner frequency of lowpass! I will now ' \
3821 'deactivate the highpass.'
3823 self.update_status()
3824 else:
3825 oldmess = self.message
3826 self.message = None
3827 if oldmess is not None:
3828 self.update_status()
3830 def gain_change(self, value, ignore):
3831 self.gain = value
3832 self.update()
3834 def rot_change(self, value, ignore):
3835 self.rotate = value
3836 self.update()
3838 def waterfall_cmap_change(self, cmap):
3839 self.waterfall_cmap = cmap
3840 self.update()
3842 def waterfall_clip_change(self, clip_min, clip_max):
3843 self.waterfall_clip_min = clip_min
3844 self.waterfall_clip_max = clip_max
3845 self.update()
3847 def waterfall_show_absolute_change(self, toggle):
3848 self.waterfall_show_absolute = toggle
3849 self.update()
3851 def waterfall_set_integrate(self, toggle):
3852 self.waterfall_integrate = toggle
3853 self.update()
3855 def set_selected_markers(self, markers):
3856 '''
3857 Set a list of markers selected
3859 :param markers: list of markers
3860 '''
3861 self.deselect_all()
3862 for m in markers:
3863 m.selected = True
3865 self.update()
3867 def deselect_all(self):
3868 for marker in self.markers:
3869 marker.selected = False
3871 def animate_picking(self):
3872 point = self.mapFromGlobal(qg.QCursor.pos())
3873 self.update_picking(point.x(), point.y(), doshift=True)
3875 def get_nslc_ids_for_track(self, ftrack):
3876 itrack = int(ftrack)
3877 return self.track_to_nslc_ids.get(itrack, [])
3879 def stop_picking(self, x, y, abort=False):
3880 if self.picking:
3881 self.update_picking(x, y, doshift=False)
3882 self.picking = None
3883 self.picking_down = None
3884 self.picking_timer.stop()
3885 self.picking_timer = None
3886 if not abort:
3887 self.add_marker(self.floating_marker)
3888 self.floating_marker.selected = True
3889 self.emit_selected_markers()
3891 self.floating_marker = None
3893 def start_picking(self, ignore):
3895 if not self.picking:
3896 self.deselect_all()
3897 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3898 point = self.mapFromGlobal(qg.QCursor.pos())
3900 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3901 self.picking.setGeometry(
3902 gpoint.x(), gpoint.y(), 1, self.height())
3903 t = self.time_projection.rev(point.x())
3905 ftrack = self.track_to_screen.rev(point.y())
3906 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3907 self.floating_marker = Marker(nslc_ids, t, t)
3908 self.floating_marker.selected = True
3910 self.picking_timer = qc.QTimer()
3911 self.picking_timer.timeout.connect(
3912 self.animate_picking)
3914 self.picking_timer.setInterval(50)
3915 self.picking_timer.start()
3917 def update_picking(self, x, y, doshift=False):
3918 if self.picking:
3919 mouset = self.time_projection.rev(x)
3920 dt = 0.0
3921 if mouset < self.tmin or mouset > self.tmax:
3922 if mouset < self.tmin:
3923 dt = -(self.tmin - mouset)
3924 else:
3925 dt = mouset - self.tmax
3926 ddt = self.tmax-self.tmin
3927 dt = max(dt, -ddt/10.)
3928 dt = min(dt, ddt/10.)
3930 x0 = x
3931 if self.picking_down is not None:
3932 x0 = self.time_projection(self.picking_down[0])
3934 w = abs(x-x0)
3935 x0 = min(x0, x)
3937 tmin, tmax = (
3938 self.time_projection.rev(x0),
3939 self.time_projection.rev(x0+w))
3941 tmin, tmax = (
3942 max(working_system_time_range[0], tmin),
3943 min(working_system_time_range[1], tmax))
3945 p1 = self.mapToGlobal(qc.QPoint(x0, 0))
3947 self.picking.setGeometry(
3948 p1.x(), p1.y(), max(w, 1), self.height())
3950 ftrack = self.track_to_screen.rev(y)
3951 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3952 self.floating_marker.set(nslc_ids, tmin, tmax)
3954 if dt != 0.0 and doshift:
3955 self.interrupt_following()
3956 self.set_time_range(self.tmin+dt, self.tmax+dt)
3958 self.update()
3960 def update_status(self):
3962 if self.message is None:
3963 point = self.mapFromGlobal(qg.QCursor.pos())
3965 mouse_t = self.time_projection.rev(point.x())
3966 if not is_working_time(mouse_t):
3967 return
3969 if self.floating_marker:
3970 tmi, tma = (
3971 self.floating_marker.tmin,
3972 self.floating_marker.tmax)
3974 tt, ms = gmtime_x(tmi)
3976 if tmi == tma:
3977 message = mystrftime(
3978 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
3979 tt=tt, milliseconds=ms)
3980 else:
3981 srange = '%g s' % (tma-tmi)
3982 message = mystrftime(
3983 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
3984 tt=tt, milliseconds=ms)
3985 else:
3986 tt, ms = gmtime_x(mouse_t)
3988 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
3989 else:
3990 message = self.message
3992 sb = self.window().statusBar()
3993 sb.clearMessage()
3994 sb.showMessage(message)
3996 def set_sortingmode_change_delay_time(self, dt):
3997 self.sortingmode_change_delay_time = dt
3999 def sortingmode_change_delayed(self):
4000 now = time.time()
4001 return (
4002 self.sortingmode_change_delay_time is not None
4003 and now - self.sortingmode_change_time
4004 < self.sortingmode_change_delay_time)
4006 def set_visible_marker_kinds(self, kinds):
4007 self.deselect_all()
4008 self.visible_marker_kinds = tuple(kinds)
4009 self.emit_selected_markers()
4011 def following(self):
4012 return self.follow_timer is not None \
4013 and not self.following_interrupted()
4015 def interrupt_following(self):
4016 self.interactive_range_change_time = time.time()
4018 def following_interrupted(self, now=None):
4019 if now is None:
4020 now = time.time()
4021 return now - self.interactive_range_change_time \
4022 < self.interactive_range_change_delay_time
4024 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4025 if tmax_start is None:
4026 tmax_start = time.time()
4027 self.show_all = False
4028 self.follow_time = tlen
4029 self.follow_timer = qc.QTimer(self)
4030 self.follow_timer.timeout.connect(
4031 self.follow_update)
4032 self.follow_timer.setInterval(interval)
4033 self.follow_timer.start()
4034 self.follow_started = time.time()
4035 self.follow_lapse = lapse
4036 self.follow_tshift = self.follow_started - tmax_start
4037 self.interactive_range_change_time = 0.0
4039 def unfollow(self):
4040 if self.follow_timer is not None:
4041 self.follow_timer.stop()
4042 self.follow_timer = None
4043 self.interactive_range_change_time = 0.0
4045 def follow_update(self):
4046 rnow = time.time()
4047 if self.follow_lapse is None:
4048 now = rnow
4049 else:
4050 now = self.follow_started + (rnow - self.follow_started) \
4051 * self.follow_lapse
4053 if self.following_interrupted(rnow):
4054 return
4055 self.set_time_range(
4056 now-self.follow_time-self.follow_tshift,
4057 now-self.follow_tshift)
4059 self.update()
4061 def myclose(self, return_tag=''):
4062 self.return_tag = return_tag
4063 self.window().close()
4065 def cleanup(self):
4066 self.about_to_close.emit()
4067 self.timer.stop()
4068 if self.follow_timer is not None:
4069 self.follow_timer.stop()
4071 for snuffling in list(self.snufflings):
4072 self.remove_snuffling(snuffling)
4074 def set_error_message(self, key, value):
4075 if value is None:
4076 if key in self.error_messages:
4077 del self.error_messages[key]
4078 else:
4079 self.error_messages[key] = value
4081 def inputline_changed(self, text):
4082 pass
4084 def inputline_finished(self, text):
4085 line = str(text)
4087 toks = line.split()
4088 clearit, hideit, error = False, True, None
4089 if len(toks) >= 1:
4090 command = toks[0].lower()
4092 try:
4093 quick_filter_commands = {
4094 'n': '%s.*.*.*',
4095 's': '*.%s.*.*',
4096 'l': '*.*.%s.*',
4097 'c': '*.*.*.%s'}
4099 if command in quick_filter_commands:
4100 if len(toks) >= 2:
4101 patterns = [
4102 quick_filter_commands[toks[0]] % pat
4103 for pat in toks[1:]]
4104 self.set_quick_filter_patterns(patterns, line)
4105 else:
4106 self.set_quick_filter_patterns(None)
4108 self.update()
4110 elif command in ('hide', 'unhide'):
4111 if len(toks) >= 2:
4112 patterns = []
4113 if len(toks) == 2:
4114 patterns = [toks[1]]
4115 elif len(toks) >= 3:
4116 x = {
4117 'n': '%s.*.*.*',
4118 's': '*.%s.*.*',
4119 'l': '*.*.%s.*',
4120 'c': '*.*.*.%s'}
4122 if toks[1] in x:
4123 patterns.extend(
4124 x[toks[1]] % tok for tok in toks[2:])
4126 for pattern in patterns:
4127 if command == 'hide':
4128 self.add_blacklist_pattern(pattern)
4129 else:
4130 self.remove_blacklist_pattern(pattern)
4132 elif command == 'unhide' and len(toks) == 1:
4133 self.clear_blacklist()
4135 clearit = True
4137 self.update()
4139 elif command == 'markers':
4140 if len(toks) == 2:
4141 if toks[1] == 'all':
4142 kinds = self.all_marker_kinds
4143 else:
4144 kinds = []
4145 for x in toks[1]:
4146 try:
4147 kinds.append(int(x))
4148 except Exception:
4149 pass
4151 self.set_visible_marker_kinds(kinds)
4153 elif len(toks) == 1:
4154 self.set_visible_marker_kinds(())
4156 self.update()
4158 elif command == 'scaling':
4159 if len(toks) == 2:
4160 hideit = False
4161 error = 'wrong number of arguments'
4163 if len(toks) >= 3:
4164 vmin, vmax = [
4165 pyrocko.model.float_or_none(x)
4166 for x in toks[-2:]]
4168 def upd(d, k, vmin, vmax):
4169 if k in d:
4170 if vmin is not None:
4171 d[k] = vmin, d[k][1]
4172 if vmax is not None:
4173 d[k] = d[k][0], vmax
4175 if len(toks) == 1:
4176 self.remove_scaling_hooks()
4178 elif len(toks) == 3:
4179 def hook(data_ranges):
4180 for k in data_ranges:
4181 upd(data_ranges, k, vmin, vmax)
4183 self.set_scaling_hook('_', hook)
4185 elif len(toks) == 4:
4186 pattern = toks[1]
4188 def hook(data_ranges):
4189 for k in pyrocko.util.match_nslcs(
4190 pattern, list(data_ranges.keys())):
4192 upd(data_ranges, k, vmin, vmax)
4194 self.set_scaling_hook(pattern, hook)
4196 elif command == 'goto':
4197 toks2 = line.split(None, 1)
4198 if len(toks2) == 2:
4199 arg = toks2[1]
4200 m = re.match(
4201 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4202 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4203 if m:
4204 tlen = None
4205 if not m.group(1):
4206 tlen = 12*32*24*60*60
4207 elif not m.group(2):
4208 tlen = 32*24*60*60
4209 elif not m.group(3):
4210 tlen = 24*60*60
4211 elif not m.group(4):
4212 tlen = 60*60
4213 elif not m.group(5):
4214 tlen = 60
4216 supl = '1970-01-01 00:00:00'
4217 if len(supl) > len(arg):
4218 arg = arg + supl[-(len(supl)-len(arg)):]
4219 t = pyrocko.util.str_to_time(arg)
4220 self.go_to_time(t, tlen=tlen)
4222 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4223 supl = '00:00:00'
4224 if len(supl) > len(arg):
4225 arg = arg + supl[-(len(supl)-len(arg)):]
4226 tmin, tmax = self.get_time_range()
4227 sdate = pyrocko.util.time_to_str(
4228 tmin/2.+tmax/2., format='%Y-%m-%d')
4229 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4230 self.go_to_time(t)
4232 else:
4233 self.go_to_event_by_name(arg)
4235 else:
4236 raise PileViewerMainException(
4237 'No such command: %s' % command)
4239 except PileViewerMainException as e:
4240 error = str(e)
4241 hideit = False
4243 return clearit, hideit, error
4245 return PileViewerMain
4248PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4249GLPileViewerMain = MakePileViewerMainClass(qgl.QGLWidget)
4252class LineEditWithAbort(qw.QLineEdit):
4254 aborted = qc.pyqtSignal()
4255 history_down = qc.pyqtSignal()
4256 history_up = qc.pyqtSignal()
4258 def keyPressEvent(self, key_event):
4259 if key_event.key() == qc.Qt.Key_Escape:
4260 self.aborted.emit()
4261 elif key_event.key() == qc.Qt.Key_Down:
4262 self.history_down.emit()
4263 elif key_event.key() == qc.Qt.Key_Up:
4264 self.history_up.emit()
4265 else:
4266 return qw.QLineEdit.keyPressEvent(self, key_event)
4269class PileViewer(qw.QFrame):
4270 '''
4271 PileViewerMain + Controls + Inputline
4272 '''
4274 def __init__(
4275 self, pile,
4276 ntracks_shown_max=20,
4277 marker_editor_sortable=True,
4278 use_opengl=False,
4279 panel_parent=None,
4280 *args):
4282 qw.QFrame.__init__(self, *args)
4284 layout = qw.QGridLayout()
4285 layout.setContentsMargins(0, 0, 0, 0)
4286 layout.setSpacing(0)
4288 self.menu = PileViewerMenuBar(self)
4290 if use_opengl:
4291 self.viewer = GLPileViewerMain(
4292 pile,
4293 ntracks_shown_max=ntracks_shown_max,
4294 panel_parent=panel_parent,
4295 menu=self.menu)
4296 else:
4297 self.viewer = PileViewerMain(
4298 pile,
4299 ntracks_shown_max=ntracks_shown_max,
4300 panel_parent=panel_parent,
4301 menu=self.menu)
4303 self.marker_editor_sortable = marker_editor_sortable
4305 self.setFrameShape(qw.QFrame.StyledPanel)
4306 self.setFrameShadow(qw.QFrame.Sunken)
4308 self.input_area = qw.QFrame(self)
4309 ia_layout = qw.QGridLayout()
4310 ia_layout.setContentsMargins(11, 11, 11, 11)
4311 self.input_area.setLayout(ia_layout)
4313 self.inputline = LineEditWithAbort(self.input_area)
4314 self.inputline.returnPressed.connect(
4315 self.inputline_returnpressed)
4316 self.inputline.editingFinished.connect(
4317 self.inputline_finished)
4318 self.inputline.aborted.connect(
4319 self.inputline_aborted)
4321 self.inputline.history_down.connect(
4322 lambda: self.step_through_history(1))
4323 self.inputline.history_up.connect(
4324 lambda: self.step_through_history(-1))
4326 self.inputline.textEdited.connect(
4327 self.inputline_changed)
4329 self.inputline.setPlaceholderText(
4330 u'Quick commands: e.g. \'c HH?\' to select channels. '
4331 u'Use ↑ or ↓ to navigate.')
4332 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4333 self.input_area.hide()
4334 self.history = None
4336 self.inputline_error_str = None
4338 self.inputline_error = qw.QLabel()
4339 self.inputline_error.hide()
4341 ia_layout.addWidget(self.inputline, 0, 0)
4342 ia_layout.addWidget(self.inputline_error, 1, 0)
4343 layout.addWidget(self.input_area, 0, 0, 1, 2)
4344 layout.addWidget(self.viewer, 1, 0)
4346 pb = Progressbars(self)
4347 layout.addWidget(pb, 2, 0, 1, 2)
4348 self.progressbars = pb
4350 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4351 self.scrollbar = scrollbar
4352 layout.addWidget(scrollbar, 1, 1)
4353 self.scrollbar.valueChanged.connect(
4354 self.scrollbar_changed)
4356 self.block_scrollbar_changes = False
4358 self.viewer.want_input.connect(
4359 self.inputline_show)
4360 self.viewer.tracks_range_changed.connect(
4361 self.tracks_range_changed)
4362 self.viewer.pile_has_changed_signal.connect(
4363 self.adjust_controls)
4364 self.viewer.about_to_close.connect(
4365 self.save_inputline_history)
4367 self.setLayout(layout)
4369 def cleanup(self):
4370 self.viewer.cleanup()
4372 def get_progressbars(self):
4373 return self.progressbars
4375 def inputline_show(self):
4376 if not self.history:
4377 self.load_inputline_history()
4379 self.input_area.show()
4380 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4381 self.inputline.selectAll()
4383 def inputline_set_error(self, string):
4384 self.inputline_error_str = string
4385 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4386 self.inputline.selectAll()
4387 self.inputline_error.setText(string)
4388 self.input_area.show()
4389 self.inputline_error.show()
4391 def inputline_clear_error(self):
4392 if self.inputline_error_str:
4393 self.inputline.setPalette(qw.QApplication.palette())
4394 self.inputline_error_str = None
4395 self.inputline_error.clear()
4396 self.inputline_error.hide()
4398 def inputline_changed(self, line):
4399 self.viewer.inputline_changed(str(line))
4400 self.inputline_clear_error()
4402 def inputline_returnpressed(self):
4403 line = str(self.inputline.text())
4404 clearit, hideit, error = self.viewer.inputline_finished(line)
4406 if error:
4407 self.inputline_set_error(error)
4409 line = line.strip()
4411 if line != '' and not error:
4412 if not (len(self.history) >= 1 and line == self.history[-1]):
4413 self.history.append(line)
4415 if clearit:
4417 self.inputline.blockSignals(True)
4418 qpat, qinp = self.viewer.get_quick_filter_patterns()
4419 if qpat is None:
4420 self.inputline.clear()
4421 else:
4422 self.inputline.setText(qinp)
4423 self.inputline.blockSignals(False)
4425 if hideit and not error:
4426 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4427 self.input_area.hide()
4429 self.hist_ind = len(self.history)
4431 def inputline_aborted(self):
4432 '''
4433 Hide the input line.
4434 '''
4435 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4436 self.hist_ind = len(self.history)
4437 self.input_area.hide()
4439 def save_inputline_history(self):
4440 '''
4441 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4442 '''
4443 if not self.history:
4444 return
4446 conf = pyrocko.config
4447 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4448 with open(fn_hist, 'w') as f:
4449 i = min(100, len(self.history))
4450 for c in self.history[-i:]:
4451 f.write('%s\n' % c)
4453 def load_inputline_history(self):
4454 '''
4455 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4456 '''
4457 conf = pyrocko.config
4458 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4459 if not os.path.exists(fn_hist):
4460 with open(fn_hist, 'w+') as f:
4461 f.write('\n')
4463 with open(fn_hist, 'r') as f:
4464 self.history = [line.strip() for line in f.readlines()]
4466 self.hist_ind = len(self.history)
4468 def step_through_history(self, ud=1):
4469 '''
4470 Step through input line history and set the input line text.
4471 '''
4472 n = len(self.history)
4473 self.hist_ind += ud
4474 self.hist_ind %= (n + 1)
4475 if len(self.history) != 0 and self.hist_ind != n:
4476 self.inputline.setText(self.history[self.hist_ind])
4477 else:
4478 self.inputline.setText('')
4480 def inputline_finished(self):
4481 pass
4483 def tracks_range_changed(self, ntracks, ilo, ihi):
4484 if self.block_scrollbar_changes:
4485 return
4487 self.scrollbar.blockSignals(True)
4488 self.scrollbar.setPageStep(ihi-ilo)
4489 vmax = max(0, ntracks-(ihi-ilo))
4490 self.scrollbar.setRange(0, vmax)
4491 self.scrollbar.setValue(ilo)
4492 self.scrollbar.setHidden(vmax == 0)
4493 self.scrollbar.blockSignals(False)
4495 def scrollbar_changed(self, value):
4496 self.block_scrollbar_changes = True
4497 ilo = value
4498 ihi = ilo + self.scrollbar.pageStep()
4499 self.viewer.set_tracks_range((ilo, ihi))
4500 self.block_scrollbar_changes = False
4501 self.update_contents()
4503 def controls(self):
4504 frame = qw.QFrame(self)
4505 layout = qw.QGridLayout()
4506 frame.setLayout(layout)
4508 minfreq = 0.001
4509 maxfreq = 1000.0
4510 self.lowpass_control = ValControl(high_is_none=True)
4511 self.lowpass_control.setup(
4512 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4513 self.highpass_control = ValControl(low_is_none=True)
4514 self.highpass_control.setup(
4515 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4516 self.gain_control = ValControl()
4517 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4518 self.rot_control = LinValControl()
4519 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4520 self.colorbar_control = ColorbarControl(self)
4522 self.lowpass_control.valchange.connect(
4523 self.viewer.lowpass_change)
4524 self.highpass_control.valchange.connect(
4525 self.viewer.highpass_change)
4526 self.gain_control.valchange.connect(
4527 self.viewer.gain_change)
4528 self.rot_control.valchange.connect(
4529 self.viewer.rot_change)
4530 self.colorbar_control.cmap_changed.connect(
4531 self.viewer.waterfall_cmap_change
4532 )
4533 self.colorbar_control.clip_changed.connect(
4534 self.viewer.waterfall_clip_change
4535 )
4536 self.colorbar_control.show_absolute_toggled.connect(
4537 self.viewer.waterfall_show_absolute_change
4538 )
4539 self.colorbar_control.show_integrate_toggled.connect(
4540 self.viewer.waterfall_set_integrate
4541 )
4543 for icontrol, control in enumerate((
4544 self.highpass_control,
4545 self.lowpass_control,
4546 self.gain_control,
4547 self.rot_control,
4548 self.colorbar_control)):
4550 for iwidget, widget in enumerate(control.widgets()):
4551 layout.addWidget(widget, icontrol, iwidget)
4553 spacer = qw.QSpacerItem(
4554 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4555 layout.addItem(spacer, 4, 0, 1, 3)
4557 self.adjust_controls()
4558 self.viewer.viewmode_change(ViewMode.Wiggle)
4559 return frame
4561 def marker_editor(self):
4562 editor = pyrocko.gui.marker_editor.MarkerEditor(
4563 self, sortable=self.marker_editor_sortable)
4565 editor.set_viewer(self.get_view())
4566 editor.get_marker_model().dataChanged.connect(
4567 self.update_contents)
4568 return editor
4570 def adjust_controls(self):
4571 dtmin, dtmax = self.viewer.content_deltat_range()
4572 maxfreq = 0.5/dtmin
4573 minfreq = (0.5/dtmax)*0.001
4574 self.lowpass_control.set_range(minfreq, maxfreq)
4575 self.highpass_control.set_range(minfreq, maxfreq)
4577 def setup_snufflings(self):
4578 self.viewer.setup_snufflings()
4580 def get_view(self):
4581 return self.viewer
4583 def update_contents(self):
4584 self.viewer.update()
4586 def get_pile(self):
4587 return self.viewer.get_pile()
4589 def show_colorbar_ctrl(self, show):
4590 for w in self.colorbar_control.widgets():
4591 w.setVisible(show)
4593 def show_gain_ctrl(self, show):
4594 for w in self.gain_control.widgets():
4595 w.setVisible(show)