Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/gui/snuffler/pile_viewer.py: 71%
2855 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import os
7import time
8import calendar
9import datetime
10import re
11import math
12import logging
13import operator
14import copy
15import enum
16from itertools import groupby
18import numpy as num
19import pyrocko.model
20import pyrocko.pile
21import pyrocko.trace
22import pyrocko.response
23import pyrocko.util
24import pyrocko.plot
25import pyrocko.gui.snuffler.snuffling
26import pyrocko.gui.snuffler.snufflings
27import pyrocko.gui.snuffler.marker_editor
29from pyrocko.util import hpfloat, gmtime_x, mystrftime
31from .marker import associate_phases_to_events, MarkerOneNSLCRequired
33from ..util import (ValControl, LinValControl, Marker, EventMarker,
34 PhaseMarker, make_QPolygonF, draw_label, Label,
35 Progressbars, ColorbarControl)
37from ..qt_compat import qc, qg, qw, qsvg
39from .pile_viewer_waterfall import TraceWaterfall
41import scipy.stats as sstats
42import platform
44MIN_LABEL_SIZE_PT = 6
46qc.QString = str
48qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
49 qw.QFileDialog.DontUseSheet
51is_macos = platform.uname()[0] == 'Darwin'
53logger = logging.getLogger('pyrocko.gui.snuffler.pile_viewer')
56def detrend(x, y):
57 slope, offset, _, _, _ = sstats.linregress(x, y)
58 y_detrended = y - slope * x - offset
59 return y_detrended, slope, offset
62def retrend(x, y_detrended, slope, offset):
63 return x * slope + y_detrended + offset
66class Global(object):
67 appOnDemand = None
70class NSLC(object):
71 def __init__(self, n, s, l=None, c=None): # noqa
72 self.network = n
73 self.station = s
74 self.location = l
75 self.channel = c
78class m_float(float):
80 def __str__(self):
81 if abs(self) >= 10000.:
82 return '%g km' % round(self/1000., 0)
83 elif abs(self) >= 1000.:
84 return '%g km' % round(self/1000., 1)
85 else:
86 return '%.5g m' % self
88 def __lt__(self, other):
89 if other is None:
90 return True
91 return float(self) < float(other)
93 def __gt__(self, other):
94 if other is None:
95 return False
96 return float(self) > float(other)
99def m_float_or_none(x):
100 if x is None:
101 return None
102 else:
103 return m_float(x)
106def make_chunks(items):
107 '''
108 Split a list of integers into sublists of consecutive elements.
109 '''
110 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
111 enumerate(items), (lambda x: x[1]-x[0]))]
114class deg_float(float):
116 def __str__(self):
117 return '%4.0f' % self
119 def __lt__(self, other):
120 if other is None:
121 return True
122 return float(self) < float(other)
124 def __gt__(self, other):
125 if other is None:
126 return False
127 return float(self) > float(other)
130def deg_float_or_none(x):
131 if x is None:
132 return None
133 else:
134 return deg_float(x)
137class sector_int(int):
139 def __str__(self):
140 return '[%i]' % self
142 def __lt__(self, other):
143 if other is None:
144 return True
145 return int(self) < int(other)
147 def __gt__(self, other):
148 if other is None:
149 return False
150 return int(self) > int(other)
153def num_to_html(num):
154 snum = '%g' % num
155 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
156 if m:
157 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
159 return snum
162gap_lap_tolerance = 5.
165class ViewMode(enum.Enum):
166 Wiggle = 1
167 Waterfall = 2
170class Timer(object):
171 def __init__(self):
172 self._start = None
173 self._stop = None
175 def start(self):
176 self._start = os.times()
178 def stop(self):
179 self._stop = os.times()
181 def get(self):
182 a = self._start
183 b = self._stop
184 if a is not None and b is not None:
185 return tuple([b[i] - a[i] for i in range(5)])
186 else:
187 return tuple([0.] * 5)
189 def __sub__(self, other):
190 a = self.get()
191 b = other.get()
192 return tuple([a[i] - b[i] for i in range(5)])
195class ObjectStyle(object):
196 def __init__(self, frame_pen, fill_brush):
197 self.frame_pen = frame_pen
198 self.fill_brush = fill_brush
201box_styles = []
202box_alpha = 100
203for color in 'orange skyblue butter chameleon chocolate plum ' \
204 'scarletred'.split():
206 box_styles.append(ObjectStyle(
207 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
208 qg.QBrush(qg.QColor(
209 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
210 ))
212box_styles_coverage = {}
214box_styles_coverage['waveform'] = [
215 ObjectStyle(
216 qg.QPen(
217 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
218 1, qc.Qt.DashLine),
219 qg.QBrush(qg.QColor(
220 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
221 ),
222 ObjectStyle(
223 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
224 qg.QBrush(qg.QColor(
225 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
226 ),
227 ObjectStyle(
228 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
229 qg.QBrush(qg.QColor(
230 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
231 )]
233box_styles_coverage['waveform_promise'] = [
234 ObjectStyle(
235 qg.QPen(
236 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
237 1, qc.Qt.DashLine),
238 qg.QBrush(qg.QColor(
239 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
240 ),
241 ObjectStyle(
242 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
243 qg.QBrush(qg.QColor(
244 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
245 ),
246 ObjectStyle(
247 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
248 qg.QBrush(qg.QColor(
249 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
250 )]
252sday = 60*60*24. # \
253smonth = 60*60*24*30. # | only used as approx. intervals...
254syear = 60*60*24*365. # /
256acceptable_tincs = num.array([
257 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
258 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
261working_system_time_range = \
262 pyrocko.util.working_system_time_range()
264initial_time_range = []
266try:
267 initial_time_range.append(
268 calendar.timegm((1950, 1, 1, 0, 0, 0)))
269except Exception:
270 initial_time_range.append(working_system_time_range[0])
272try:
273 initial_time_range.append(
274 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
275except Exception:
276 initial_time_range.append(working_system_time_range[1])
279def is_working_time(t):
280 return working_system_time_range[0] <= t and \
281 t <= working_system_time_range[1]
284def fancy_time_ax_format(inc):
285 l0_fmt_brief = ''
286 l2_fmt = ''
287 l2_trig = 0
288 if inc < 0.000001:
289 l0_fmt = '.%n'
290 l0_center = False
291 l1_fmt = '%H:%M:%S'
292 l1_trig = 6
293 l2_fmt = '%b %d, %Y'
294 l2_trig = 3
295 elif inc < 0.001:
296 l0_fmt = '.%u'
297 l0_center = False
298 l1_fmt = '%H:%M:%S'
299 l1_trig = 6
300 l2_fmt = '%b %d, %Y'
301 l2_trig = 3
302 elif inc < 1:
303 l0_fmt = '.%r'
304 l0_center = False
305 l1_fmt = '%H:%M:%S'
306 l1_trig = 6
307 l2_fmt = '%b %d, %Y'
308 l2_trig = 3
309 elif inc < 60:
310 l0_fmt = '%H:%M:%S'
311 l0_center = False
312 l1_fmt = '%b %d, %Y'
313 l1_trig = 3
314 elif inc < 3600:
315 l0_fmt = '%H:%M'
316 l0_center = False
317 l1_fmt = '%b %d, %Y'
318 l1_trig = 3
319 elif inc < sday:
320 l0_fmt = '%H:%M'
321 l0_center = False
322 l1_fmt = '%b %d, %Y'
323 l1_trig = 3
324 elif inc < smonth:
325 l0_fmt = '%a %d'
326 l0_fmt_brief = '%d'
327 l0_center = True
328 l1_fmt = '%b, %Y'
329 l1_trig = 2
330 elif inc < syear:
331 l0_fmt = '%b'
332 l0_center = True
333 l1_fmt = '%Y'
334 l1_trig = 1
335 else:
336 l0_fmt = '%Y'
337 l0_center = False
338 l1_fmt = ''
339 l1_trig = 0
341 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
344def day_start(timestamp):
345 tt = time.gmtime(int(timestamp))
346 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
347 return calendar.timegm(tts)
350def month_start(timestamp):
351 tt = time.gmtime(int(timestamp))
352 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
353 return calendar.timegm(tts)
356def year_start(timestamp):
357 tt = time.gmtime(int(timestamp))
358 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
359 return calendar.timegm(tts)
362def time_nice_value(inc0):
363 if inc0 < acceptable_tincs[0]:
364 return pyrocko.plot.nice_value(inc0)
365 elif inc0 > acceptable_tincs[-1]:
366 return pyrocko.plot.nice_value(inc0/syear)*syear
367 else:
368 i = num.argmin(num.abs(acceptable_tincs-inc0))
369 return acceptable_tincs[i]
372class TimeScaler(pyrocko.plot.AutoScaler):
373 def __init__(self):
374 pyrocko.plot.AutoScaler.__init__(self)
375 self.mode = 'min-max'
377 def make_scale(self, data_range):
378 assert self.mode in ('min-max', 'off'), \
379 'mode must be "min-max" or "off" for TimeScaler'
381 data_min = min(data_range)
382 data_max = max(data_range)
383 is_reverse = (data_range[0] > data_range[1])
385 mi, ma = data_min, data_max
386 nmi = mi
387 if self.mode != 'off':
388 nmi = mi - self.space*(ma-mi)
390 nma = ma
391 if self.mode != 'off':
392 nma = ma + self.space*(ma-mi)
394 mi, ma = nmi, nma
396 if mi == ma and self.mode != 'off':
397 mi -= 1.0
398 ma += 1.0
400 mi = max(working_system_time_range[0], mi)
401 ma = min(working_system_time_range[1], ma)
403 # make nice tick increment
404 if self.inc is not None:
405 inc = self.inc
406 else:
407 if self.approx_ticks > 0.:
408 inc = time_nice_value((ma-mi)/self.approx_ticks)
409 else:
410 inc = time_nice_value((ma-mi)*10.)
412 if inc == 0.0:
413 inc = 1.0
415 if is_reverse:
416 return ma, mi, -inc
417 else:
418 return mi, ma, inc
420 def make_ticks(self, data_range):
421 mi, ma, inc = self.make_scale(data_range)
423 is_reverse = False
424 if inc < 0:
425 mi, ma, inc = ma, mi, -inc
426 is_reverse = True
428 ticks = []
430 if inc < sday:
431 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
432 if inc < 0.001:
433 mi_day = hpfloat(mi_day)
435 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
436 if inc < 0.001:
437 base = hpfloat(base)
439 base_day = mi_day
440 i = 0
441 while True:
442 tick = base+i*inc
443 if tick > ma:
444 break
446 tick_day = day_start(tick)
447 if tick_day > base_day:
448 base_day = tick_day
449 base = base_day
450 i = 0
451 else:
452 ticks.append(tick)
453 i += 1
455 elif inc < smonth:
456 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
457 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
458 delta = datetime.timedelta(days=int(round(inc/sday)))
459 if mi_day == mi:
460 dt_base += delta
461 i = 0
462 while True:
463 current = dt_base + i*delta
464 tick = calendar.timegm(current.timetuple())
465 if tick > ma:
466 break
467 ticks.append(tick)
468 i += 1
470 elif inc < syear:
471 mi_month = month_start(max(
472 mi, working_system_time_range[0]+smonth*1.5))
474 y, m = time.gmtime(mi_month)[:2]
475 while True:
476 tick = calendar.timegm((y, m, 1, 0, 0, 0))
477 m += 1
478 if m > 12:
479 y, m = y+1, 1
481 if tick > ma:
482 break
484 if tick >= mi:
485 ticks.append(tick)
487 else:
488 mi_year = year_start(max(
489 mi, working_system_time_range[0]+syear*1.5))
491 incy = int(round(inc/syear))
492 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
494 while True:
495 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
496 y += incy
497 if tick > ma:
498 break
499 if tick >= mi:
500 ticks.append(tick)
502 if is_reverse:
503 ticks.reverse()
505 return ticks, inc
508def need_l1_tick(tt, ms, l1_trig):
509 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
512def tick_to_labels(tick, inc):
513 tt, ms = gmtime_x(tick)
514 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
515 fancy_time_ax_format(inc)
517 l0 = mystrftime(l0_fmt, tt, ms)
518 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
519 l1, l2 = None, None
520 if need_l1_tick(tt, ms, l1_trig):
521 l1 = mystrftime(l1_fmt, tt, ms)
522 if need_l1_tick(tt, ms, l2_trig):
523 l2 = mystrftime(l2_fmt, tt, ms)
525 return l0, l0_brief, l0_center, l1, l2
528def l1_l2_tick(tick, inc):
529 tt, ms = gmtime_x(tick)
530 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
531 fancy_time_ax_format(inc)
533 l1 = mystrftime(l1_fmt, tt, ms)
534 l2 = mystrftime(l2_fmt, tt, ms)
535 return l1, l2
538class TimeAx(TimeScaler):
539 def __init__(self, *args):
540 TimeScaler.__init__(self, *args)
542 def drawit(self, p, xprojection, yprojection):
543 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
544 p.setPen(pen)
545 font = qg.QFont()
546 font.setBold(True)
547 p.setFont(font)
548 fm = p.fontMetrics()
549 ticklen = 10
550 pad = 10
551 tmin, tmax = xprojection.get_in_range()
552 ticks, inc = self.make_ticks((tmin, tmax))
553 l1_hits = 0
554 l2_hits = 0
556 vmin, vmax = yprojection(0), yprojection(ticklen)
557 uumin, uumax = xprojection.get_out_range()
558 first_tick_with_label = None
560 data = []
561 for tick in ticks:
562 umin = xprojection(tick)
564 umin_approx_next = xprojection(tick+inc)
565 umax = xprojection(tick)
567 pinc_approx = umin_approx_next - umin
569 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
570 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
572 if tick == 0.0 and tmax - tmin < 3600*24:
573 # hide year at epoch
574 # (we assume that synthetic data is shown)
575 if l2:
576 l2 = None
577 elif l1:
578 l1 = None
580 if l0_center:
581 ushift = (umin_approx_next-umin)/2.
582 else:
583 ushift = 0.
585 abbr_level = 0
586 for l0x in (l0, l0_brief, ''):
587 label0 = l0x
588 rect0 = fm.boundingRect(label0)
589 if rect0.width() <= pinc_approx*0.9:
590 break
592 abbr_level += 1
594 data.append((
595 l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
596 pinc_approx))
598 for (l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
599 pinc_approx) in data:
601 label0 = (l0, l0_brief, '')[abbr_level]
602 rect0 = fm.boundingRect(label0)
604 if uumin+pad < umin-rect0.width()/2.+ushift and \
605 umin+rect0.width()/2.+ushift < uumax-pad:
607 if first_tick_with_label is None:
608 first_tick_with_label = tick
609 p.drawText(qc.QPointF(
610 umin-rect0.width()/2.+ushift,
611 vmin+rect0.height()+ticklen), label0)
613 if l1:
614 label1 = l1
615 rect1 = fm.boundingRect(label1)
616 if uumin+pad < umin-rect1.width()/2. and \
617 umin+rect1.width()/2. < uumax-pad:
619 p.drawText(qc.QPointF(
620 umin-rect1.width()/2.,
621 vmin+rect0.height()+rect1.height()+ticklen),
622 label1)
624 l1_hits += 1
626 if l2:
627 label2 = l2
628 rect2 = fm.boundingRect(label2)
629 if uumin+pad < umin-rect2.width()/2. and \
630 umin+rect2.width()/2. < uumax-pad:
632 p.drawText(qc.QPointF(
633 umin-rect2.width()/2.,
634 vmin+rect0.height()+rect1.height()+rect2.height() +
635 ticklen), label2)
637 l2_hits += 1
639 if first_tick_with_label is None:
640 first_tick_with_label = tmin
642 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
644 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
645 tmax - tmin < 3600*24:
647 # hide year at epoch (we assume that synthetic data is shown)
648 if l2:
649 l2 = None
650 elif l1:
651 l1 = None
653 if l1_hits == 0 and l1:
654 label1 = l1
655 rect1 = fm.boundingRect(label1)
656 p.drawText(qc.QPointF(
657 uumin+pad,
658 vmin+rect0.height()+rect1.height()+ticklen),
659 label1)
661 l1_hits += 1
663 if l2_hits == 0 and l2:
664 label2 = l2
665 rect2 = fm.boundingRect(label2)
666 p.drawText(qc.QPointF(
667 uumin+pad,
668 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
669 label2)
671 v = yprojection(0)
672 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
675class Projection(object):
676 def __init__(self):
677 self.xr = 0., 1.
678 self.ur = 0., 1.
680 def set_in_range(self, xmin, xmax):
681 if xmax == xmin:
682 xmax = xmin + 1.
684 self.xr = xmin, xmax
686 def get_in_range(self):
687 return self.xr
689 def set_out_range(self, umin, umax):
690 if umax == umin:
691 umax = umin + 1.
693 self.ur = umin, umax
695 def get_out_range(self):
696 return self.ur
698 def __call__(self, x):
699 umin, umax = self.ur
700 xmin, xmax = self.xr
701 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
703 def clipped(self, x, umax_pad):
704 umin, umax = self.ur
705 xmin, xmax = self.xr
706 return min(
707 umax-umax_pad,
708 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
710 def rev(self, u):
711 umin, umax = self.ur
712 xmin, xmax = self.xr
713 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
715 def copy(self):
716 return copy.copy(self)
719def add_radiobuttongroup(menu, menudef, target, default=None):
720 group = qw.QActionGroup(menu)
721 group.setExclusive(True)
722 menuitems = []
724 for name, value, *shortcut in menudef:
725 action = menu.addAction(name)
726 action.setCheckable(True)
727 action.setActionGroup(group)
728 if shortcut:
729 action.setShortcut(shortcut[0])
731 menuitems.append((action, value))
732 if default is not None and (
733 name.lower().replace(' ', '_') == default or
734 value == default):
735 action.setChecked(True)
737 group.triggered.connect(target)
739 if default is None:
740 menuitems[0][0].setChecked(True)
742 return menuitems
745def sort_actions(menu):
746 actions = [act for act in menu.actions() if not act.menu()]
747 for action in actions:
748 menu.removeAction(action)
749 actions.sort(key=lambda x: str(x.text()))
751 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
752 if help_action:
753 actions.insert(0, actions.pop(actions.index(help_action[0])))
754 for action in actions:
755 menu.addAction(action)
758fkey_map = dict(zip(
759 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
760 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
761 range(10)))
764class PileViewerMainException(Exception):
765 pass
768class PileViewerMenuBar(qw.QMenuBar):
769 ...
772class PileViewerMenuBarButton(qw.QPushButton):
774 def __init__(self, text, *args, **kwargs):
775 qw.QPushButton.__init__(self, text, *args, **kwargs)
776 self.setFlat(True)
777 self.setSizePolicy(
778 qw.QSizePolicy.Preferred, qw.QSizePolicy.Preferred)
779 self.setContentsMargins(0, 0, 0, 0)
780 s = self.fontMetrics().boundingRect(text)
781 self.setMaximumHeight(s.height() + 3)
782 self.setMaximumWidth(max(s.height() + 3, s.width() + 10))
784 def sizeHint(self):
785 s = qw.QPushButton.sizeHint(self)
786 return qc.QSize(max(s.height(), s.width()), s.height())
789def MakePileViewerMainClass(base):
791 class PileViewerMain(base):
793 want_input = qc.pyqtSignal()
794 toggle_input = qc.pyqtSignal()
795 about_to_close = qc.pyqtSignal()
796 pile_has_changed_signal = qc.pyqtSignal()
797 tracks_range_changed = qc.pyqtSignal(int, int, int)
798 frequency_filter_changed = qc.pyqtSignal(float, float)
800 begin_markers_add = qc.pyqtSignal(int, int)
801 end_markers_add = qc.pyqtSignal()
802 begin_markers_remove = qc.pyqtSignal(int, int)
803 end_markers_remove = qc.pyqtSignal()
805 marker_selection_changed = qc.pyqtSignal(list)
806 active_event_marker_changed = qc.pyqtSignal()
808 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
809 menu=None):
810 base.__init__(self, *args)
812 self.pile = pile
813 self.ax_height = 80
814 self.panel_parent = panel_parent
816 self.click_tolerance = 5
818 self.ntracks_shown_max = ntracks_shown_max
819 self.initial_ntracks_shown_max = ntracks_shown_max
820 self.ntracks = 0
821 self.show_all = True
822 self.shown_tracks_range = None
823 self.track_start = None
824 self.track_trange = None
826 self.lowpass = None
827 self.highpass = None
828 self.gain = 1.0
829 self.rotate = 0.0
830 self.picking_down = None
831 self.picking = None
832 self.floating_marker = None
833 self.markers = pyrocko.pile.Sorted([], 'tmin')
834 self.markers_deltat_max = 0.
835 self.n_selected_markers = 0
836 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
837 self.visible_marker_kinds = self.all_marker_kinds
838 self.active_event_marker = None
839 self.ignore_releases = 0
840 self.reloaded = False
841 self.pile_has_changed = False
842 self.config = pyrocko.config.config('snuffler')
844 self.tax = TimeAx()
845 self.setBackgroundRole(qg.QPalette.Base)
846 self.setAutoFillBackground(True)
847 poli = qw.QSizePolicy(
848 qw.QSizePolicy.Expanding,
849 qw.QSizePolicy.Expanding)
851 self.setSizePolicy(poli)
852 self.setMinimumSize(300, 200)
853 self.setFocusPolicy(qc.Qt.ClickFocus)
855 self.menu = menu
857 file_menu = self.menu.addMenu('&File')
858 view_menu = self.menu.addMenu('&View')
859 options_menu = self.menu.addMenu('&Options')
860 scale_menu = self.menu.addMenu('&Scaling')
861 sort_menu = self.menu.addMenu('Sor&ting')
862 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
864 help_menu = self.menu.addMenu('&Help')
866 self.snufflings_menu = self.toggle_panel_menu.addMenu(
867 'Run Snuffling')
868 self.toggle_panel_menu.addSeparator()
869 self.snuffling_help = help_menu.addMenu('Snuffling Help')
870 help_menu.addSeparator()
872 file_menu.addAction(
873 qg.QIcon.fromTheme('document-open'),
874 'Open waveform files...',
875 self.open_waveforms,
876 qg.QKeySequence.Open)
878 file_menu.addAction(
879 qg.QIcon.fromTheme('document-open'),
880 'Open waveform directory...',
881 self.open_waveform_directory)
883 file_menu.addAction(
884 'Open station files...',
885 self.open_stations)
887 file_menu.addAction(
888 'Open StationXML files...',
889 self.open_stations_xml)
891 file_menu.addAction(
892 'Open event file...',
893 self.read_events)
895 file_menu.addSeparator()
896 file_menu.addAction(
897 'Open marker file...',
898 self.read_markers)
900 file_menu.addAction(
901 qg.QIcon.fromTheme('document-save'),
902 'Save markers...',
903 self.write_markers,
904 qg.QKeySequence.Save)
906 file_menu.addAction(
907 qg.QIcon.fromTheme('document-save-as'),
908 'Save selected markers...',
909 self.write_selected_markers,
910 qg.QKeySequence.SaveAs)
912 file_menu.addSeparator()
913 file_menu.addAction(
914 qg.QIcon.fromTheme('document-print'),
915 'Print',
916 self.printit,
917 qg.QKeySequence.Print)
919 file_menu.addAction(
920 qg.QIcon.fromTheme('insert-image'),
921 'Save as SVG or PNG',
922 self.savesvg,
923 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
925 file_menu.addSeparator()
926 close = file_menu.addAction(
927 qg.QIcon.fromTheme('window-close'),
928 'Close',
929 self.myclose)
930 close.setShortcuts(
931 (qg.QKeySequence(qc.Qt.Key_Q),
932 qg.QKeySequence(qc.Qt.Key_X)))
934 # Scale Menu
935 menudef = [
936 ('Individual Scale',
937 lambda tr: tr.nslc_id,
938 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
939 ('Common Scale',
940 lambda tr: None,
941 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
942 ('Common Scale per Station',
943 lambda tr: (tr.network, tr.station),
944 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
945 ('Common Scale per Station Location',
946 lambda tr: (tr.network, tr.station, tr.location)),
947 ('Common Scale per Component',
948 lambda tr: (tr.channel)),
949 ]
951 self.menuitems_scaling = add_radiobuttongroup(
952 scale_menu, menudef, self.scalingmode_change,
953 default=self.config.trace_scale)
954 scale_menu.addSeparator()
956 self.scaling_key = self.menuitems_scaling[0][1]
957 self.scaling_hooks = {}
958 self.scalingmode_change()
960 menudef = [
961 ('Scaling based on Minimum and Maximum',
962 ('minmax', 'minmax')),
963 ('Scaling based on Minimum and Maximum (Robust)',
964 ('minmax', 'robust')),
965 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
966 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
967 ]
969 self.menuitems_scaling_base = add_radiobuttongroup(
970 scale_menu, menudef, self.scaling_base_change)
972 self.scaling_base = self.menuitems_scaling_base[0][1]
973 scale_menu.addSeparator()
975 self.menuitem_fixscalerange = scale_menu.addAction(
976 'Fix Scale Ranges')
977 self.menuitem_fixscalerange.setCheckable(True)
979 # Sort Menu
980 def sector_dist(sta):
981 if sta.dist_m is None:
982 return None, None
983 else:
984 return (
985 sector_int(round((sta.azimuth+15.)/30.)),
986 m_float(sta.dist_m))
988 menudef = [
989 ('Sort by Names',
990 lambda tr: (),
991 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
992 ('Sort by Distance',
993 lambda tr: self.station_attrib(
994 tr,
995 lambda sta: (m_float_or_none(sta.dist_m),),
996 lambda tr: (None,)),
997 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
998 ('Sort by Azimuth',
999 lambda tr: self.station_attrib(
1000 tr,
1001 lambda sta: (deg_float_or_none(sta.azimuth),),
1002 lambda tr: (None,))),
1003 ('Sort by Distance in 12 Azimuthal Blocks',
1004 lambda tr: self.station_attrib(
1005 tr,
1006 sector_dist,
1007 lambda tr: (None, None))),
1008 ('Sort by Backazimuth',
1009 lambda tr: self.station_attrib(
1010 tr,
1011 lambda sta: (deg_float_or_none(sta.backazimuth),),
1012 lambda tr: (None,))),
1013 ]
1014 self.menuitems_ssorting = add_radiobuttongroup(
1015 sort_menu, menudef, self.s_sortingmode_change)
1016 sort_menu.addSeparator()
1018 self._ssort = lambda tr: ()
1020 self.menu.addSeparator()
1022 menudef = [
1023 ('Subsort by Network, Station, Location, Channel',
1024 ((0, 1, 2, 3), # gathering
1025 lambda tr: tr.location)), # coloring
1026 ('Subsort by Network, Station, Channel, Location',
1027 ((0, 1, 3, 2),
1028 lambda tr: tr.channel)),
1029 ('Subsort by Station, Network, Channel, Location',
1030 ((1, 0, 3, 2),
1031 lambda tr: tr.channel)),
1032 ('Subsort by Location, Network, Station, Channel',
1033 ((2, 0, 1, 3),
1034 lambda tr: tr.channel)),
1035 ('Subsort by Channel, Network, Station, Location',
1036 ((3, 0, 1, 2),
1037 lambda tr: (tr.network, tr.station, tr.location))),
1038 ('Subsort by Network, Station, Channel (Grouped by Location)',
1039 ((0, 1, 3),
1040 lambda tr: tr.location)),
1041 ('Subsort by Station, Network, Channel (Grouped by Location)',
1042 ((1, 0, 3),
1043 lambda tr: tr.location)),
1044 ]
1046 self.menuitems_sorting = add_radiobuttongroup(
1047 sort_menu, menudef, self.sortingmode_change)
1049 menudef = [(x.key, x.value) for x in
1050 self.config.visible_length_setting]
1052 # View menu
1053 self.menuitems_visible_length = add_radiobuttongroup(
1054 view_menu, menudef,
1055 self.visible_length_change)
1056 view_menu.addSeparator()
1058 view_modes = [
1059 ('Wiggle Plot', ViewMode.Wiggle),
1060 ('Waterfall', ViewMode.Waterfall)
1061 ]
1063 self.menuitems_viewmode = add_radiobuttongroup(
1064 view_menu, view_modes,
1065 self.viewmode_change, default=ViewMode.Wiggle)
1066 view_menu.addSeparator()
1068 self.menuitem_cliptraces = view_menu.addAction(
1069 'Clip Traces')
1070 self.menuitem_cliptraces.setCheckable(True)
1071 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1073 self.menuitem_showboxes = view_menu.addAction(
1074 'Show Boxes')
1075 self.menuitem_showboxes.setCheckable(True)
1076 self.menuitem_showboxes.setChecked(
1077 self.config.show_boxes)
1079 self.menuitem_colortraces = view_menu.addAction(
1080 'Color Traces')
1081 self.menuitem_colortraces.setCheckable(True)
1082 self.menuitem_antialias = view_menu.addAction(
1083 'Antialiasing')
1084 self.menuitem_antialias.setCheckable(True)
1086 view_menu.addSeparator()
1087 self.menuitem_showscalerange = view_menu.addAction(
1088 'Show Scale Ranges')
1089 self.menuitem_showscalerange.setCheckable(True)
1090 self.menuitem_showscalerange.setChecked(
1091 self.config.show_scale_ranges)
1093 self.menuitem_showscaleaxis = view_menu.addAction(
1094 'Show Scale Axes')
1095 self.menuitem_showscaleaxis.setCheckable(True)
1096 self.menuitem_showscaleaxis.setChecked(
1097 self.config.show_scale_axes)
1099 self.menuitem_showzeroline = view_menu.addAction(
1100 'Show Zero Lines')
1101 self.menuitem_showzeroline.setCheckable(True)
1103 view_menu.addSeparator()
1104 view_menu.addAction(
1105 qg.QIcon.fromTheme('view-fullscreen'),
1106 'Fullscreen',
1107 self.toggle_fullscreen,
1108 qg.QKeySequence(qc.Qt.Key_F11))
1110 # Options Menu
1111 self.menuitem_demean = options_menu.addAction('Demean')
1112 self.menuitem_demean.setCheckable(True)
1113 self.menuitem_demean.setChecked(self.config.demean)
1114 self.menuitem_demean.setShortcut(
1115 qg.QKeySequence(qc.Qt.Key_Underscore))
1117 self.menuitem_distances_3d = options_menu.addAction(
1118 '3D distances',
1119 self.distances_3d_changed)
1120 self.menuitem_distances_3d.setCheckable(True)
1122 self.menuitem_allowdownsampling = options_menu.addAction(
1123 'Allow Downsampling')
1124 self.menuitem_allowdownsampling.setCheckable(True)
1125 self.menuitem_allowdownsampling.setChecked(True)
1127 self.menuitem_degap = options_menu.addAction(
1128 'Allow Degapping')
1129 self.menuitem_degap.setCheckable(True)
1130 self.menuitem_degap.setChecked(True)
1132 options_menu.addSeparator()
1134 self.menuitem_fft_filtering = options_menu.addAction(
1135 'FFT Filtering')
1136 self.menuitem_fft_filtering.setCheckable(True)
1138 self.menuitem_lphp = options_menu.addAction(
1139 'Bandpass is Low- + Highpass')
1140 self.menuitem_lphp.setCheckable(True)
1141 self.menuitem_lphp.setChecked(True)
1143 options_menu.addSeparator()
1144 self.menuitem_watch = options_menu.addAction(
1145 'Watch Files')
1146 self.menuitem_watch.setCheckable(True)
1148 self.menuitem_liberal_fetch = options_menu.addAction(
1149 'Liberal Fetch Optimization')
1150 self.menuitem_liberal_fetch.setCheckable(True)
1152 self.visible_length = menudef[0][1]
1154 self.snufflings_menu.addAction(
1155 'Reload Snufflings',
1156 self.setup_snufflings)
1158 # Disable ShadowPileTest
1159 if False:
1160 test_action = self.menu.addAction(
1161 'Test',
1162 self.toggletest)
1163 test_action.setCheckable(True)
1165 help_menu.addAction(
1166 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1167 'Snuffler Controls',
1168 self.help,
1169 qg.QKeySequence(qc.Qt.Key_Question))
1171 help_menu.addAction(
1172 'About',
1173 self.about)
1175 toolbar = qw.QFrame(self.menu)
1176 toolbar_layout = qw.QHBoxLayout()
1177 toolbar_layout.setContentsMargins(1, 1, 1, 1)
1178 toolbar.setLayout(toolbar_layout)
1180 def tracks_plus(*args):
1181 self.zoom_tracks(0., 1.)
1183 button = PileViewerMenuBarButton('+')
1184 button.clicked.connect(tracks_plus)
1185 button.setToolTip('Show more traces.')
1186 toolbar_layout.addWidget(button)
1188 def tracks_minus(*args):
1189 self.zoom_tracks(0., -1.)
1191 button = PileViewerMenuBarButton('-')
1192 button.clicked.connect(tracks_minus)
1193 button.setToolTip('Show fewer traces.')
1194 toolbar_layout.addWidget(button)
1196 def toggle_input(*args):
1197 self.toggle_input.emit()
1199 button = PileViewerMenuBarButton(':')
1200 button.setToolTip('Show command line.')
1201 button.clicked.connect(toggle_input)
1202 toolbar_layout.addWidget(button)
1204 self.menu.setCornerWidget(toolbar)
1206 self.time_projection = Projection()
1207 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1208 self.time_projection.set_out_range(0., self.width())
1210 self.gather = None
1212 self.trace_filter = None
1213 self.quick_filter = None
1214 self.quick_filter_patterns = None, None
1215 self.blacklist = []
1217 self.track_to_screen = Projection()
1218 self.track_to_nslc_ids = {}
1220 self.cached_vec = None
1221 self.cached_processed_traces = None
1223 self.timer = qc.QTimer(self)
1224 self.timer.timeout.connect(self.periodical)
1225 self.timer.setInterval(1000)
1226 self.timer.start()
1228 self._pile_changed = self.pile_changed # need to keep a strong ref
1229 self.pile.add_listener(self._pile_changed)
1231 self.trace_styles = {}
1232 if self.get_squirrel() is None:
1233 self.determine_box_styles()
1235 self.setMouseTracking(True)
1237 user_home_dir = os.path.expanduser('~')
1238 self.snuffling_modules = {}
1239 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1240 self.default_snufflings = None
1241 self.snufflings = []
1243 self.stations = {}
1245 self.timer_draw = Timer()
1246 self.timer_cutout = Timer()
1247 self.time_spent_painting = 0.0
1248 self.time_last_painted = time.time()
1250 self.interactive_range_change_time = 0.0
1251 self.interactive_range_change_delay_time = 10.0
1252 self.follow_timer = None
1254 self.sortingmode_change_time = 0.0
1255 self.sortingmode_change_delay_time = None
1257 self.old_data_ranges = {}
1259 self.return_tag = None
1260 self.wheel_pos = 60
1262 self.setAcceptDrops(True)
1263 self._paths_to_load = []
1265 self.tf_cache = {}
1267 self.waterfall = TraceWaterfall()
1268 self.waterfall_cmap = 'viridis'
1269 self.waterfall_clip_min = 0.
1270 self.waterfall_clip_max = 1.
1271 self.waterfall_show_absolute = False
1272 self.waterfall_integrate = False
1273 self.view_mode = ViewMode.Wiggle
1275 self.automatic_updates = True
1277 self.closing = False
1278 self.in_paint_event = False
1280 def fail(self, reason):
1281 box = qw.QMessageBox(self)
1282 box.setText(reason)
1283 box.exec_()
1285 def set_trace_filter(self, filter_func):
1286 self.trace_filter = filter_func
1287 self.sortingmode_change()
1289 def update_trace_filter(self):
1290 if self.blacklist:
1292 def blacklist_func(tr):
1293 return not pyrocko.util.match_nslc(
1294 self.blacklist, tr.nslc_id)
1296 else:
1297 blacklist_func = None
1299 if self.quick_filter is None and blacklist_func is None:
1300 self.set_trace_filter(None)
1301 elif self.quick_filter is None:
1302 self.set_trace_filter(blacklist_func)
1303 elif blacklist_func is None:
1304 self.set_trace_filter(self.quick_filter)
1305 else:
1306 self.set_trace_filter(
1307 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1309 def set_quick_filter(self, filter_func):
1310 self.quick_filter = filter_func
1311 self.update_trace_filter()
1313 def set_quick_filter_patterns(self, patterns, inputline=None):
1314 if patterns is not None:
1315 self.set_quick_filter(
1316 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1317 else:
1318 self.set_quick_filter(None)
1320 self.quick_filter_patterns = patterns, inputline
1322 def get_quick_filter_patterns(self):
1323 return self.quick_filter_patterns
1325 def add_blacklist_pattern(self, pattern):
1326 if pattern == 'empty':
1327 keys = set(self.pile.nslc_ids)
1328 trs = self.pile.all(
1329 tmin=self.tmin,
1330 tmax=self.tmax,
1331 load_data=False,
1332 degap=False)
1334 for tr in trs:
1335 if tr.nslc_id in keys:
1336 keys.remove(tr.nslc_id)
1338 for key in keys:
1339 xpattern = '.'.join(key)
1340 if xpattern not in self.blacklist:
1341 self.blacklist.append(xpattern)
1343 else:
1344 if pattern in self.blacklist:
1345 self.blacklist.remove(pattern)
1347 self.blacklist.append(pattern)
1349 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1350 self.update_trace_filter()
1352 def remove_blacklist_pattern(self, pattern):
1353 if pattern in self.blacklist:
1354 self.blacklist.remove(pattern)
1355 else:
1356 raise PileViewerMainException(
1357 'Pattern not found in blacklist.')
1359 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1360 self.update_trace_filter()
1362 def clear_blacklist(self):
1363 self.blacklist = []
1364 self.update_trace_filter()
1366 def ssort(self, tr):
1367 return self._ssort(tr)
1369 def station_key(self, x):
1370 return x.network, x.station
1372 def station_keys(self, x):
1373 return [
1374 (x.network, x.station, x.location),
1375 (x.network, x.station)]
1377 def station_attrib(self, tr, getter, default_getter):
1378 for sk in self.station_keys(tr):
1379 if sk in self.stations:
1380 station = self.stations[sk]
1381 return getter(station)
1383 return default_getter(tr)
1385 def get_station(self, sk):
1386 return self.stations[sk]
1388 def has_station(self, station):
1389 for sk in self.station_keys(station):
1390 if sk in self.stations:
1391 return True
1393 return False
1395 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1396 return self.station_attrib(
1397 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1399 def set_stations(self, stations):
1400 self.stations = {}
1401 self.add_stations(stations)
1403 def add_stations(self, stations):
1404 for station in stations:
1405 for sk in self.station_keys(station):
1406 self.stations[sk] = station
1408 ev = self.get_active_event()
1409 if ev:
1410 self.set_origin(ev)
1412 def add_event(self, event):
1413 marker = EventMarker(event)
1414 self.add_marker(marker)
1416 def add_events(self, events):
1417 markers = [EventMarker(e) for e in events]
1418 self.add_markers(markers)
1420 def set_event_marker_as_origin(self, ignore=None):
1421 selected = self.selected_markers()
1422 if not selected:
1423 self.fail('An event marker must be selected.')
1424 return
1426 m = selected[0]
1427 if not isinstance(m, EventMarker):
1428 self.fail('Selected marker is not an event.')
1429 return
1431 self.set_active_event_marker(m)
1433 def deactivate_event_marker(self):
1434 if self.active_event_marker:
1435 self.active_event_marker.active = False
1437 self.active_event_marker_changed.emit()
1438 self.active_event_marker = None
1440 def set_active_event_marker(self, event_marker):
1441 if self.active_event_marker:
1442 self.active_event_marker.active = False
1444 self.active_event_marker = event_marker
1445 event_marker.active = True
1446 event = event_marker.get_event()
1447 self.set_origin(event)
1448 self.active_event_marker_changed.emit()
1450 def set_active_event(self, event):
1451 for marker in self.markers:
1452 if isinstance(marker, EventMarker):
1453 if marker.get_event() is event:
1454 self.set_active_event_marker(marker)
1456 def get_active_event_marker(self):
1457 return self.active_event_marker
1459 def get_active_event(self):
1460 m = self.get_active_event_marker()
1461 if m is not None:
1462 return m.get_event()
1463 else:
1464 return None
1466 def get_active_markers(self):
1467 emarker = self.get_active_event_marker()
1468 if emarker is None:
1469 return None, []
1471 else:
1472 ev = emarker.get_event()
1473 pmarkers = [
1474 m for m in self.markers
1475 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1477 return emarker, pmarkers
1479 def set_origin(self, location):
1480 for station in self.stations.values():
1481 station.set_event_relative_data(
1482 location,
1483 distance_3d=self.menuitem_distances_3d.isChecked())
1485 self.sortingmode_change()
1487 def distances_3d_changed(self):
1488 ignore = self.menuitem_distances_3d.isChecked()
1489 self.set_event_marker_as_origin(ignore)
1491 def iter_snuffling_modules(self):
1492 pjoin = os.path.join
1493 for path in self.snuffling_paths:
1495 if not os.path.isdir(path):
1496 os.mkdir(path)
1498 for entry in os.listdir(path):
1499 directory = path
1500 fn = entry
1501 d = pjoin(path, entry)
1502 if os.path.isdir(d):
1503 directory = d
1504 if os.path.isfile(
1505 os.path.join(directory, 'snuffling.py')):
1506 fn = 'snuffling.py'
1508 if not fn.endswith('.py'):
1509 continue
1511 name = fn[:-3]
1513 if (directory, name) not in self.snuffling_modules:
1514 self.snuffling_modules[directory, name] = \
1515 pyrocko.gui.snuffler.snuffling.SnufflingModule(
1516 directory, name, self)
1518 yield self.snuffling_modules[directory, name]
1520 def setup_snufflings(self):
1521 # user snufflings
1522 from pyrocko.gui.snuffler.snuffling import BrokenSnufflingModule
1523 for mod in self.iter_snuffling_modules():
1524 try:
1525 mod.load_if_needed()
1526 except BrokenSnufflingModule as e:
1527 logger.warning('Snuffling module "%s" is broken' % e)
1529 # load the default snufflings on first run
1530 if self.default_snufflings is None:
1531 self.default_snufflings = pyrocko.gui.snuffler\
1532 .snufflings.__snufflings__()
1533 for snuffling in self.default_snufflings:
1534 self.add_snuffling(snuffling)
1536 def set_panel_parent(self, panel_parent):
1537 self.panel_parent = panel_parent
1539 def get_panel_parent(self):
1540 return self.panel_parent
1542 def add_snuffling(self, snuffling, reloaded=False):
1543 logger.debug('Adding snuffling %s' % snuffling.get_name())
1544 snuffling.init_gui(
1545 self, self.get_panel_parent(), self, reloaded=reloaded)
1546 self.snufflings.append(snuffling)
1547 self.update()
1549 def remove_snuffling(self, snuffling):
1550 snuffling.delete_gui()
1551 self.update()
1552 self.snufflings.remove(snuffling)
1553 snuffling.pre_destroy()
1555 def add_snuffling_menuitem(self, item):
1556 self.snufflings_menu.addAction(item)
1557 item.setParent(self.snufflings_menu)
1558 sort_actions(self.snufflings_menu)
1560 def remove_snuffling_menuitem(self, item):
1561 self.snufflings_menu.removeAction(item)
1563 def add_snuffling_help_menuitem(self, item):
1564 self.snuffling_help.addAction(item)
1565 item.setParent(self.snuffling_help)
1566 sort_actions(self.snuffling_help)
1568 def remove_snuffling_help_menuitem(self, item):
1569 self.snuffling_help.removeAction(item)
1571 def add_panel_toggler(self, item):
1572 self.toggle_panel_menu.addAction(item)
1573 item.setParent(self.toggle_panel_menu)
1574 sort_actions(self.toggle_panel_menu)
1576 def remove_panel_toggler(self, item):
1577 self.toggle_panel_menu.removeAction(item)
1579 def load(self, paths, regex=None, format='detect',
1580 cache_dir=None, force_cache=False):
1582 if cache_dir is None:
1583 cache_dir = pyrocko.config.config().cache_dir
1584 if isinstance(paths, str):
1585 paths = [paths]
1587 fns = pyrocko.util.select_files(
1588 paths, selector=None, include=regex, show_progress=False)
1590 if not fns:
1591 return
1593 cache = pyrocko.pile.get_cache(cache_dir)
1595 t = [time.time()]
1597 def update_bar(label, value):
1598 pbs = self.parent().get_progressbars()
1599 if label.lower() == 'looking at files':
1600 label = 'Looking at %i files' % len(fns)
1601 else:
1602 label = 'Scanning %i files' % len(fns)
1604 return pbs.set_status(label, value)
1606 def update_progress(label, i, n):
1607 abort = False
1609 qw.qApp.processEvents()
1610 if n != 0:
1611 perc = i*100/n
1612 else:
1613 perc = 100
1614 abort |= update_bar(label, perc)
1615 abort |= self.window().is_closing()
1617 tnow = time.time()
1618 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1619 self.update()
1620 t[0] = tnow
1622 return abort
1624 self.automatic_updates = False
1626 self.pile.load_files(
1627 sorted(fns),
1628 filename_attributes=regex,
1629 cache=cache,
1630 fileformat=format,
1631 show_progress=False,
1632 update_progress=update_progress)
1634 self.automatic_updates = True
1635 self.update()
1637 def load_queued(self):
1638 if not self._paths_to_load:
1639 return
1640 paths = self._paths_to_load
1641 self._paths_to_load = []
1642 self.load(paths)
1644 def load_soon(self, paths):
1645 self._paths_to_load.extend(paths)
1646 qc.QTimer.singleShot(200, self.load_queued)
1648 def open_waveforms(self):
1649 caption = 'Select one or more files to open'
1651 fns, _ = qw.QFileDialog.getOpenFileNames(
1652 self, caption, options=qfiledialog_options)
1654 if fns:
1655 self.load(list(str(fn) for fn in fns))
1657 def open_waveform_directory(self):
1658 caption = 'Select directory to scan for waveform files'
1660 dn = qw.QFileDialog.getExistingDirectory(
1661 self, caption, options=qfiledialog_options)
1663 if dn:
1664 self.load([str(dn)])
1666 def open_stations(self, fns=None):
1667 caption = 'Select one or more Pyrocko station files to open'
1669 if not fns:
1670 fns, _ = qw.QFileDialog.getOpenFileNames(
1671 self, caption, options=qfiledialog_options)
1673 try:
1674 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1675 for stat in stations:
1676 self.add_stations(stat)
1678 except Exception as e:
1679 self.fail('Failed to read station file: %s' % str(e))
1681 def open_stations_xml(self, fns=None):
1682 from pyrocko.io import stationxml
1684 caption = 'Select one or more StationXML files'
1685 if not fns:
1686 fns, _ = qw.QFileDialog.getOpenFileNames(
1687 self, caption, options=qfiledialog_options,
1688 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1689 ';;All files (*)')
1691 try:
1692 stations = [
1693 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1694 for x in fns]
1696 for stat in stations:
1697 self.add_stations(stat)
1699 except Exception as e:
1700 self.fail('Failed to read StationXML file: %s' % str(e))
1702 def add_traces(self, traces):
1703 if traces:
1704 mtf = pyrocko.pile.MemTracesFile(None, traces)
1705 self.pile.add_file(mtf)
1706 ticket = (self.pile, mtf)
1707 return ticket
1708 else:
1709 return (None, None)
1711 def release_data(self, tickets):
1712 for ticket in tickets:
1713 pile, mtf = ticket
1714 if pile is not None:
1715 pile.remove_file(mtf)
1717 def periodical(self):
1718 if self.menuitem_watch.isChecked():
1719 if self.pile.reload_modified():
1720 self.update()
1722 def get_pile(self):
1723 return self.pile
1725 def pile_changed(self, what, content):
1726 self.pile_has_changed = True
1727 self.pile_has_changed_signal.emit()
1728 if self.automatic_updates:
1729 self.update()
1731 def set_gathering(self, gather=None, color=None):
1733 if gather is None:
1734 def gather_func(tr):
1735 return tr.nslc_id
1737 gather = (0, 1, 2, 3)
1739 else:
1740 def gather_func(tr):
1741 return (
1742 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1744 if color is None:
1745 def color(tr):
1746 return tr.location
1748 self.gather = gather_func
1749 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1751 self.color_gather = color
1752 self.color_keys = self.pile.gather_keys(color)
1753 previous_ntracks = self.ntracks
1754 self.set_ntracks(len(keys))
1756 if self.shown_tracks_range is None or \
1757 previous_ntracks == 0 or \
1758 self.show_all:
1760 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1761 key_at_top = None
1762 n = high-low
1764 else:
1765 low, high = self.shown_tracks_range
1766 key_at_top = self.track_keys[low]
1767 n = high-low
1769 self.track_keys = sorted(keys)
1771 track_patterns = []
1772 for k in self.track_keys:
1773 pat = ['*', '*', '*', '*']
1774 for i, j in enumerate(gather):
1775 pat[j] = k[-len(gather)+i]
1777 track_patterns.append(pat)
1779 self.track_patterns = track_patterns
1781 if key_at_top is not None:
1782 try:
1783 ind = self.track_keys.index(key_at_top)
1784 low = ind
1785 high = low+n
1786 except Exception:
1787 pass
1789 self.set_tracks_range((low, high))
1791 self.key_to_row = dict(
1792 [(key, i) for (i, key) in enumerate(self.track_keys)])
1794 def inrange(x, r):
1795 return r[0] <= x and x < r[1]
1797 def trace_selector(trace):
1798 gt = self.gather(trace)
1799 return (
1800 gt in self.key_to_row and
1801 inrange(self.key_to_row[gt], self.shown_tracks_range))
1803 self.trace_selector = lambda x: \
1804 (self.trace_filter is None or self.trace_filter(x)) \
1805 and trace_selector(x)
1807 if self.tmin == working_system_time_range[0] and \
1808 self.tmax == working_system_time_range[1] or \
1809 self.show_all:
1811 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1812 if tmin is not None and tmax is not None:
1813 tlen = (tmax - tmin)
1814 tpad = tlen * 5./self.width()
1815 self.set_time_range(tmin-tpad, tmax+tpad)
1817 def set_time_range(self, tmin, tmax):
1818 if tmin is None:
1819 tmin = initial_time_range[0]
1821 if tmax is None:
1822 tmax = initial_time_range[1]
1824 if tmin > tmax:
1825 tmin, tmax = tmax, tmin
1827 if tmin == tmax:
1828 tmin -= 1.
1829 tmax += 1.
1831 tmin = max(working_system_time_range[0], tmin)
1832 tmax = min(working_system_time_range[1], tmax)
1834 min_deltat = self.content_deltat_range()[0]
1835 if (tmax - tmin < min_deltat):
1836 m = (tmin + tmax) / 2.
1837 tmin = m - min_deltat/2.
1838 tmax = m + min_deltat/2.
1840 self.time_projection.set_in_range(tmin, tmax)
1841 self.tmin, self.tmax = tmin, tmax
1843 def get_time_range(self):
1844 return self.tmin, self.tmax
1846 def ypart(self, y):
1847 if y < self.ax_height:
1848 return -1
1849 elif y > self.height()-self.ax_height:
1850 return 1
1851 else:
1852 return 0
1854 def time_fractional_digits(self):
1855 min_deltat = self.content_deltat_range()[0]
1856 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1858 def write_markers(self, fn=None):
1859 caption = 'Choose a file name to write markers'
1860 if not fn:
1861 fn, _ = qw.QFileDialog.getSaveFileName(
1862 self, caption, options=qfiledialog_options)
1863 if fn:
1864 try:
1865 Marker.save_markers(
1866 self.markers, fn,
1867 fdigits=self.time_fractional_digits())
1869 except Exception as e:
1870 self.fail('Failed to write marker file: %s' % str(e))
1872 def write_selected_markers(self, fn=None):
1873 caption = 'Choose a file name to write selected markers'
1874 if not fn:
1875 fn, _ = qw.QFileDialog.getSaveFileName(
1876 self, caption, options=qfiledialog_options)
1877 if fn:
1878 try:
1879 Marker.save_markers(
1880 self.iter_selected_markers(),
1881 fn,
1882 fdigits=self.time_fractional_digits())
1884 except Exception as e:
1885 self.fail('Failed to write marker file: %s' % str(e))
1887 def read_events(self, fn=None):
1888 '''
1889 Open QFileDialog to open, read and add
1890 :py:class:`pyrocko.model.Event` instances and their marker
1891 representation to the pile viewer.
1892 '''
1893 caption = 'Selet one or more files to open'
1894 if not fn:
1895 fn, _ = qw.QFileDialog.getOpenFileName(
1896 self, caption, options=qfiledialog_options)
1897 if fn:
1898 try:
1899 self.add_events(pyrocko.model.load_events(fn))
1900 self.associate_phases_to_events()
1902 except Exception as e:
1903 self.fail('Failed to read event file: %s' % str(e))
1905 def read_markers(self, fn=None):
1906 '''
1907 Open QFileDialog to open, read and add markers to the pile viewer.
1908 '''
1909 caption = 'Selet one or more marker files to open'
1910 if not fn:
1911 fn, _ = qw.QFileDialog.getOpenFileName(
1912 self, caption, options=qfiledialog_options)
1913 if fn:
1914 try:
1915 self.add_markers(Marker.load_markers(fn))
1916 self.associate_phases_to_events()
1918 except Exception as e:
1919 self.fail('Failed to read marker file: %s' % str(e))
1921 def associate_phases_to_events(self):
1922 associate_phases_to_events(self.markers)
1924 def add_marker(self, marker):
1925 # need index to inform QAbstactTableModel about upcoming change,
1926 # but have to restore current state in order to not cause problems
1927 self.markers.insert(marker)
1928 i = self.markers.remove(marker)
1930 self.begin_markers_add.emit(i, i)
1931 self.markers.insert(marker)
1932 self.end_markers_add.emit()
1933 self.markers_deltat_max = max(
1934 self.markers_deltat_max, marker.tmax - marker.tmin)
1936 def add_markers(self, markers):
1937 if not self.markers:
1938 self.begin_markers_add.emit(0, len(markers) - 1)
1939 self.markers.insert_many(markers)
1940 self.end_markers_add.emit()
1941 self.update_markers_deltat_max()
1942 else:
1943 for marker in markers:
1944 self.add_marker(marker)
1946 def update_markers_deltat_max(self):
1947 if self.markers:
1948 self.markers_deltat_max = max(
1949 marker.tmax - marker.tmin for marker in self.markers)
1951 def remove_marker(self, marker):
1952 '''
1953 Remove a ``marker`` from the :py:class:`PileViewer`.
1955 :param marker:
1956 :py:class:`~pyrocko.gui.snuffler.marker.Marker` (or subclass)
1957 instance
1958 '''
1960 if marker is self.active_event_marker:
1961 self.deactivate_event_marker()
1963 try:
1964 i = self.markers.index(marker)
1965 self.begin_markers_remove.emit(i, i)
1966 self.markers.remove_at(i)
1967 self.end_markers_remove.emit()
1968 except ValueError:
1969 pass
1971 def remove_markers(self, markers):
1972 '''
1973 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1975 :param markers:
1976 list of :py:class:`~pyrocko.gui.snuffler.marker.Marker` (or
1977 subclass) instances
1978 '''
1980 if markers is self.markers:
1981 markers = list(markers)
1983 for marker in markers:
1984 self.remove_marker(marker)
1986 self.update_markers_deltat_max()
1988 def remove_selected_markers(self):
1989 def delete_segment(istart, iend):
1990 self.begin_markers_remove.emit(istart, iend-1)
1991 for _ in range(iend - istart):
1992 self.markers.remove_at(istart)
1994 self.end_markers_remove.emit()
1996 istart = None
1997 ipos = 0
1998 markers = self.markers
1999 nmarkers = len(self.markers)
2000 while ipos < nmarkers:
2001 marker = markers[ipos]
2002 if marker.is_selected():
2003 if marker is self.active_event_marker:
2004 self.deactivate_event_marker()
2006 if istart is None:
2007 istart = ipos
2008 else:
2009 if istart is not None:
2010 delete_segment(istart, ipos)
2011 nmarkers -= ipos - istart
2012 ipos = istart - 1
2013 istart = None
2015 ipos += 1
2017 if istart is not None:
2018 delete_segment(istart, ipos)
2020 self.update_markers_deltat_max()
2022 def selected_markers(self):
2023 return [marker for marker in self.markers if marker.is_selected()]
2025 def iter_selected_markers(self):
2026 for marker in self.markers:
2027 if marker.is_selected():
2028 yield marker
2030 def get_markers(self):
2031 return self.markers
2033 def mousePressEvent(self, mouse_ev):
2034 ''
2035 self.show_all = False
2036 point = self.mapFromGlobal(mouse_ev.globalPos())
2038 if mouse_ev.button() == qc.Qt.LeftButton:
2039 marker = self.marker_under_cursor(point.x(), point.y())
2040 if self.picking:
2041 if self.picking_down is None:
2042 self.picking_down = (
2043 self.time_projection.rev(mouse_ev.x()),
2044 mouse_ev.y())
2046 elif marker is not None:
2047 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
2048 self.deselect_all()
2049 marker.selected = True
2050 self.emit_selected_markers()
2051 self.update()
2052 else:
2053 self.track_start = mouse_ev.x(), mouse_ev.y()
2054 self.track_trange = self.tmin, self.tmax
2056 if mouse_ev.button() == qc.Qt.RightButton \
2057 and isinstance(self.menu, qw.QMenu):
2058 self.menu.exec_(qg.QCursor.pos())
2059 self.update_status()
2061 def mouseReleaseEvent(self, mouse_ev):
2062 ''
2063 if self.ignore_releases:
2064 self.ignore_releases -= 1
2065 return
2067 if self.picking:
2068 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2069 self.emit_selected_markers()
2071 if self.track_start:
2072 self.update()
2074 self.track_start = None
2075 self.track_trange = None
2076 self.update_status()
2078 def mouseDoubleClickEvent(self, mouse_ev):
2079 ''
2080 self.show_all = False
2081 self.start_picking(None)
2082 self.ignore_releases = 1
2084 def mouseMoveEvent(self, mouse_ev):
2085 ''
2086 point = self.mapFromGlobal(mouse_ev.globalPos())
2088 if self.picking:
2089 self.update_picking(point.x(), point.y())
2091 elif self.track_start is not None:
2092 x0, y0 = self.track_start
2093 dx = (point.x() - x0)/float(self.width())
2094 dy = (point.y() - y0)/float(self.height())
2095 if self.ypart(y0) == 1:
2096 dy = 0
2098 tmin0, tmax0 = self.track_trange
2100 scale = math.exp(-dy*5.)
2101 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2102 frac = x0/float(self.width())
2103 dt = dx*(tmax0-tmin0)*scale
2105 self.interrupt_following()
2106 self.set_time_range(
2107 tmin0 - dt - dtr*frac,
2108 tmax0 - dt + dtr*(1.-frac))
2110 self.update()
2111 else:
2112 self.hoovering(point.x(), point.y())
2114 self.update_status()
2116 def nslc_ids_under_cursor(self, x, y):
2117 ftrack = self.track_to_screen.rev(y)
2118 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2119 return nslc_ids
2121 def marker_under_cursor(self, x, y):
2122 mouset = self.time_projection.rev(x)
2123 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2124 relevant_nslc_ids = None
2125 for marker in self.markers:
2126 if marker.kind not in self.visible_marker_kinds:
2127 continue
2129 if (abs(mouset-marker.tmin) < deltat or
2130 abs(mouset-marker.tmax) < deltat):
2132 if relevant_nslc_ids is None:
2133 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2135 marker_nslc_ids = marker.get_nslc_ids()
2136 if not marker_nslc_ids:
2137 return marker
2139 for nslc_id in marker_nslc_ids:
2140 if nslc_id in relevant_nslc_ids:
2141 return marker
2143 def hoovering(self, x, y):
2144 mouset = self.time_projection.rev(x)
2145 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2146 needupdate = False
2147 haveone = False
2148 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2149 for marker in self.markers:
2150 if marker.kind not in self.visible_marker_kinds:
2151 continue
2153 state = abs(mouset-marker.tmin) < deltat or \
2154 abs(mouset-marker.tmax) < deltat and not haveone
2156 if state:
2157 xstate = False
2159 marker_nslc_ids = marker.get_nslc_ids()
2160 if not marker_nslc_ids:
2161 xstate = True
2163 for nslc in relevant_nslc_ids:
2164 if marker.match_nslc(nslc):
2165 xstate = True
2167 state = xstate
2169 if state:
2170 haveone = True
2171 oldstate = marker.is_alerted()
2172 if oldstate != state:
2173 needupdate = True
2174 marker.set_alerted(state)
2175 if state:
2176 self.window().status_messages.set(
2177 'marker', marker.hoover_message())
2179 if needupdate:
2180 self.update()
2182 self.update_status()
2184 def event(self, event):
2185 ''
2186 if event.type() == qc.QEvent.KeyPress:
2187 self.keyPressEvent(event)
2188 return True
2189 else:
2190 return base.event(self, event)
2192 def keyPressEvent(self, key_event):
2193 ''
2194 self.show_all = False
2195 dt = self.tmax - self.tmin
2196 tmid = (self.tmin + self.tmax) / 2.
2198 key = key_event.key()
2199 try:
2200 keytext = str(key_event.text())
2201 except UnicodeEncodeError:
2202 return
2204 if key == qc.Qt.Key_Space:
2205 self.interrupt_following()
2206 self.set_time_range(self.tmin+dt, self.tmax+dt)
2208 elif key == qc.Qt.Key_Up:
2209 for m in self.selected_markers():
2210 if isinstance(m, PhaseMarker):
2211 if key_event.modifiers() & qc.Qt.ShiftModifier:
2212 p = 0
2213 else:
2214 p = 1 if m.get_polarity() != 1 else None
2215 m.set_polarity(p)
2217 elif key == qc.Qt.Key_Down:
2218 for m in self.selected_markers():
2219 if isinstance(m, PhaseMarker):
2220 if key_event.modifiers() & qc.Qt.ShiftModifier:
2221 p = 0
2222 else:
2223 p = -1 if m.get_polarity() != -1 else None
2224 m.set_polarity(p)
2226 elif key == qc.Qt.Key_B:
2227 dt = self.tmax - self.tmin
2228 self.interrupt_following()
2229 self.set_time_range(self.tmin-dt, self.tmax-dt)
2231 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2232 self.interrupt_following()
2234 tgo = None
2236 class TraceDummy(object):
2237 def __init__(self, marker):
2238 self._marker = marker
2240 @property
2241 def nslc_id(self):
2242 return self._marker.one_nslc()
2244 def marker_to_itrack(marker):
2245 try:
2246 return self.key_to_row.get(
2247 self.gather(TraceDummy(marker)), -1)
2249 except MarkerOneNSLCRequired:
2250 return -1
2252 emarker, pmarkers = self.get_active_markers()
2253 pmarkers = [
2254 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2255 pmarkers.sort(key=lambda m: (
2256 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2258 if key == qc.Qt.Key_Backtab:
2259 pmarkers.reverse()
2261 smarkers = self.selected_markers()
2262 iselected = []
2263 for sm in smarkers:
2264 try:
2265 iselected.append(pmarkers.index(sm))
2266 except ValueError:
2267 pass
2269 if iselected:
2270 icurrent = max(iselected) + 1
2271 else:
2272 icurrent = 0
2274 if icurrent < len(pmarkers):
2275 self.deselect_all()
2276 cmarker = pmarkers[icurrent]
2277 cmarker.selected = True
2278 tgo = cmarker.tmin
2279 if not self.tmin < tgo < self.tmax:
2280 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2282 itrack = marker_to_itrack(cmarker)
2283 if itrack != -1:
2284 if itrack < self.shown_tracks_range[0]:
2285 self.scroll_tracks(
2286 - (self.shown_tracks_range[0] - itrack))
2287 elif self.shown_tracks_range[1] <= itrack:
2288 self.scroll_tracks(
2289 itrack - self.shown_tracks_range[1]+1)
2291 if itrack not in self.track_to_nslc_ids:
2292 self.go_to_selection()
2294 elif keytext in ('p', 'n', 'P', 'N'):
2295 smarkers = self.selected_markers()
2296 tgo = None
2297 dir = str(keytext)
2298 if smarkers:
2299 tmid = smarkers[0].tmin
2300 for smarker in smarkers:
2301 if dir == 'n':
2302 tmid = max(smarker.tmin, tmid)
2303 else:
2304 tmid = min(smarker.tmin, tmid)
2306 tgo = tmid
2308 if dir.lower() == 'n':
2309 for marker in sorted(
2310 self.markers,
2311 key=operator.attrgetter('tmin')):
2313 t = marker.tmin
2314 if t > tmid and \
2315 marker.kind in self.visible_marker_kinds and \
2316 (dir == 'n' or
2317 isinstance(marker, EventMarker)):
2319 self.deselect_all()
2320 marker.selected = True
2321 tgo = t
2322 break
2323 else:
2324 for marker in sorted(
2325 self.markers,
2326 key=operator.attrgetter('tmin'),
2327 reverse=True):
2329 t = marker.tmin
2330 if t < tmid and \
2331 marker.kind in self.visible_marker_kinds and \
2332 (dir == 'p' or
2333 isinstance(marker, EventMarker)):
2334 self.deselect_all()
2335 marker.selected = True
2336 tgo = t
2337 break
2339 if tgo is not None:
2340 self.interrupt_following()
2341 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2343 elif keytext == 'r':
2344 if self.pile.reload_modified():
2345 self.reloaded = True
2347 elif keytext == 'R':
2348 self.setup_snufflings()
2350 elif key == qc.Qt.Key_Backspace:
2351 self.remove_selected_markers()
2353 elif keytext == 'a':
2354 for marker in self.markers:
2355 if ((self.tmin <= marker.tmin <= self.tmax or
2356 self.tmin <= marker.tmax <= self.tmax) and
2357 marker.kind in self.visible_marker_kinds):
2358 marker.selected = True
2359 else:
2360 marker.selected = False
2362 elif keytext == 'A':
2363 for marker in self.markers:
2364 if marker.kind in self.visible_marker_kinds:
2365 marker.selected = True
2367 elif keytext == 'd':
2368 self.deselect_all()
2370 elif keytext == 'E':
2371 self.deactivate_event_marker()
2373 elif keytext == 'e':
2374 markers = self.selected_markers()
2375 event_markers_in_spe = [
2376 marker for marker in markers
2377 if not isinstance(marker, PhaseMarker)]
2379 phase_markers = [
2380 marker for marker in markers
2381 if isinstance(marker, PhaseMarker)]
2383 if len(event_markers_in_spe) == 1:
2384 event_marker = event_markers_in_spe[0]
2385 if not isinstance(event_marker, EventMarker):
2386 nslcs = list(event_marker.nslc_ids)
2387 lat, lon = 0.0, 0.0
2388 old = self.get_active_event()
2389 if len(nslcs) == 1:
2390 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2391 elif old is not None:
2392 lat, lon = old.lat, old.lon
2394 event_marker.convert_to_event_marker(lat, lon)
2396 self.set_active_event_marker(event_marker)
2397 event = event_marker.get_event()
2398 for marker in phase_markers:
2399 marker.set_event(event)
2401 else:
2402 for marker in event_markers_in_spe:
2403 marker.convert_to_event_marker()
2405 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2406 for marker in self.selected_markers():
2407 marker.set_kind(int(keytext))
2408 self.emit_selected_markers()
2410 elif key in fkey_map:
2411 self.handle_fkeys(key)
2413 elif key == qc.Qt.Key_Escape:
2414 if self.picking:
2415 self.stop_picking(0, 0, abort=True)
2417 elif key == qc.Qt.Key_PageDown:
2418 self.scroll_tracks(
2419 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2421 elif key == qc.Qt.Key_PageUp:
2422 self.scroll_tracks(
2423 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2425 elif key == qc.Qt.Key_Plus:
2426 self.zoom_tracks(0., 1.)
2428 elif key == qc.Qt.Key_Minus:
2429 self.zoom_tracks(0., -1.)
2431 elif key == qc.Qt.Key_Equal:
2432 ntracks_shown = self.shown_tracks_range[1] - \
2433 self.shown_tracks_range[0]
2434 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2435 self.zoom_tracks(0., dtracks)
2437 elif key == qc.Qt.Key_Colon:
2438 self.want_input.emit()
2440 elif keytext == 'f':
2441 self.toggle_fullscreen()
2443 elif keytext == 'g':
2444 self.go_to_selection()
2446 elif keytext == 'G':
2447 self.go_to_selection(tight=True)
2449 elif keytext == 'm':
2450 self.toggle_marker_editor()
2452 elif keytext == 'c':
2453 self.toggle_main_controls()
2455 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2456 dir = 1
2457 amount = 1
2458 if key_event.key() == qc.Qt.Key_Left:
2459 dir = -1
2460 if key_event.modifiers() & qc.Qt.ShiftModifier:
2461 amount = 10
2462 self.nudge_selected_markers(dir*amount)
2463 else:
2464 super().keyPressEvent(key_event)
2466 if keytext != '' and keytext in 'degaApPnN':
2467 self.emit_selected_markers()
2469 self.update()
2470 self.update_status()
2472 def handle_fkeys(self, key):
2473 self.set_phase_kind(
2474 self.selected_markers(),
2475 fkey_map[key] + 1)
2476 self.emit_selected_markers()
2478 def emit_selected_markers(self):
2479 ibounds = []
2480 last_selected = False
2481 for imarker, marker in enumerate(self.markers):
2482 this_selected = marker.is_selected()
2483 if this_selected != last_selected:
2484 ibounds.append(imarker)
2486 last_selected = this_selected
2488 if last_selected:
2489 ibounds.append(len(self.markers))
2491 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2492 self.n_selected_markers = sum(
2493 chunk[1] - chunk[0] for chunk in chunks)
2494 self.marker_selection_changed.emit(chunks)
2496 def toggle_marker_editor(self):
2497 self.panel_parent.toggle_marker_editor()
2499 def toggle_main_controls(self):
2500 self.panel_parent.toggle_main_controls()
2502 def nudge_selected_markers(self, npixels):
2503 a, b = self.time_projection.ur
2504 c, d = self.time_projection.xr
2505 for marker in self.selected_markers():
2506 if not isinstance(marker, EventMarker):
2507 marker.tmin += npixels * (d-c)/b
2508 marker.tmax += npixels * (d-c)/b
2510 def toggle_fullscreen(self):
2511 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2512 self.window().windowState() & qc.Qt.WindowMaximized:
2513 self.window().showNormal()
2514 else:
2515 if is_macos:
2516 self.window().showMaximized()
2517 else:
2518 self.window().showFullScreen()
2520 def about(self):
2521 fn = pyrocko.util.data_file('snuffler.png')
2522 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2523 txt = f.read()
2524 label = qw.QLabel(txt % {'logo': fn})
2525 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2526 self.show_doc('About', [label], target='tab')
2528 def help(self):
2529 class MyScrollArea(qw.QScrollArea):
2531 def sizeHint(self):
2532 s = qc.QSize()
2533 s.setWidth(self.widget().sizeHint().width())
2534 s.setHeight(self.widget().sizeHint().height())
2535 return s
2537 with open(pyrocko.util.data_file(
2538 'snuffler_help.html')) as f:
2539 hcheat = qw.QLabel(f.read())
2541 with open(pyrocko.util.data_file(
2542 'snuffler_help_epilog.html')) as f:
2543 hepilog = qw.QLabel(f.read())
2545 for h in [hcheat, hepilog]:
2546 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2547 h.setWordWrap(True)
2549 self.show_doc('Help', [hcheat, hepilog], target='panel')
2551 def show_doc(self, name, labels, target='panel'):
2552 scroller = qw.QScrollArea()
2553 frame = qw.QFrame(scroller)
2554 frame.setLineWidth(0)
2555 layout = qw.QVBoxLayout()
2556 layout.setContentsMargins(0, 0, 0, 0)
2557 layout.setSpacing(0)
2558 frame.setLayout(layout)
2559 scroller.setWidget(frame)
2560 scroller.setWidgetResizable(True)
2561 frame.setBackgroundRole(qg.QPalette.Base)
2562 for h in labels:
2563 h.setParent(frame)
2564 h.setMargin(3)
2565 h.setTextInteractionFlags(
2566 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2567 h.setBackgroundRole(qg.QPalette.Base)
2568 layout.addWidget(h)
2569 h.linkActivated.connect(
2570 self.open_link)
2572 if self.panel_parent is not None:
2573 if target == 'panel':
2574 self.panel_parent.add_panel(
2575 name, scroller, True, volatile=False)
2576 else:
2577 self.panel_parent.add_tab(name, scroller)
2579 def open_link(self, link):
2580 qg.QDesktopServices.openUrl(qc.QUrl(link))
2582 def wheelEvent(self, wheel_event):
2583 ''
2584 self.wheel_pos += wheel_event.angleDelta().y()
2586 n = self.wheel_pos // 120
2587 self.wheel_pos = self.wheel_pos % 120
2588 if n == 0:
2589 return
2591 amount = max(
2592 1.,
2593 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2594 wdelta = amount * n
2596 trmin, trmax = self.track_to_screen.get_in_range()
2597 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2598 / (trmax-trmin)
2600 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2601 self.zoom_tracks(anchor, wdelta)
2602 else:
2603 self.scroll_tracks(-wdelta)
2605 def dragEnterEvent(self, event):
2606 ''
2607 if event.mimeData().hasUrls():
2608 if any(url.toLocalFile() for url in event.mimeData().urls()):
2609 event.setDropAction(qc.Qt.LinkAction)
2610 event.accept()
2612 def dropEvent(self, event):
2613 ''
2614 if event.mimeData().hasUrls():
2615 paths = list(
2616 str(url.toLocalFile()) for url in event.mimeData().urls())
2617 event.acceptProposedAction()
2618 self.load(paths)
2620 def get_phase_name(self, kind):
2621 return self.config.get_phase_name(kind)
2623 def set_phase_kind(self, markers, kind):
2624 phasename = self.get_phase_name(kind)
2626 for marker in markers:
2627 if isinstance(marker, PhaseMarker):
2628 if kind == 10:
2629 marker.convert_to_marker()
2630 else:
2631 marker.set_phasename(phasename)
2632 marker.set_event(self.get_active_event())
2634 elif isinstance(marker, EventMarker):
2635 pass
2637 else:
2638 if kind != 10:
2639 event = self.get_active_event()
2640 marker.convert_to_phase_marker(
2641 event, phasename, None, False)
2643 def set_ntracks(self, ntracks):
2644 if self.ntracks != ntracks:
2645 self.ntracks = ntracks
2646 if self.shown_tracks_range is not None:
2647 low, high = self.shown_tracks_range
2648 else:
2649 low, high = 0, self.ntracks
2651 self.tracks_range_changed.emit(self.ntracks, low, high)
2653 def set_tracks_range(self, range, start=None):
2655 low, high = range
2656 low = min(self.ntracks-1, low)
2657 high = min(self.ntracks, high)
2658 low = max(0, low)
2659 high = max(1, high)
2661 if start is None:
2662 start = float(low)
2664 if self.shown_tracks_range != (low, high):
2665 self.shown_tracks_range = low, high
2666 self.shown_tracks_start = start
2668 self.tracks_range_changed.emit(self.ntracks, low, high)
2670 def scroll_tracks(self, shift):
2671 shown = self.shown_tracks_range
2672 shiftmin = -shown[0]
2673 shiftmax = self.ntracks-shown[1]
2674 shift = max(shiftmin, shift)
2675 shift = min(shiftmax, shift)
2676 shown = shown[0] + shift, shown[1] + shift
2678 self.set_tracks_range((int(shown[0]), int(shown[1])))
2680 self.update()
2682 def zoom_tracks(self, anchor, delta):
2683 ntracks_shown = self.shown_tracks_range[1] \
2684 - self.shown_tracks_range[0]
2686 if (ntracks_shown == 1 and delta <= 0) or \
2687 (ntracks_shown == self.ntracks and delta >= 0):
2688 return
2690 ntracks_shown += int(round(delta))
2691 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2693 u = self.shown_tracks_start
2694 nu = max(0., u-anchor*delta)
2695 nv = nu + ntracks_shown
2696 if nv > self.ntracks:
2697 nu -= nv - self.ntracks
2698 nv -= nv - self.ntracks
2700 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2702 self.ntracks_shown_max = self.shown_tracks_range[1] \
2703 - self.shown_tracks_range[0]
2705 self.update()
2707 def content_time_range(self):
2708 pile = self.get_pile()
2709 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2710 if tmin is None:
2711 tmin = initial_time_range[0]
2712 if tmax is None:
2713 tmax = initial_time_range[1]
2715 return tmin, tmax
2717 def content_deltat_range(self):
2718 pile = self.get_pile()
2720 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2722 if deltatmin is None or deltatmin == 0.0:
2723 deltatmin = 0.001
2725 if deltatmax is None:
2726 deltatmax = 1000.0
2728 return deltatmin, deltatmax
2730 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2731 if tmax < tmin:
2732 tmin, tmax = tmax, tmin
2734 deltatmin = self.content_deltat_range()[0]
2735 dt = deltatmin * self.visible_length * 0.95
2737 if dt == 0.0:
2738 dt = 1.0
2740 if tight:
2741 if tmax != tmin:
2742 dtm = tmax - tmin
2743 tmin -= dtm*0.1
2744 tmax += dtm*0.1
2745 return tmin, tmax
2746 else:
2747 tcenter = (tmin + tmax) / 2.
2748 tmin = tcenter - 0.5*dt
2749 tmax = tcenter + 0.5*dt
2750 return tmin, tmax
2752 if tmax-tmin < dt:
2753 vmin, vmax = self.get_time_range()
2754 dt = min(vmax - vmin, dt)
2756 tcenter = (tmin+tmax)/2.
2757 etmin, etmax = tmin, tmax
2758 tmin = min(etmin, tcenter - 0.5*dt)
2759 tmax = max(etmax, tcenter + 0.5*dt)
2760 dtm = tmax-tmin
2761 if etmin == tmin:
2762 tmin -= dtm*0.1
2763 if etmax == tmax:
2764 tmax += dtm*0.1
2766 else:
2767 dtm = tmax-tmin
2768 tmin -= dtm*0.1
2769 tmax += dtm*0.1
2771 return tmin, tmax
2773 def go_to_selection(self, tight=False):
2774 markers = self.selected_markers()
2775 if markers:
2776 tmax, tmin = self.content_time_range()
2777 for marker in markers:
2778 tmin = min(tmin, marker.tmin)
2779 tmax = max(tmax, marker.tmax)
2781 else:
2782 if tight:
2783 vmin, vmax = self.get_time_range()
2784 tmin = tmax = (vmin + vmax) / 2.
2785 else:
2786 tmin, tmax = self.content_time_range()
2788 tmin, tmax = self.make_good_looking_time_range(
2789 tmin, tmax, tight=tight)
2791 self.interrupt_following()
2792 self.set_time_range(tmin, tmax)
2793 self.update()
2795 def go_to_time(self, t, tlen=None):
2796 tmax = t
2797 if tlen is not None:
2798 tmax = t+tlen
2799 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2800 self.interrupt_following()
2801 self.set_time_range(tmin, tmax)
2802 self.update()
2804 def go_to_event_by_name(self, name):
2805 for marker in self.markers:
2806 if isinstance(marker, EventMarker):
2807 event = marker.get_event()
2808 if event.name and event.name.lower() == name.lower():
2809 tmin, tmax = self.make_good_looking_time_range(
2810 event.time, event.time)
2812 self.interrupt_following()
2813 self.set_time_range(tmin, tmax)
2815 def printit(self):
2816 from ..qt_compat import qprint
2817 printer = qprint.QPrinter()
2818 printer.setOrientation(qprint.QPrinter.Landscape)
2820 dialog = qprint.QPrintDialog(printer, self)
2821 dialog.setWindowTitle('Print')
2823 if dialog.exec_() != qw.QDialog.Accepted:
2824 return
2826 painter = qg.QPainter()
2827 painter.begin(printer)
2828 page = printer.pageRect()
2829 self.drawit(
2830 painter, printmode=False, w=page.width(), h=page.height())
2832 painter.end()
2834 def savesvg(self, fn=None):
2836 if not fn:
2837 fn, _ = qw.QFileDialog.getSaveFileName(
2838 self,
2839 'Save as SVG|PNG',
2840 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2841 'SVG|PNG (*.svg *.png)',
2842 options=qfiledialog_options)
2844 if fn == '':
2845 return
2847 fn = str(fn)
2849 if fn.lower().endswith('.svg'):
2850 try:
2851 w, h = 842, 595
2852 margin = 0.025
2853 m = max(w, h)*margin
2855 generator = qsvg.QSvgGenerator()
2856 generator.setFileName(fn)
2857 generator.setSize(qc.QSize(w, h))
2858 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2860 painter = qg.QPainter()
2861 painter.begin(generator)
2862 self.drawit(painter, printmode=False, w=w, h=h)
2863 painter.end()
2865 except Exception as e:
2866 self.fail('Failed to write SVG file: %s' % str(e))
2868 elif fn.lower().endswith('.png'):
2869 pixmap = self.grab()
2871 try:
2872 pixmap.save(fn)
2874 except Exception as e:
2875 self.fail('Failed to write PNG file: %s' % str(e))
2877 else:
2878 self.fail(
2879 'Unsupported file type: filename must end with ".svg" or '
2880 '".png".')
2882 def paintEvent(self, paint_ev):
2883 '''
2884 Called by QT whenever widget needs to be painted.
2885 '''
2886 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2887 # was called twice (by different threads?), causing segfaults.
2888 if self.in_paint_event:
2889 logger.warning('Blocking reentrant call to paintEvent().')
2890 return
2892 self.in_paint_event = True
2894 painter = qg.QPainter(self)
2896 if self.menuitem_antialias.isChecked():
2897 painter.setRenderHint(qg.QPainter.Antialiasing)
2899 self.drawit(painter)
2901 logger.debug(
2902 'Time spent drawing: '
2903 ' user:%.3f sys:%.3f children_user:%.3f'
2904 ' childred_sys:%.3f elapsed:%.3f' %
2905 (self.timer_draw - self.timer_cutout))
2907 logger.debug(
2908 'Time spent processing:'
2909 ' user:%.3f sys:%.3f children_user:%.3f'
2910 ' childred_sys:%.3f elapsed:%.3f' %
2911 self.timer_cutout.get())
2913 self.time_spent_painting = self.timer_draw.get()[-1]
2914 self.time_last_painted = time.time()
2915 self.in_paint_event = False
2917 def determine_box_styles(self):
2919 traces = list(self.pile.iter_traces())
2920 traces.sort(key=operator.attrgetter('full_id'))
2921 istyle = 0
2922 trace_styles = {}
2923 for itr, tr in enumerate(traces):
2924 if itr > 0:
2925 other = traces[itr-1]
2926 if not (
2927 other.nslc_id == tr.nslc_id
2928 and other.deltat == tr.deltat
2929 and abs(other.tmax - tr.tmin)
2930 < gap_lap_tolerance*tr.deltat):
2932 istyle += 1
2934 trace_styles[tr.full_id, tr.deltat] = istyle
2936 self.trace_styles = trace_styles
2938 def draw_trace_boxes(self, p, time_projection, track_projections):
2940 for v_projection in track_projections.values():
2941 v_projection.set_in_range(0., 1.)
2943 def selector(x):
2944 return x.overlaps(*time_projection.get_in_range())
2946 if self.trace_filter is not None:
2947 def tselector(x):
2948 return selector(x) and self.trace_filter(x)
2950 else:
2951 tselector = selector
2953 traces = list(self.pile.iter_traces(
2954 group_selector=selector, trace_selector=tselector))
2956 traces.sort(key=operator.attrgetter('full_id'))
2958 def drawbox(itrack, istyle, traces):
2959 v_projection = track_projections[itrack]
2960 dvmin = v_projection(0.)
2961 dvmax = v_projection(1.)
2962 dtmin = time_projection.clipped(traces[0].tmin, 0)
2963 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2965 style = box_styles[istyle % len(box_styles)]
2966 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2967 p.fillRect(rect, style.fill_brush)
2968 p.setPen(style.frame_pen)
2969 p.drawRect(rect)
2971 traces_by_style = {}
2972 for itr, tr in enumerate(traces):
2973 gt = self.gather(tr)
2974 if gt not in self.key_to_row:
2975 continue
2977 itrack = self.key_to_row[gt]
2978 if itrack not in track_projections:
2979 continue
2981 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2983 if len(traces) < 500:
2984 drawbox(itrack, istyle, [tr])
2985 else:
2986 if (itrack, istyle) not in traces_by_style:
2987 traces_by_style[itrack, istyle] = []
2988 traces_by_style[itrack, istyle].append(tr)
2990 for (itrack, istyle), traces in traces_by_style.items():
2991 drawbox(itrack, istyle, traces)
2993 def draw_visible_markers(
2994 self, p, vcenter_projection, primary_pen):
2996 try:
2997 markers = self.markers.with_key_in_limited(
2998 self.tmin - self.markers_deltat_max, self.tmax, 2000)
3000 except pyrocko.pile.TooMany:
3001 tmin = self.markers[0].tmin
3002 tmax = self.markers[-1].tmax
3003 umin_view, umax_view = self.time_projection.get_out_range()
3004 umin = max(umin_view, self.time_projection(tmin))
3005 umax = min(umax_view, self.time_projection(tmax))
3006 v0, _ = vcenter_projection.get_out_range()
3007 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
3009 p.save()
3011 pen = qg.QPen(primary_pen)
3012 pen.setWidth(2)
3013 pen.setStyle(qc.Qt.DotLine)
3014 # pat = [5., 3.]
3015 # pen.setDashPattern(pat)
3016 p.setPen(pen)
3018 if self.n_selected_markers == len(self.markers):
3019 s_selected = ' (all selected)'
3020 elif self.n_selected_markers > 0:
3021 s_selected = ' (%i selected)' % self.n_selected_markers
3022 else:
3023 s_selected = ''
3025 draw_label(
3026 p, umin+10., v0-10.,
3027 '%i Markers' % len(self.markers) + s_selected,
3028 label_bg, 'LB')
3030 line = qc.QLineF(umin, v0, umax, v0)
3031 p.drawLine(line)
3032 p.restore()
3034 return
3036 for marker in markers:
3037 if marker.tmin < self.tmax and self.tmin < marker.tmax \
3038 and marker.kind in self.visible_marker_kinds:
3040 marker.draw(
3041 p, self.time_projection, vcenter_projection,
3042 with_label=True)
3044 def get_squirrel(self):
3045 try:
3046 return self.pile._squirrel
3047 except AttributeError:
3048 return None
3050 def draw_coverage(self, p, time_projection, track_projections):
3051 sq = self.get_squirrel()
3052 if sq is None:
3053 return
3055 def drawbox(itrack, tmin, tmax, style):
3056 v_projection = track_projections[itrack]
3057 dvmin = v_projection(0.)
3058 dvmax = v_projection(1.)
3059 dtmin = time_projection.clipped(tmin, 0)
3060 dtmax = time_projection.clipped(tmax, 1)
3062 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3063 p.fillRect(rect, style.fill_brush)
3064 p.setPen(style.frame_pen)
3065 p.drawRect(rect)
3067 pattern_list = []
3068 pattern_to_itrack = {}
3069 for key in self.track_keys:
3070 itrack = self.key_to_row[key]
3071 if itrack not in track_projections:
3072 continue
3074 pattern = self.track_patterns[itrack]
3075 pattern_to_itrack[tuple(pattern)] = itrack
3076 pattern_list.append(tuple(pattern))
3078 vmin, vmax = self.get_time_range()
3080 for kind in ['waveform', 'waveform_promise']:
3081 for coverage in sq.get_coverage(
3082 kind, vmin, vmax, pattern_list, limit=500):
3083 itrack = pattern_to_itrack[coverage.pattern.nslc]
3085 if coverage.changes is None:
3086 drawbox(
3087 itrack, coverage.tmin, coverage.tmax,
3088 box_styles_coverage[kind][0])
3089 else:
3090 t = None
3091 pcount = 0
3092 for tb, count in coverage.changes:
3093 if t is not None and tb > t:
3094 if pcount > 0:
3095 drawbox(
3096 itrack, t, tb,
3097 box_styles_coverage[kind][
3098 min(len(box_styles_coverage)-1,
3099 pcount)])
3101 t = tb
3102 pcount = count
3104 def drawit(self, p, printmode=False, w=None, h=None):
3105 '''
3106 This performs the actual drawing.
3107 '''
3109 self.timer_draw.start()
3110 show_boxes = self.menuitem_showboxes.isChecked()
3111 sq = self.get_squirrel()
3113 if self.gather is None:
3114 self.set_gathering()
3116 if self.pile_has_changed:
3118 if not self.sortingmode_change_delayed():
3119 self.sortingmode_change()
3121 if show_boxes and sq is None:
3122 self.determine_box_styles()
3124 self.pile_has_changed = False
3126 if h is None:
3127 h = float(self.height())
3128 if w is None:
3129 w = float(self.width())
3131 if printmode:
3132 primary_color = (0, 0, 0)
3133 else:
3134 primary_color = pyrocko.plot.tango_colors['aluminium5']
3136 primary_pen = qg.QPen(qg.QColor(*primary_color))
3138 ax_h = float(self.ax_height)
3140 vbottom_ax_projection = Projection()
3141 vtop_ax_projection = Projection()
3142 vcenter_projection = Projection()
3144 self.time_projection.set_out_range(0., w)
3145 vbottom_ax_projection.set_out_range(h-ax_h, h)
3146 vtop_ax_projection.set_out_range(0., ax_h)
3147 vcenter_projection.set_out_range(ax_h, h-ax_h)
3148 vcenter_projection.set_in_range(0., 1.)
3149 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3151 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3152 track_projections = {}
3153 for i in range(*self.shown_tracks_range):
3154 proj = Projection()
3155 proj.set_out_range(
3156 self.track_to_screen(i+0.05),
3157 self.track_to_screen(i+1.-0.05))
3159 track_projections[i] = proj
3161 if self.tmin > self.tmax:
3162 return
3164 self.time_projection.set_in_range(self.tmin, self.tmax)
3165 vbottom_ax_projection.set_in_range(0, ax_h)
3167 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3169 yscaler = pyrocko.plot.AutoScaler()
3171 p.setPen(primary_pen)
3173 font = qg.QFont()
3174 font.setBold(True)
3176 axannotfont = qg.QFont()
3177 axannotfont.setBold(True)
3178 axannotfont.setPointSize(8)
3180 processed_traces = self.prepare_cutout2(
3181 self.tmin, self.tmax,
3182 trace_selector=self.trace_selector,
3183 degap=self.menuitem_degap.isChecked(),
3184 demean=self.menuitem_demean.isChecked())
3186 if not printmode and show_boxes:
3187 if (self.view_mode is ViewMode.Wiggle) \
3188 or (self.view_mode is ViewMode.Waterfall
3189 and not processed_traces):
3191 if sq is None:
3192 self.draw_trace_boxes(
3193 p, self.time_projection, track_projections)
3195 else:
3196 self.draw_coverage(
3197 p, self.time_projection, track_projections)
3199 p.setFont(font)
3200 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3202 color_lookup = dict(
3203 [(k, i) for (i, k) in enumerate(self.color_keys)])
3205 self.track_to_nslc_ids = {}
3206 nticks = 0
3207 annot_labels = []
3209 if self.view_mode is ViewMode.Waterfall and processed_traces:
3210 waterfall = self.waterfall
3211 waterfall.set_time_range(self.tmin, self.tmax)
3212 waterfall.set_traces(processed_traces)
3213 waterfall.set_cmap(self.waterfall_cmap)
3214 waterfall.set_integrate(self.waterfall_integrate)
3215 waterfall.set_clip(
3216 self.waterfall_clip_min, self.waterfall_clip_max)
3217 waterfall.show_absolute_values(
3218 self.waterfall_show_absolute)
3220 rect = qc.QRectF(
3221 0, self.ax_height,
3222 self.width(), self.height() - self.ax_height*2
3223 )
3224 waterfall.draw_waterfall(p, rect=rect)
3226 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3227 show_scales = self.menuitem_showscalerange.isChecked() \
3228 or self.menuitem_showscaleaxis.isChecked()
3230 fm = qg.QFontMetrics(axannotfont, p.device())
3231 trackheight = self.track_to_screen(1.-0.05) \
3232 - self.track_to_screen(0.05)
3234 nlinesavail = trackheight/float(fm.lineSpacing())
3236 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3237 if self.menuitem_showscaleaxis.isChecked() \
3238 else 15
3240 yscaler = pyrocko.plot.AutoScaler(
3241 no_exp_interval=(-3, 2), approx_ticks=nticks,
3242 snap=show_scales
3243 and not self.menuitem_showscaleaxis.isChecked())
3245 data_ranges = pyrocko.trace.minmax(
3246 processed_traces,
3247 key=self.scaling_key,
3248 mode=self.scaling_base[0],
3249 outer_mode=self.scaling_base[1])
3251 if not self.menuitem_fixscalerange.isChecked():
3252 self.old_data_ranges = data_ranges
3253 else:
3254 data_ranges.update(self.old_data_ranges)
3256 self.apply_scaling_hooks(data_ranges)
3258 trace_to_itrack = {}
3259 track_scaling_keys = {}
3260 track_scaling_colors = {}
3261 for trace in processed_traces:
3262 gt = self.gather(trace)
3263 if gt not in self.key_to_row:
3264 continue
3266 itrack = self.key_to_row[gt]
3267 if itrack not in track_projections:
3268 continue
3270 trace_to_itrack[trace] = itrack
3272 if itrack not in self.track_to_nslc_ids:
3273 self.track_to_nslc_ids[itrack] = set()
3275 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3277 if itrack not in track_scaling_keys:
3278 track_scaling_keys[itrack] = set()
3280 scaling_key = self.scaling_key(trace)
3281 track_scaling_keys[itrack].add(scaling_key)
3283 color = pyrocko.plot.color(
3284 color_lookup[self.color_gather(trace)])
3286 k = itrack, scaling_key
3287 if k not in track_scaling_colors \
3288 and self.menuitem_colortraces.isChecked():
3289 track_scaling_colors[k] = color
3290 else:
3291 track_scaling_colors[k] = primary_color
3293 # y axes, zero lines
3294 trace_projections = {}
3295 for itrack in list(track_projections.keys()):
3296 if itrack not in track_scaling_keys:
3297 continue
3298 uoff = 0
3299 for scaling_key in track_scaling_keys[itrack]:
3300 data_range = data_ranges[scaling_key]
3301 dymin, dymax = data_range
3302 ymin, ymax, yinc = yscaler.make_scale(
3303 (dymin/self.gain, dymax/self.gain))
3304 iexp = yscaler.make_exp(yinc)
3305 factor = 10**iexp
3306 trace_projection = track_projections[itrack].copy()
3307 trace_projection.set_in_range(ymax, ymin)
3308 trace_projections[itrack, scaling_key] = \
3309 trace_projection
3310 umin, umax = self.time_projection.get_out_range()
3311 vmin, vmax = trace_projection.get_out_range()
3312 umax_zeroline = umax
3313 uoffnext = uoff
3315 if show_scales:
3316 pen = qg.QPen(primary_pen)
3317 k = itrack, scaling_key
3318 if k in track_scaling_colors:
3319 c = qg.QColor(*track_scaling_colors[
3320 itrack, scaling_key])
3322 pen.setColor(c)
3324 p.setPen(pen)
3325 if nlinesavail > 3:
3326 if self.menuitem_showscaleaxis.isChecked():
3327 ymin_annot = math.ceil(ymin/yinc)*yinc
3328 ny_annot = int(
3329 math.floor(ymax/yinc)
3330 - math.ceil(ymin/yinc)) + 1
3332 for iy_annot in range(ny_annot):
3333 y = ymin_annot + iy_annot*yinc
3334 v = trace_projection(y)
3335 line = qc.QLineF(
3336 umax-10-uoff, v, umax-uoff, v)
3338 p.drawLine(line)
3339 if iy_annot == ny_annot - 1 \
3340 and iexp != 0:
3341 sexp = ' × ' \
3342 '10<sup>%i</sup>' % iexp
3343 else:
3344 sexp = ''
3346 snum = num_to_html(y/factor)
3347 lab = Label(
3348 p,
3349 umax-20-uoff,
3350 v, '%s%s' % (snum, sexp),
3351 label_bg=None,
3352 anchor='MR',
3353 font=axannotfont,
3354 color=c)
3356 uoffnext = max(
3357 lab.rect.width()+30., uoffnext)
3359 annot_labels.append(lab)
3360 if y == 0.:
3361 umax_zeroline = \
3362 umax - 20 \
3363 - lab.rect.width() - 10 \
3364 - uoff
3365 else:
3366 if not show_boxes:
3367 qpoints = make_QPolygonF(
3368 [umax-20-uoff,
3369 umax-10-uoff,
3370 umax-10-uoff,
3371 umax-20-uoff],
3372 [vmax, vmax, vmin, vmin])
3373 p.drawPolyline(qpoints)
3375 snum = num_to_html(ymin)
3376 labmin = Label(
3377 p, umax-15-uoff, vmax, snum,
3378 label_bg=None,
3379 anchor='BR',
3380 font=axannotfont,
3381 color=c)
3383 annot_labels.append(labmin)
3384 snum = num_to_html(ymax)
3385 labmax = Label(
3386 p, umax-15-uoff, vmin, snum,
3387 label_bg=None,
3388 anchor='TR',
3389 font=axannotfont,
3390 color=c)
3392 annot_labels.append(labmax)
3394 for lab in (labmin, labmax):
3395 uoffnext = max(
3396 lab.rect.width()+10., uoffnext)
3398 if self.menuitem_showzeroline.isChecked():
3399 v = trace_projection(0.)
3400 if vmin <= v <= vmax:
3401 line = qc.QLineF(umin, v, umax_zeroline, v)
3402 p.drawLine(line)
3404 uoff = uoffnext
3406 p.setFont(font)
3407 p.setPen(primary_pen)
3408 for trace in processed_traces:
3409 if self.view_mode is not ViewMode.Wiggle:
3410 break
3412 if trace not in trace_to_itrack:
3413 continue
3415 itrack = trace_to_itrack[trace]
3416 scaling_key = self.scaling_key(trace)
3417 trace_projection = trace_projections[
3418 itrack, scaling_key]
3420 vdata = trace_projection(trace.get_ydata())
3422 udata_min = float(self.time_projection(trace.tmin))
3423 udata_max = float(self.time_projection(
3424 trace.tmin+trace.deltat*(vdata.size-1)))
3425 udata = num.linspace(udata_min, udata_max, vdata.size)
3427 qpoints = make_QPolygonF(udata, vdata)
3429 umin, umax = self.time_projection.get_out_range()
3430 vmin, vmax = trace_projection.get_out_range()
3432 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3434 if self.menuitem_cliptraces.isChecked():
3435 p.setClipRect(trackrect)
3437 if self.menuitem_colortraces.isChecked():
3438 color = pyrocko.plot.color(
3439 color_lookup[self.color_gather(trace)])
3440 pen = qg.QPen(qg.QColor(*color), 1)
3441 p.setPen(pen)
3443 p.drawPolyline(qpoints)
3445 if self.floating_marker:
3446 self.floating_marker.draw_trace(
3447 self, p, trace,
3448 self.time_projection, trace_projection, 1.0)
3450 for marker in self.markers.with_key_in(
3451 self.tmin - self.markers_deltat_max,
3452 self.tmax):
3454 if marker.tmin < self.tmax \
3455 and self.tmin < marker.tmax \
3456 and marker.kind \
3457 in self.visible_marker_kinds:
3458 marker.draw_trace(
3459 self, p, trace, self.time_projection,
3460 trace_projection, 1.0)
3462 p.setPen(primary_pen)
3464 if self.menuitem_cliptraces.isChecked():
3465 p.setClipRect(0, 0, int(w), int(h))
3467 if self.floating_marker:
3468 self.floating_marker.draw(
3469 p, self.time_projection, vcenter_projection)
3471 self.draw_visible_markers(
3472 p, vcenter_projection, primary_pen)
3474 p.setPen(primary_pen)
3475 while font.pointSize() > 2:
3476 fm = qg.QFontMetrics(font, p.device())
3477 trackheight = self.track_to_screen(1.-0.05) \
3478 - self.track_to_screen(0.05)
3479 nlinesavail = trackheight/float(fm.lineSpacing())
3480 if nlinesavail > 1:
3481 break
3483 font.setPointSize(font.pointSize()-1)
3485 p.setFont(font)
3486 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3488 for key in self.track_keys:
3489 itrack = self.key_to_row[key]
3490 if itrack in track_projections:
3491 plabel = ' '.join(
3492 [str(x) for x in key if x is not None])
3493 lx = 10
3494 ly = self.track_to_screen(itrack+0.5)
3496 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3497 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3498 continue
3500 contains_cursor = \
3501 self.track_to_screen(itrack) \
3502 < mouse_pos.y() \
3503 < self.track_to_screen(itrack+1)
3505 if not contains_cursor:
3506 continue
3508 font_large = p.font()
3509 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3510 p.setFont(font_large)
3511 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3512 p.setFont(font)
3514 for lab in annot_labels:
3515 lab.draw()
3517 self.timer_draw.stop()
3519 def see_data_params(self):
3521 min_deltat = self.content_deltat_range()[0]
3523 # determine padding and downampling requirements
3524 if self.lowpass is not None:
3525 deltat_target = 1./self.lowpass * 0.25
3526 ndecimate = min(
3527 50,
3528 max(1, int(round(deltat_target / min_deltat))))
3529 tpad = 1./self.lowpass * 2.
3530 else:
3531 ndecimate = 1
3532 tpad = min_deltat*5.
3534 if self.highpass is not None:
3535 tpad = max(1./self.highpass * 2., tpad)
3537 nsee_points_per_trace = 5000*10
3538 tsee = ndecimate*nsee_points_per_trace*min_deltat
3540 return ndecimate, tpad, tsee
3542 def clean_update(self):
3543 self.cached_processed_traces = None
3544 self.update()
3546 def get_adequate_tpad(self):
3547 tpad = 0.
3548 for f in [self.highpass, self.lowpass]:
3549 if f is not None:
3550 tpad = max(tpad, 2.0/f)
3552 for snuffling in self.snufflings:
3553 if snuffling._post_process_hook_enabled \
3554 or snuffling._pre_process_hook_enabled:
3556 tpad = max(tpad, snuffling.get_tpad())
3558 return tpad
3560 def prepare_cutout2(
3561 self, tmin, tmax, trace_selector=None, degap=True,
3562 demean=True, nmax=6000):
3564 if self.pile.is_empty():
3565 return []
3567 nmax = self.visible_length
3569 self.timer_cutout.start()
3571 tsee = tmax-tmin
3572 min_deltat_wo_decimate = tsee/nmax
3573 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3575 min_deltat_allow = min_deltat_wo_decimate
3576 if self.lowpass is not None:
3577 target_deltat_lp = 0.25/self.lowpass
3578 if target_deltat_lp > min_deltat_wo_decimate:
3579 min_deltat_allow = min_deltat_w_decimate
3581 min_deltat_allow = math.exp(
3582 int(math.floor(math.log(min_deltat_allow))))
3584 tmin_ = tmin
3585 tmax_ = tmax
3587 # fetch more than needed?
3588 if self.menuitem_liberal_fetch.isChecked():
3589 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3590 tmin = math.floor(tmin/tlen) * tlen
3591 tmax = math.ceil(tmax/tlen) * tlen
3593 fft_filtering = self.menuitem_fft_filtering.isChecked()
3594 lphp = self.menuitem_lphp.isChecked()
3595 ads = self.menuitem_allowdownsampling.isChecked()
3597 tpad = self.get_adequate_tpad()
3598 tpad = min(tpad, tsee*3)
3600 # state vector to decide if cached traces can be used
3601 vec = (
3602 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3603 self.highpass, fft_filtering, lphp,
3604 min_deltat_allow, self.rotate, self.shown_tracks_range,
3605 ads, self.pile.get_update_count())
3607 if (self.cached_vec
3608 and self.cached_vec[0] <= vec[0]
3609 and vec[1] <= self.cached_vec[1]
3610 and vec[2:] == self.cached_vec[2:]
3611 and not (self.reloaded or self.menuitem_watch.isChecked())
3612 and self.cached_processed_traces is not None):
3614 logger.debug('Using cached traces')
3615 processed_traces = self.cached_processed_traces
3617 else:
3618 processed_traces = []
3619 if self.pile.deltatmax >= min_deltat_allow:
3621 if isinstance(self.pile, pyrocko.pile.Pile):
3622 def group_selector(gr):
3623 return gr.deltatmax >= min_deltat_allow
3625 kwargs = dict(group_selector=group_selector)
3626 else:
3627 kwargs = {}
3629 if trace_selector is not None:
3630 def trace_selectorx(tr):
3631 return tr.deltat >= min_deltat_allow \
3632 and trace_selector(tr)
3633 else:
3634 def trace_selectorx(tr):
3635 return tr.deltat >= min_deltat_allow
3637 for traces in self.pile.chopper(
3638 tmin=tmin, tmax=tmax, tpad=tpad,
3639 want_incomplete=True,
3640 degap=degap,
3641 maxgap=gap_lap_tolerance,
3642 maxlap=gap_lap_tolerance,
3643 keep_current_files_open=True,
3644 trace_selector=trace_selectorx,
3645 accessor_id=id(self),
3646 snap=(math.floor, math.ceil),
3647 include_last=True, **kwargs):
3649 if demean:
3650 for tr in traces:
3651 if (tr.meta and tr.meta.get('tabu', False)):
3652 continue
3653 y = tr.get_ydata()
3654 tr.set_ydata(y - num.mean(y))
3656 traces = self.pre_process_hooks(traces)
3658 for trace in traces:
3660 if not (trace.meta
3661 and trace.meta.get('tabu', False)):
3663 if fft_filtering:
3664 but = pyrocko.response.ButterworthResponse
3665 multres = pyrocko.response.MultiplyResponse
3666 if self.lowpass is not None \
3667 or self.highpass is not None:
3669 it = num.arange(
3670 trace.data_len(), dtype=float)
3671 detr_data, m, b = detrend(
3672 it, trace.get_ydata())
3674 trace.set_ydata(detr_data)
3676 freqs, fdata = trace.spectrum(
3677 pad_to_pow2=True, tfade=None)
3679 nfreqs = fdata.size
3681 key = (trace.deltat, nfreqs)
3683 if key not in self.tf_cache:
3684 resps = []
3685 if self.lowpass is not None:
3686 resps.append(but(
3687 order=4,
3688 corner=self.lowpass,
3689 type='low'))
3691 if self.highpass is not None:
3692 resps.append(but(
3693 order=4,
3694 corner=self.highpass,
3695 type='high'))
3697 resp = multres(resps)
3698 self.tf_cache[key] = \
3699 resp.evaluate(freqs)
3701 filtered_data = num.fft.irfft(
3702 fdata*self.tf_cache[key]
3703 )[:trace.data_len()]
3705 retrended_data = retrend(
3706 it, filtered_data, m, b)
3708 trace.set_ydata(retrended_data)
3710 else:
3712 if ads and self.lowpass is not None:
3713 while trace.deltat \
3714 < min_deltat_wo_decimate:
3716 trace.downsample(2, demean=False)
3718 fmax = 0.5/trace.deltat
3719 if not lphp and (
3720 self.lowpass is not None
3721 and self.highpass is not None
3722 and self.lowpass < fmax
3723 and self.highpass < fmax
3724 and self.highpass < self.lowpass):
3726 trace.bandpass(
3727 2, self.highpass, self.lowpass)
3728 else:
3729 if self.lowpass is not None:
3730 if self.lowpass < 0.5/trace.deltat:
3731 trace.lowpass(
3732 4, self.lowpass,
3733 demean=False)
3735 if self.highpass is not None:
3736 if self.lowpass is None \
3737 or self.highpass \
3738 < self.lowpass:
3740 if self.highpass < \
3741 0.5/trace.deltat:
3742 trace.highpass(
3743 4, self.highpass,
3744 demean=False)
3746 processed_traces.append(trace)
3748 if self.rotate != 0.0:
3749 phi = self.rotate/180.*math.pi
3750 cphi = math.cos(phi)
3751 sphi = math.sin(phi)
3752 for a in processed_traces:
3753 for b in processed_traces:
3754 if (a.network == b.network
3755 and a.station == b.station
3756 and a.location == b.location
3757 and ((a.channel.lower().endswith('n')
3758 and b.channel.lower().endswith('e'))
3759 or (a.channel.endswith('1')
3760 and b.channel.endswith('2')))
3761 and abs(a.deltat-b.deltat) < a.deltat*0.001
3762 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3763 len(a.get_ydata()) == len(b.get_ydata())):
3765 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3766 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3767 a.set_ydata(aydata)
3768 b.set_ydata(bydata)
3770 processed_traces = self.post_process_hooks(processed_traces)
3772 self.cached_processed_traces = processed_traces
3773 self.cached_vec = vec
3775 chopped_traces = []
3776 for trace in processed_traces:
3777 chop_tmin = tmin_ - trace.deltat*4
3778 chop_tmax = tmax_ + trace.deltat*4
3780 try:
3781 ctrace = trace.chop(
3782 chop_tmin, chop_tmax,
3783 inplace=False)
3785 except pyrocko.trace.NoData:
3786 continue
3788 if ctrace.data_len() < 2:
3789 continue
3791 chopped_traces.append(ctrace)
3793 self.timer_cutout.stop()
3794 return chopped_traces
3796 def pre_process_hooks(self, traces):
3797 for snuffling in self.snufflings:
3798 if snuffling._pre_process_hook_enabled:
3799 traces = snuffling.pre_process_hook(traces)
3801 return traces
3803 def post_process_hooks(self, traces):
3804 for snuffling in self.snufflings:
3805 if snuffling._post_process_hook_enabled:
3806 traces = snuffling.post_process_hook(traces)
3808 return traces
3810 def visible_length_change(self, ignore=None):
3811 for menuitem, vlen in self.menuitems_visible_length:
3812 if menuitem.isChecked():
3813 self.visible_length = vlen
3815 def scaling_base_change(self, ignore=None):
3816 for menuitem, scaling_base in self.menuitems_scaling_base:
3817 if menuitem.isChecked():
3818 self.scaling_base = scaling_base
3820 def scalingmode_change(self, ignore=None):
3821 for menuitem, scaling_key in self.menuitems_scaling:
3822 if menuitem.isChecked():
3823 self.scaling_key = scaling_key
3824 self.update()
3826 def apply_scaling_hooks(self, data_ranges):
3827 for k in sorted(self.scaling_hooks.keys()):
3828 hook = self.scaling_hooks[k]
3829 hook(data_ranges)
3831 def viewmode_change(self, ignore=True):
3832 for item, mode in self.menuitems_viewmode:
3833 if item.isChecked():
3834 self.view_mode = mode
3835 break
3836 else:
3837 raise AttributeError('unknown view mode')
3839 items_waterfall_disabled = (
3840 self.menuitem_showscaleaxis,
3841 self.menuitem_showscalerange,
3842 self.menuitem_showzeroline,
3843 self.menuitem_colortraces,
3844 self.menuitem_cliptraces,
3845 *(itm[0] for itm in self.menuitems_visible_length)
3846 )
3848 if self.view_mode is ViewMode.Waterfall:
3849 self.parent().show_colorbar_ctrl(True)
3850 self.parent().show_gain_ctrl(False)
3852 for item in items_waterfall_disabled:
3853 item.setDisabled(True)
3855 self.visible_length = 180.
3856 else:
3857 self.parent().show_colorbar_ctrl(False)
3858 self.parent().show_gain_ctrl(True)
3860 for item in items_waterfall_disabled:
3861 item.setDisabled(False)
3863 self.visible_length_change()
3864 self.update()
3866 def set_scaling_hook(self, k, hook):
3867 self.scaling_hooks[k] = hook
3869 def remove_scaling_hook(self, k):
3870 del self.scaling_hooks[k]
3872 def remove_scaling_hooks(self):
3873 self.scaling_hooks = {}
3875 def s_sortingmode_change(self, ignore=None):
3876 for menuitem, valfunc in self.menuitems_ssorting:
3877 if menuitem.isChecked():
3878 self._ssort = valfunc
3880 self.sortingmode_change()
3882 def sortingmode_change(self, ignore=None):
3883 for menuitem, (gather, color) in self.menuitems_sorting:
3884 if menuitem.isChecked():
3885 self.set_gathering(gather, color)
3887 self.sortingmode_change_time = time.time()
3889 def lowpass_change(self, value, ignore=None):
3890 self.lowpass = value
3891 self.passband_check()
3892 self.tf_cache = {}
3893 self.update()
3894 self.frequency_filter_changed.emit(
3895 self.lowpass or 0.0, self.highpass or 0.0)
3897 def highpass_change(self, value, ignore=None):
3898 self.highpass = value
3899 self.passband_check()
3900 self.tf_cache = {}
3901 self.update()
3902 self.frequency_filter_changed.emit(
3903 self.lowpass or 0.0, self.highpass or 0.0)
3905 def passband_check(self):
3906 if self.highpass and self.lowpass \
3907 and self.highpass >= self.lowpass:
3909 self.window().status_messages.set(
3910 'filter_error',
3911 'Corner frequency of highpass greater than '
3912 'corner frequency of lowpass. Highpass deactivated.')
3913 else:
3914 self.window().status_messages.clear('filter_error')
3916 def gain_change(self, value, ignore):
3917 self.gain = value
3918 self.update()
3920 def rot_change(self, value, ignore):
3921 self.rotate = value
3922 self.update()
3924 def waterfall_cmap_change(self, cmap):
3925 self.waterfall_cmap = cmap
3926 self.update()
3928 def waterfall_clip_change(self, clip_min, clip_max):
3929 self.waterfall_clip_min = clip_min
3930 self.waterfall_clip_max = clip_max
3931 self.update()
3933 def waterfall_show_absolute_change(self, toggle):
3934 self.waterfall_show_absolute = toggle
3935 self.update()
3937 def waterfall_set_integrate(self, toggle):
3938 self.waterfall_integrate = toggle
3939 self.update()
3941 def set_selected_markers(self, markers):
3942 '''
3943 Set a list of markers selected
3945 :param markers: list of markers
3946 '''
3947 self.deselect_all()
3948 for m in markers:
3949 m.selected = True
3951 self.update()
3953 def deselect_all(self):
3954 for marker in self.markers:
3955 marker.selected = False
3957 def animate_picking(self):
3958 point = self.mapFromGlobal(qg.QCursor.pos())
3959 self.update_picking(point.x(), point.y(), doshift=True)
3961 def get_nslc_ids_for_track(self, ftrack):
3962 itrack = int(ftrack)
3963 return self.track_to_nslc_ids.get(itrack, [])
3965 def stop_picking(self, x, y, abort=False):
3966 if self.picking:
3967 self.update_picking(x, y, doshift=False)
3968 self.picking = None
3969 self.picking_down = None
3970 self.picking_timer.stop()
3971 self.picking_timer = None
3972 if not abort:
3973 self.add_marker(self.floating_marker)
3974 self.floating_marker.selected = True
3975 self.emit_selected_markers()
3977 self.floating_marker = None
3979 def start_picking(self, ignore):
3981 if not self.picking:
3982 self.deselect_all()
3983 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3984 point = self.mapFromGlobal(qg.QCursor.pos())
3986 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3987 self.picking.setGeometry(
3988 gpoint.x(), gpoint.y(), 1, self.height())
3989 t = self.time_projection.rev(point.x())
3991 ftrack = self.track_to_screen.rev(point.y())
3992 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3993 self.floating_marker = Marker(nslc_ids, t, t)
3994 self.floating_marker.selected = True
3996 self.picking_timer = qc.QTimer()
3997 self.picking_timer.timeout.connect(
3998 self.animate_picking)
4000 self.picking_timer.setInterval(50)
4001 self.picking_timer.start()
4003 def update_picking(self, x, y, doshift=False):
4004 if self.picking:
4005 mouset = self.time_projection.rev(x)
4006 dt = 0.0
4007 if mouset < self.tmin or mouset > self.tmax:
4008 if mouset < self.tmin:
4009 dt = -(self.tmin - mouset)
4010 else:
4011 dt = mouset - self.tmax
4012 ddt = self.tmax-self.tmin
4013 dt = max(dt, -ddt/10.)
4014 dt = min(dt, ddt/10.)
4016 x0 = x
4017 if self.picking_down is not None:
4018 x0 = self.time_projection(self.picking_down[0])
4020 w = abs(x-x0)
4021 x0 = min(x0, x)
4023 tmin, tmax = (
4024 self.time_projection.rev(x0),
4025 self.time_projection.rev(x0+w))
4027 tmin, tmax = (
4028 max(working_system_time_range[0], tmin),
4029 min(working_system_time_range[1], tmax))
4031 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
4033 self.picking.setGeometry(
4034 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
4036 ftrack = self.track_to_screen.rev(y)
4037 nslc_ids = self.get_nslc_ids_for_track(ftrack)
4038 self.floating_marker.set(nslc_ids, tmin, tmax)
4040 if dt != 0.0 and doshift:
4041 self.interrupt_following()
4042 self.set_time_range(self.tmin+dt, self.tmax+dt)
4044 self.update()
4046 def update_status(self):
4048 point = self.mapFromGlobal(qg.QCursor.pos())
4050 mouse_t = self.time_projection.rev(point.x())
4051 if not is_working_time(mouse_t):
4052 return
4054 if self.floating_marker:
4055 tmi, tma = (
4056 self.floating_marker.tmin,
4057 self.floating_marker.tmax)
4059 tt, ms = gmtime_x(tmi)
4061 if tmi == tma:
4062 message = mystrftime(
4063 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4064 tt=tt, milliseconds=ms)
4065 else:
4066 srange = '%g s' % (tma-tmi)
4067 message = mystrftime(
4068 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4069 tt=tt, milliseconds=ms)
4070 else:
4071 tt, ms = gmtime_x(mouse_t)
4073 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4075 sb = self.window().statusBar()
4076 sb.clearMessage()
4077 sb.showMessage(message)
4079 def set_sortingmode_change_delay_time(self, dt):
4080 self.sortingmode_change_delay_time = dt
4082 def sortingmode_change_delayed(self):
4083 now = time.time()
4084 return (
4085 self.sortingmode_change_delay_time is not None
4086 and now - self.sortingmode_change_time
4087 < self.sortingmode_change_delay_time)
4089 def set_visible_marker_kinds(self, kinds):
4090 self.deselect_all()
4091 self.visible_marker_kinds = tuple(kinds)
4092 self.emit_selected_markers()
4094 def following(self):
4095 return self.follow_timer is not None \
4096 and not self.following_interrupted()
4098 def interrupt_following(self):
4099 self.interactive_range_change_time = time.time()
4101 def following_interrupted(self, now=None):
4102 if now is None:
4103 now = time.time()
4104 return now - self.interactive_range_change_time \
4105 < self.interactive_range_change_delay_time
4107 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4108 if tmax_start is None:
4109 tmax_start = time.time()
4110 self.show_all = False
4111 self.follow_time = tlen
4112 self.follow_timer = qc.QTimer(self)
4113 self.follow_timer.timeout.connect(
4114 self.follow_update)
4115 self.follow_timer.setInterval(interval)
4116 self.follow_timer.start()
4117 self.follow_started = time.time()
4118 self.follow_lapse = lapse
4119 self.follow_tshift = self.follow_started - tmax_start
4120 self.interactive_range_change_time = 0.0
4122 def unfollow(self):
4123 if self.follow_timer is not None:
4124 self.follow_timer.stop()
4125 self.follow_timer = None
4126 self.interactive_range_change_time = 0.0
4128 def follow_update(self):
4129 rnow = time.time()
4130 if self.follow_lapse is None:
4131 now = rnow
4132 else:
4133 now = self.follow_started + (rnow - self.follow_started) \
4134 * self.follow_lapse
4136 if self.following_interrupted(rnow):
4137 return
4138 self.set_time_range(
4139 now-self.follow_time-self.follow_tshift,
4140 now-self.follow_tshift)
4142 self.update()
4144 def myclose(self, return_tag=''):
4145 self.return_tag = return_tag
4146 self.window().close()
4148 def cleanup(self):
4149 self.about_to_close.emit()
4150 self.timer.stop()
4151 if self.follow_timer is not None:
4152 self.follow_timer.stop()
4154 for snuffling in list(self.snufflings):
4155 self.remove_snuffling(snuffling)
4157 def inputline_changed(self, text):
4158 pass
4160 def inputline_finished(self, text):
4161 line = str(text)
4163 toks = line.split()
4164 clearit, hideit, error = False, True, None
4165 if len(toks) >= 1:
4166 command = toks[0].lower()
4168 try:
4169 quick_filter_commands = {
4170 'n': '%s.*.*.*',
4171 's': '*.%s.*.*',
4172 'l': '*.*.%s.*',
4173 'c': '*.*.*.%s'}
4175 if command in quick_filter_commands:
4176 if len(toks) >= 2:
4177 patterns = [
4178 quick_filter_commands[toks[0]] % pat
4179 for pat in toks[1:]]
4180 self.set_quick_filter_patterns(patterns, line)
4181 else:
4182 self.set_quick_filter_patterns(None)
4184 self.update()
4186 elif command in ('hide', 'unhide'):
4187 if len(toks) >= 2:
4188 patterns = []
4189 if len(toks) == 2:
4190 patterns = [toks[1]]
4191 elif len(toks) >= 3:
4192 x = {
4193 'n': '%s.*.*.*',
4194 's': '*.%s.*.*',
4195 'l': '*.*.%s.*',
4196 'c': '*.*.*.%s'}
4198 if toks[1] in x:
4199 patterns.extend(
4200 x[toks[1]] % tok for tok in toks[2:])
4202 for pattern in patterns:
4203 if command == 'hide':
4204 self.add_blacklist_pattern(pattern)
4205 else:
4206 self.remove_blacklist_pattern(pattern)
4208 elif command == 'unhide' and len(toks) == 1:
4209 self.clear_blacklist()
4211 clearit = True
4213 self.update()
4215 elif command == 'markers':
4216 if len(toks) == 2:
4217 if toks[1] == 'all':
4218 kinds = self.all_marker_kinds
4219 else:
4220 kinds = []
4221 for x in toks[1]:
4222 try:
4223 kinds.append(int(x))
4224 except Exception:
4225 pass
4227 self.set_visible_marker_kinds(kinds)
4229 elif len(toks) == 1:
4230 self.set_visible_marker_kinds(())
4232 self.update()
4234 elif command == 'scaling':
4235 if len(toks) == 2:
4236 hideit = False
4237 error = 'wrong number of arguments'
4239 if len(toks) >= 3:
4240 vmin, vmax = [
4241 pyrocko.model.float_or_none(x)
4242 for x in toks[-2:]]
4244 def upd(d, k, vmin, vmax):
4245 if k in d:
4246 if vmin is not None:
4247 d[k] = vmin, d[k][1]
4248 if vmax is not None:
4249 d[k] = d[k][0], vmax
4251 if len(toks) == 1:
4252 self.remove_scaling_hooks()
4254 elif len(toks) == 3:
4255 def hook(data_ranges):
4256 for k in data_ranges:
4257 upd(data_ranges, k, vmin, vmax)
4259 self.set_scaling_hook('_', hook)
4261 elif len(toks) == 4:
4262 pattern = toks[1]
4264 def hook(data_ranges):
4265 for k in pyrocko.util.match_nslcs(
4266 pattern, list(data_ranges.keys())):
4268 upd(data_ranges, k, vmin, vmax)
4270 self.set_scaling_hook(pattern, hook)
4272 elif command == 'goto':
4273 toks2 = line.split(None, 1)
4274 if len(toks2) == 2:
4275 arg = toks2[1]
4276 m = re.match(
4277 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4278 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4279 if m:
4280 tlen = None
4281 if not m.group(1):
4282 tlen = 12*32*24*60*60
4283 elif not m.group(2):
4284 tlen = 32*24*60*60
4285 elif not m.group(3):
4286 tlen = 24*60*60
4287 elif not m.group(4):
4288 tlen = 60*60
4289 elif not m.group(5):
4290 tlen = 60
4292 supl = '1970-01-01 00:00:00'
4293 if len(supl) > len(arg):
4294 arg = arg + supl[-(len(supl)-len(arg)):]
4295 t = pyrocko.util.str_to_time(arg)
4296 self.go_to_time(t, tlen=tlen)
4298 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4299 supl = '00:00:00'
4300 if len(supl) > len(arg):
4301 arg = arg + supl[-(len(supl)-len(arg)):]
4302 tmin, tmax = self.get_time_range()
4303 sdate = pyrocko.util.time_to_str(
4304 tmin/2.+tmax/2., format='%Y-%m-%d')
4305 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4306 self.go_to_time(t)
4308 elif arg == 'today':
4309 self.go_to_time(
4310 day_start(
4311 time.time()), tlen=24*60*60)
4313 elif arg == 'yesterday':
4314 self.go_to_time(
4315 day_start(
4316 time.time()-24*60*60), tlen=24*60*60)
4318 else:
4319 self.go_to_event_by_name(arg)
4321 else:
4322 raise PileViewerMainException(
4323 'No such command: %s' % command)
4325 except PileViewerMainException as e:
4326 error = str(e)
4327 hideit = False
4329 return clearit, hideit, error
4331 return PileViewerMain
4334PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4335GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4338class LineEditWithAbort(qw.QLineEdit):
4340 aborted = qc.pyqtSignal()
4341 history_down = qc.pyqtSignal()
4342 history_up = qc.pyqtSignal()
4344 def keyPressEvent(self, key_event):
4345 if key_event.key() == qc.Qt.Key_Escape:
4346 self.aborted.emit()
4347 elif key_event.key() == qc.Qt.Key_Down:
4348 self.history_down.emit()
4349 elif key_event.key() == qc.Qt.Key_Up:
4350 self.history_up.emit()
4351 else:
4352 return qw.QLineEdit.keyPressEvent(self, key_event)
4355class PileViewer(qw.QFrame):
4356 '''
4357 PileViewerMain + Controls + Inputline
4358 '''
4360 def __init__(
4361 self, pile,
4362 ntracks_shown_max=20,
4363 marker_editor_sortable=True,
4364 use_opengl=None,
4365 panel_parent=None,
4366 *args):
4368 qw.QFrame.__init__(self, *args)
4370 layout = qw.QGridLayout()
4371 layout.setContentsMargins(0, 0, 0, 0)
4372 layout.setSpacing(0)
4374 self.menu = PileViewerMenuBar(self)
4376 if use_opengl is None:
4377 use_opengl = is_macos
4379 if use_opengl:
4380 self.viewer = GLPileViewerMain(
4381 pile,
4382 ntracks_shown_max=ntracks_shown_max,
4383 panel_parent=panel_parent,
4384 menu=self.menu)
4385 else:
4386 self.viewer = PileViewerMain(
4387 pile,
4388 ntracks_shown_max=ntracks_shown_max,
4389 panel_parent=panel_parent,
4390 menu=self.menu)
4392 self.marker_editor_sortable = marker_editor_sortable
4394 # self.setFrameShape(qw.QFrame.StyledPanel)
4395 # self.setFrameShadow(qw.QFrame.Sunken)
4397 self.input_area = qw.QFrame(self)
4398 ia_layout = qw.QGridLayout()
4399 ia_layout.setContentsMargins(11, 11, 11, 11)
4400 self.input_area.setLayout(ia_layout)
4402 self.inputline = LineEditWithAbort(self.input_area)
4403 self.inputline.returnPressed.connect(
4404 self.inputline_returnpressed)
4405 self.inputline.editingFinished.connect(
4406 self.inputline_finished)
4407 self.inputline.aborted.connect(
4408 self.inputline_aborted)
4410 self.inputline.history_down.connect(
4411 lambda: self.step_through_history(1))
4412 self.inputline.history_up.connect(
4413 lambda: self.step_through_history(-1))
4415 self.inputline.textEdited.connect(
4416 self.inputline_changed)
4418 self.inputline.setPlaceholderText(
4419 u"Quick commands: e.g. 'c HH?' to select channels. "
4420 u'Use ↑ or ↓ to navigate.')
4421 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4422 self.input_area.hide()
4423 self.history = None
4425 self.inputline_error_str = None
4427 self.inputline_error = qw.QLabel()
4428 self.inputline_error.hide()
4430 ia_layout.addWidget(self.inputline, 0, 0)
4431 ia_layout.addWidget(self.inputline_error, 1, 0)
4432 layout.addWidget(self.input_area, 0, 0, 1, 2)
4433 layout.addWidget(self.viewer, 1, 0)
4435 pb = Progressbars(self)
4436 layout.addWidget(pb, 2, 0, 1, 2)
4437 self.progressbars = pb
4439 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4440 self.scrollbar = scrollbar
4441 layout.addWidget(scrollbar, 1, 1)
4442 self.scrollbar.valueChanged.connect(
4443 self.scrollbar_changed)
4445 self.block_scrollbar_changes = False
4447 self.viewer.want_input.connect(
4448 self.inputline_show)
4449 self.viewer.toggle_input.connect(
4450 self.inputline_toggle)
4451 self.viewer.tracks_range_changed.connect(
4452 self.tracks_range_changed)
4453 self.viewer.pile_has_changed_signal.connect(
4454 self.adjust_controls)
4455 self.viewer.about_to_close.connect(
4456 self.save_inputline_history)
4458 self.setLayout(layout)
4460 def cleanup(self):
4461 self.viewer.cleanup()
4463 def get_progressbars(self):
4464 return self.progressbars
4466 def inputline_show(self):
4467 if not self.history:
4468 self.load_inputline_history()
4470 self.input_area.show()
4471 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4472 self.inputline.selectAll()
4474 def inputline_set_error(self, string):
4475 self.inputline_error_str = string
4476 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4477 self.inputline.selectAll()
4478 self.inputline_error.setText(string)
4479 self.input_area.show()
4480 self.inputline_error.show()
4482 def inputline_clear_error(self):
4483 if self.inputline_error_str:
4484 self.inputline.setPalette(qw.QApplication.palette())
4485 self.inputline_error_str = None
4486 self.inputline_error.clear()
4487 self.inputline_error.hide()
4489 def inputline_changed(self, line):
4490 self.viewer.inputline_changed(str(line))
4491 self.inputline_clear_error()
4493 def inputline_returnpressed(self):
4494 line = str(self.inputline.text())
4495 clearit, hideit, error = self.viewer.inputline_finished(line)
4497 if error:
4498 self.inputline_set_error(error)
4500 line = line.strip()
4502 if line != '' and not error:
4503 if not (len(self.history) >= 1 and line == self.history[-1]):
4504 self.history.append(line)
4506 if clearit:
4508 self.inputline.blockSignals(True)
4509 qpat, qinp = self.viewer.get_quick_filter_patterns()
4510 if qpat is None:
4511 self.inputline.clear()
4512 else:
4513 self.inputline.setText(qinp)
4514 self.inputline.blockSignals(False)
4516 if hideit and not error:
4517 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4518 self.input_area.hide()
4520 self.hist_ind = len(self.history)
4522 def inputline_aborted(self):
4523 '''
4524 Hide the input line.
4525 '''
4526 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4527 self.hist_ind = len(self.history)
4528 self.input_area.hide()
4530 def inputline_toggle(self):
4531 if self.input_area.isVisible():
4532 self.inputline_aborted()
4533 else:
4534 self.inputline_show()
4536 def save_inputline_history(self):
4537 '''
4538 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4539 '''
4540 if not self.history:
4541 return
4543 conf = pyrocko.config
4544 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4545 with open(fn_hist, 'w') as f:
4546 i = min(100, len(self.history))
4547 for c in self.history[-i:]:
4548 f.write('%s\n' % c)
4550 def load_inputline_history(self):
4551 '''
4552 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4553 '''
4554 conf = pyrocko.config
4555 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4556 if not os.path.exists(fn_hist):
4557 with open(fn_hist, 'w+') as f:
4558 f.write('\n')
4560 with open(fn_hist, 'r') as f:
4561 self.history = [line.strip() for line in f.readlines()]
4563 self.hist_ind = len(self.history)
4565 def step_through_history(self, ud=1):
4566 '''
4567 Step through input line history and set the input line text.
4568 '''
4569 n = len(self.history)
4570 self.hist_ind += ud
4571 self.hist_ind %= (n + 1)
4572 if len(self.history) != 0 and self.hist_ind != n:
4573 self.inputline.setText(self.history[self.hist_ind])
4574 else:
4575 self.inputline.setText('')
4577 def inputline_finished(self):
4578 pass
4580 def tracks_range_changed(self, ntracks, ilo, ihi):
4581 if self.block_scrollbar_changes:
4582 return
4584 self.scrollbar.blockSignals(True)
4585 self.scrollbar.setPageStep(ihi-ilo)
4586 vmax = max(0, ntracks-(ihi-ilo))
4587 self.scrollbar.setRange(0, vmax)
4588 self.scrollbar.setValue(ilo)
4589 self.scrollbar.setHidden(vmax == 0)
4590 self.scrollbar.blockSignals(False)
4592 def scrollbar_changed(self, value):
4593 self.block_scrollbar_changes = True
4594 ilo = value
4595 ihi = ilo + self.scrollbar.pageStep()
4596 self.viewer.set_tracks_range((ilo, ihi))
4597 self.block_scrollbar_changes = False
4598 self.update_contents()
4600 def controls(self):
4601 frame = qw.QFrame(self)
4602 layout = qw.QGridLayout()
4603 frame.setLayout(layout)
4605 minfreq = 0.001
4606 maxfreq = 1000.0
4607 self.lowpass_control = ValControl(high_is_none=True)
4608 self.lowpass_control.setup(
4609 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4610 self.highpass_control = ValControl(low_is_none=True)
4611 self.highpass_control.setup(
4612 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4613 self.gain_control = ValControl()
4614 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4615 self.rot_control = LinValControl()
4616 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4617 self.colorbar_control = ColorbarControl(self)
4619 self.lowpass_control.valchange.connect(
4620 self.viewer.lowpass_change)
4621 self.highpass_control.valchange.connect(
4622 self.viewer.highpass_change)
4623 self.gain_control.valchange.connect(
4624 self.viewer.gain_change)
4625 self.rot_control.valchange.connect(
4626 self.viewer.rot_change)
4627 self.colorbar_control.cmap_changed.connect(
4628 self.viewer.waterfall_cmap_change
4629 )
4630 self.colorbar_control.clip_changed.connect(
4631 self.viewer.waterfall_clip_change
4632 )
4633 self.colorbar_control.show_absolute_toggled.connect(
4634 self.viewer.waterfall_show_absolute_change
4635 )
4636 self.colorbar_control.show_integrate_toggled.connect(
4637 self.viewer.waterfall_set_integrate
4638 )
4640 for icontrol, control in enumerate((
4641 self.highpass_control,
4642 self.lowpass_control,
4643 self.gain_control,
4644 self.rot_control,
4645 self.colorbar_control)):
4647 for iwidget, widget in enumerate(control.widgets()):
4648 layout.addWidget(widget, icontrol, iwidget)
4650 spacer = qw.QSpacerItem(
4651 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4652 layout.addItem(spacer, 4, 0, 1, 3)
4654 self.adjust_controls()
4655 self.viewer.viewmode_change(ViewMode.Wiggle)
4656 return frame
4658 def marker_editor(self):
4659 editor = pyrocko.gui.snuffler.marker_editor.MarkerEditor(
4660 self, sortable=self.marker_editor_sortable)
4662 editor.set_viewer(self.get_view())
4663 editor.get_marker_model().dataChanged.connect(
4664 self.update_contents)
4665 return editor
4667 def adjust_controls(self):
4668 dtmin, dtmax = self.viewer.content_deltat_range()
4669 maxfreq = 0.5/dtmin
4670 minfreq = (0.5/dtmax)*0.0001
4671 self.lowpass_control.set_range(minfreq, maxfreq)
4672 self.highpass_control.set_range(minfreq, maxfreq)
4674 def setup_snufflings(self):
4675 self.viewer.setup_snufflings()
4677 def get_view(self):
4678 return self.viewer
4680 def update_contents(self):
4681 self.viewer.update()
4683 def get_pile(self):
4684 return self.viewer.get_pile()
4686 def show_colorbar_ctrl(self, show):
4687 for w in self.colorbar_control.widgets():
4688 w.setVisible(show)
4690 def show_gain_ctrl(self, show):
4691 for w in self.gain_control.widgets():
4692 w.setVisible(show)