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 PileViewerMenu(qw.QMenu):
773 ...
776def MakePileViewerMainClass(base):
778 class PileViewerMain(base):
780 want_input = qc.pyqtSignal()
781 about_to_close = qc.pyqtSignal()
782 pile_has_changed_signal = qc.pyqtSignal()
783 tracks_range_changed = qc.pyqtSignal(int, int, int)
785 begin_markers_add = qc.pyqtSignal(int, int)
786 end_markers_add = qc.pyqtSignal()
787 begin_markers_remove = qc.pyqtSignal(int, int)
788 end_markers_remove = qc.pyqtSignal()
790 marker_selection_changed = qc.pyqtSignal(list)
791 active_event_marker_changed = qc.pyqtSignal()
793 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
794 menu=None):
795 base.__init__(self, *args)
797 self.pile = pile
798 self.ax_height = 80
799 self.panel_parent = panel_parent
801 self.click_tolerance = 5
803 self.ntracks_shown_max = ntracks_shown_max
804 self.initial_ntracks_shown_max = ntracks_shown_max
805 self.ntracks = 0
806 self.show_all = True
807 self.shown_tracks_range = None
808 self.track_start = None
809 self.track_trange = None
811 self.lowpass = None
812 self.highpass = None
813 self.gain = 1.0
814 self.rotate = 0.0
815 self.picking_down = None
816 self.picking = None
817 self.floating_marker = None
818 self.markers = pyrocko.pile.Sorted([], 'tmin')
819 self.markers_deltat_max = 0.
820 self.n_selected_markers = 0
821 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
822 self.visible_marker_kinds = self.all_marker_kinds
823 self.active_event_marker = None
824 self.ignore_releases = 0
825 self.message = None
826 self.reloaded = False
827 self.pile_has_changed = False
828 self.config = pyrocko.config.config('snuffler')
830 self.tax = TimeAx()
831 self.setBackgroundRole(qg.QPalette.Base)
832 self.setAutoFillBackground(True)
833 poli = qw.QSizePolicy(
834 qw.QSizePolicy.Expanding,
835 qw.QSizePolicy.Expanding)
837 self.setSizePolicy(poli)
838 self.setMinimumSize(300, 200)
839 self.setFocusPolicy(qc.Qt.ClickFocus)
841 self.menu = menu or PileViewerMenu(self)
843 file_menu = self.menu.addMenu('&File')
844 view_menu = self.menu.addMenu('&View')
845 options_menu = self.menu.addMenu('&Options')
846 scale_menu = self.menu.addMenu('&Scaling')
847 sort_menu = self.menu.addMenu('Sor&ting')
848 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
850 help_menu = self.menu.addMenu('&Help')
852 self.snufflings_menu = self.toggle_panel_menu.addMenu(
853 'Run Snuffling')
854 self.toggle_panel_menu.addSeparator()
855 self.snuffling_help = help_menu.addMenu('Snuffling Help')
856 help_menu.addSeparator()
858 file_menu.addAction(
859 qg.QIcon.fromTheme('document-open'),
860 'Open waveform files...',
861 self.open_waveforms,
862 qg.QKeySequence.Open)
864 file_menu.addAction(
865 qg.QIcon.fromTheme('document-open'),
866 'Open waveform directory...',
867 self.open_waveform_directory)
869 file_menu.addAction(
870 'Open station files...',
871 self.open_stations)
873 file_menu.addAction(
874 'Open StationXML files...',
875 self.open_stations_xml)
877 file_menu.addAction(
878 'Open event file...',
879 self.read_events)
881 file_menu.addSeparator()
882 file_menu.addAction(
883 'Open marker file...',
884 self.read_markers)
886 file_menu.addAction(
887 qg.QIcon.fromTheme('document-save'),
888 'Save markers...',
889 self.write_markers,
890 qg.QKeySequence.Save)
892 file_menu.addAction(
893 qg.QIcon.fromTheme('document-save-as'),
894 'Save selected markers...',
895 self.write_selected_markers,
896 qg.QKeySequence.SaveAs)
898 file_menu.addSeparator()
899 file_menu.addAction(
900 qg.QIcon.fromTheme('document-print'),
901 'Print',
902 self.printit,
903 qg.QKeySequence.Print)
905 file_menu.addAction(
906 qg.QIcon.fromTheme('insert-image'),
907 'Save as SVG or PNG',
908 self.savesvg,
909 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
911 file_menu.addSeparator()
912 close = file_menu.addAction(
913 qg.QIcon.fromTheme('window-close'),
914 'Close',
915 self.myclose)
916 close.setShortcuts(
917 (qg.QKeySequence(qc.Qt.Key_Q),
918 qg.QKeySequence(qc.Qt.Key_X)))
920 # Scale Menu
921 menudef = [
922 ('Individual Scale',
923 lambda tr: tr.nslc_id,
924 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
925 ('Common Scale',
926 lambda tr: None,
927 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
928 ('Common Scale per Station',
929 lambda tr: (tr.network, tr.station),
930 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
931 ('Common Scale per Station Location',
932 lambda tr: (tr.network, tr.station, tr.location)),
933 ('Common Scale per Component',
934 lambda tr: (tr.channel)),
935 ]
937 self.menuitems_scaling = add_radiobuttongroup(
938 scale_menu, menudef, self.scalingmode_change,
939 default=self.config.trace_scale)
940 scale_menu.addSeparator()
942 self.scaling_key = self.menuitems_scaling[0][1]
943 self.scaling_hooks = {}
944 self.scalingmode_change()
946 menudef = [
947 ('Scaling based on Minimum and Maximum',
948 ('minmax', 'minmax')),
949 ('Scaling based on Minimum and Maximum (Robust)',
950 ('minmax', 'robust')),
951 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
952 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
953 ]
955 self.menuitems_scaling_base = add_radiobuttongroup(
956 scale_menu, menudef, self.scaling_base_change)
958 self.scaling_base = self.menuitems_scaling_base[0][1]
959 scale_menu.addSeparator()
961 self.menuitem_fixscalerange = scale_menu.addAction(
962 'Fix Scale Ranges')
963 self.menuitem_fixscalerange.setCheckable(True)
965 # Sort Menu
966 def sector_dist(sta):
967 if sta.dist_m is None:
968 return None, None
969 else:
970 return (
971 sector_int(round((sta.azimuth+15.)/30.)),
972 m_float(sta.dist_m))
974 menudef = [
975 ('Sort by Names',
976 lambda tr: (),
977 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
978 ('Sort by Distance',
979 lambda tr: self.station_attrib(
980 tr,
981 lambda sta: (m_float_or_none(sta.dist_m),),
982 lambda tr: (None,)),
983 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
984 ('Sort by Azimuth',
985 lambda tr: self.station_attrib(
986 tr,
987 lambda sta: (deg_float_or_none(sta.azimuth),),
988 lambda tr: (None,))),
989 ('Sort by Distance in 12 Azimuthal Blocks',
990 lambda tr: self.station_attrib(
991 tr,
992 sector_dist,
993 lambda tr: (None, None))),
994 ('Sort by Backazimuth',
995 lambda tr: self.station_attrib(
996 tr,
997 lambda sta: (deg_float_or_none(sta.backazimuth),),
998 lambda tr: (None,))),
999 ]
1000 self.menuitems_ssorting = add_radiobuttongroup(
1001 sort_menu, menudef, self.s_sortingmode_change)
1002 sort_menu.addSeparator()
1004 self._ssort = lambda tr: ()
1006 self.menu.addSeparator()
1008 menudef = [
1009 ('Subsort by Network, Station, Location, Channel',
1010 ((0, 1, 2, 3), # gathering
1011 lambda tr: tr.location)), # coloring
1012 ('Subsort by Network, Station, Channel, Location',
1013 ((0, 1, 3, 2),
1014 lambda tr: tr.channel)),
1015 ('Subsort by Station, Network, Channel, Location',
1016 ((1, 0, 3, 2),
1017 lambda tr: tr.channel)),
1018 ('Subsort by Location, Network, Station, Channel',
1019 ((2, 0, 1, 3),
1020 lambda tr: tr.channel)),
1021 ('Subsort by Channel, Network, Station, Location',
1022 ((3, 0, 1, 2),
1023 lambda tr: (tr.network, tr.station, tr.location))),
1024 ('Subsort by Network, Station, Channel (Grouped by Location)',
1025 ((0, 1, 3),
1026 lambda tr: tr.location)),
1027 ('Subsort by Station, Network, Channel (Grouped by Location)',
1028 ((1, 0, 3),
1029 lambda tr: tr.location)),
1030 ]
1032 self.menuitems_sorting = add_radiobuttongroup(
1033 sort_menu, menudef, self.sortingmode_change)
1035 menudef = [(x.key, x.value) for x in
1036 self.config.visible_length_setting]
1038 # View menu
1039 self.menuitems_visible_length = add_radiobuttongroup(
1040 view_menu, menudef,
1041 self.visible_length_change)
1042 view_menu.addSeparator()
1044 view_modes = [
1045 ('Wiggle Plot', ViewMode.Wiggle),
1046 ('Waterfall', ViewMode.Waterfall)
1047 ]
1049 self.menuitems_viewmode = add_radiobuttongroup(
1050 view_menu, view_modes,
1051 self.viewmode_change, default=ViewMode.Wiggle)
1052 view_menu.addSeparator()
1054 self.menuitem_cliptraces = view_menu.addAction(
1055 'Clip Traces')
1056 self.menuitem_cliptraces.setCheckable(True)
1057 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1059 self.menuitem_showboxes = view_menu.addAction(
1060 'Show Boxes')
1061 self.menuitem_showboxes.setCheckable(True)
1062 self.menuitem_showboxes.setChecked(
1063 self.config.show_boxes)
1065 self.menuitem_colortraces = view_menu.addAction(
1066 'Color Traces')
1067 self.menuitem_colortraces.setCheckable(True)
1068 self.menuitem_antialias = view_menu.addAction(
1069 'Antialiasing')
1070 self.menuitem_antialias.setCheckable(True)
1072 view_menu.addSeparator()
1073 self.menuitem_showscalerange = view_menu.addAction(
1074 'Show Scale Ranges')
1075 self.menuitem_showscalerange.setCheckable(True)
1076 self.menuitem_showscalerange.setChecked(
1077 self.config.show_scale_ranges)
1079 self.menuitem_showscaleaxis = view_menu.addAction(
1080 'Show Scale Axes')
1081 self.menuitem_showscaleaxis.setCheckable(True)
1082 self.menuitem_showscaleaxis.setChecked(
1083 self.config.show_scale_axes)
1085 self.menuitem_showzeroline = view_menu.addAction(
1086 'Show Zero Lines')
1087 self.menuitem_showzeroline.setCheckable(True)
1089 view_menu.addSeparator()
1090 view_menu.addAction(
1091 qg.QIcon.fromTheme('view-fullscreen'),
1092 'Fullscreen',
1093 self.toggle_fullscreen,
1094 qg.QKeySequence(qc.Qt.Key_F11))
1096 # Options Menu
1097 self.menuitem_demean = options_menu.addAction('Demean')
1098 self.menuitem_demean.setCheckable(True)
1099 self.menuitem_demean.setChecked(self.config.demean)
1100 self.menuitem_demean.setShortcut(
1101 qg.QKeySequence(qc.Qt.Key_Underscore))
1103 self.menuitem_distances_3d = options_menu.addAction(
1104 '3D distances',
1105 self.distances_3d_changed)
1106 self.menuitem_distances_3d.setCheckable(True)
1108 self.menuitem_allowdownsampling = options_menu.addAction(
1109 'Allow Downsampling')
1110 self.menuitem_allowdownsampling.setCheckable(True)
1111 self.menuitem_allowdownsampling.setChecked(True)
1113 self.menuitem_degap = options_menu.addAction(
1114 'Allow Degapping')
1115 self.menuitem_degap.setCheckable(True)
1116 self.menuitem_degap.setChecked(True)
1118 options_menu.addSeparator()
1120 self.menuitem_fft_filtering = options_menu.addAction(
1121 'FFT Filtering')
1122 self.menuitem_fft_filtering.setCheckable(True)
1124 self.menuitem_lphp = options_menu.addAction(
1125 'Bandpass is Low- + Highpass')
1126 self.menuitem_lphp.setCheckable(True)
1127 self.menuitem_lphp.setChecked(True)
1129 options_menu.addSeparator()
1130 self.menuitem_watch = options_menu.addAction(
1131 'Watch Files')
1132 self.menuitem_watch.setCheckable(True)
1134 self.menuitem_liberal_fetch = options_menu.addAction(
1135 'Liberal Fetch Optimization')
1136 self.menuitem_liberal_fetch.setCheckable(True)
1138 self.visible_length = menudef[0][1]
1140 self.snufflings_menu.addAction(
1141 'Reload Snufflings',
1142 self.setup_snufflings)
1144 # Disable ShadowPileTest
1145 if False:
1146 test_action = self.menu.addAction(
1147 'Test',
1148 self.toggletest)
1149 test_action.setCheckable(True)
1151 help_menu.addAction(
1152 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1153 'Snuffler Controls',
1154 self.help,
1155 qg.QKeySequence(qc.Qt.Key_Question))
1157 help_menu.addAction(
1158 'About',
1159 self.about)
1161 self.time_projection = Projection()
1162 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1163 self.time_projection.set_out_range(0., self.width())
1165 self.gather = None
1167 self.trace_filter = None
1168 self.quick_filter = None
1169 self.quick_filter_patterns = None, None
1170 self.blacklist = []
1172 self.track_to_screen = Projection()
1173 self.track_to_nslc_ids = {}
1175 self.cached_vec = None
1176 self.cached_processed_traces = None
1178 self.timer = qc.QTimer(self)
1179 self.timer.timeout.connect(self.periodical)
1180 self.timer.setInterval(1000)
1181 self.timer.start()
1183 self._pile_changed = self.pile_changed # need to keep a strong ref
1184 self.pile.add_listener(self._pile_changed)
1186 self.trace_styles = {}
1187 if self.get_squirrel() is None:
1188 self.determine_box_styles()
1190 self.setMouseTracking(True)
1192 user_home_dir = os.path.expanduser('~')
1193 self.snuffling_modules = {}
1194 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1195 self.default_snufflings = None
1196 self.snufflings = []
1198 self.stations = {}
1200 self.timer_draw = Timer()
1201 self.timer_cutout = Timer()
1202 self.time_spent_painting = 0.0
1203 self.time_last_painted = time.time()
1205 self.interactive_range_change_time = 0.0
1206 self.interactive_range_change_delay_time = 10.0
1207 self.follow_timer = None
1209 self.sortingmode_change_time = 0.0
1210 self.sortingmode_change_delay_time = None
1212 self.old_data_ranges = {}
1214 self.error_messages = {}
1215 self.return_tag = None
1216 self.wheel_pos = 60
1218 self.setAcceptDrops(True)
1219 self._paths_to_load = []
1221 self.tf_cache = {}
1223 self.waterfall = TraceWaterfall()
1224 self.waterfall_cmap = 'viridis'
1225 self.waterfall_clip_min = 0.
1226 self.waterfall_clip_max = 1.
1227 self.waterfall_show_absolute = False
1228 self.waterfall_integrate = False
1229 self.view_mode = ViewMode.Wiggle
1231 self.automatic_updates = True
1233 self.closing = False
1234 self.in_paint_event = False
1236 def fail(self, reason):
1237 box = qw.QMessageBox(self)
1238 box.setText(reason)
1239 box.exec_()
1241 def set_trace_filter(self, filter_func):
1242 self.trace_filter = filter_func
1243 self.sortingmode_change()
1245 def update_trace_filter(self):
1246 if self.blacklist:
1248 def blacklist_func(tr):
1249 return not pyrocko.util.match_nslc(
1250 self.blacklist, tr.nslc_id)
1252 else:
1253 blacklist_func = None
1255 if self.quick_filter is None and blacklist_func is None:
1256 self.set_trace_filter(None)
1257 elif self.quick_filter is None:
1258 self.set_trace_filter(blacklist_func)
1259 elif blacklist_func is None:
1260 self.set_trace_filter(self.quick_filter)
1261 else:
1262 self.set_trace_filter(
1263 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1265 def set_quick_filter(self, filter_func):
1266 self.quick_filter = filter_func
1267 self.update_trace_filter()
1269 def set_quick_filter_patterns(self, patterns, inputline=None):
1270 if patterns is not None:
1271 self.set_quick_filter(
1272 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1273 else:
1274 self.set_quick_filter(None)
1276 self.quick_filter_patterns = patterns, inputline
1278 def get_quick_filter_patterns(self):
1279 return self.quick_filter_patterns
1281 def add_blacklist_pattern(self, pattern):
1282 if pattern == 'empty':
1283 keys = set(self.pile.nslc_ids)
1284 trs = self.pile.all(
1285 tmin=self.tmin,
1286 tmax=self.tmax,
1287 load_data=False,
1288 degap=False)
1290 for tr in trs:
1291 if tr.nslc_id in keys:
1292 keys.remove(tr.nslc_id)
1294 for key in keys:
1295 xpattern = '.'.join(key)
1296 if xpattern not in self.blacklist:
1297 self.blacklist.append(xpattern)
1299 else:
1300 if pattern in self.blacklist:
1301 self.blacklist.remove(pattern)
1303 self.blacklist.append(pattern)
1305 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1306 self.update_trace_filter()
1308 def remove_blacklist_pattern(self, pattern):
1309 if pattern in self.blacklist:
1310 self.blacklist.remove(pattern)
1311 else:
1312 raise PileViewerMainException(
1313 'Pattern not found in blacklist.')
1315 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1316 self.update_trace_filter()
1318 def clear_blacklist(self):
1319 self.blacklist = []
1320 self.update_trace_filter()
1322 def ssort(self, tr):
1323 return self._ssort(tr)
1325 def station_key(self, x):
1326 return x.network, x.station
1328 def station_keys(self, x):
1329 return [
1330 (x.network, x.station, x.location),
1331 (x.network, x.station)]
1333 def station_attrib(self, tr, getter, default_getter):
1334 for sk in self.station_keys(tr):
1335 if sk in self.stations:
1336 station = self.stations[sk]
1337 return getter(station)
1339 return default_getter(tr)
1341 def get_station(self, sk):
1342 return self.stations[sk]
1344 def has_station(self, station):
1345 for sk in self.station_keys(station):
1346 if sk in self.stations:
1347 return True
1349 return False
1351 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1352 return self.station_attrib(
1353 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1355 def set_stations(self, stations):
1356 self.stations = {}
1357 self.add_stations(stations)
1359 def add_stations(self, stations):
1360 for station in stations:
1361 for sk in self.station_keys(station):
1362 self.stations[sk] = station
1364 ev = self.get_active_event()
1365 if ev:
1366 self.set_origin(ev)
1368 def add_event(self, event):
1369 marker = EventMarker(event)
1370 self.add_marker(marker)
1372 def add_events(self, events):
1373 markers = [EventMarker(e) for e in events]
1374 self.add_markers(markers)
1376 def set_event_marker_as_origin(self, ignore=None):
1377 selected = self.selected_markers()
1378 if not selected:
1379 self.fail('An event marker must be selected.')
1380 return
1382 m = selected[0]
1383 if not isinstance(m, EventMarker):
1384 self.fail('Selected marker is not an event.')
1385 return
1387 self.set_active_event_marker(m)
1389 def deactivate_event_marker(self):
1390 if self.active_event_marker:
1391 self.active_event_marker.active = False
1393 self.active_event_marker_changed.emit()
1394 self.active_event_marker = None
1396 def set_active_event_marker(self, event_marker):
1397 if self.active_event_marker:
1398 self.active_event_marker.active = False
1400 self.active_event_marker = event_marker
1401 event_marker.active = True
1402 event = event_marker.get_event()
1403 self.set_origin(event)
1404 self.active_event_marker_changed.emit()
1406 def set_active_event(self, event):
1407 for marker in self.markers:
1408 if isinstance(marker, EventMarker):
1409 if marker.get_event() is event:
1410 self.set_active_event_marker(marker)
1412 def get_active_event_marker(self):
1413 return self.active_event_marker
1415 def get_active_event(self):
1416 m = self.get_active_event_marker()
1417 if m is not None:
1418 return m.get_event()
1419 else:
1420 return None
1422 def get_active_markers(self):
1423 emarker = self.get_active_event_marker()
1424 if emarker is None:
1425 return None, []
1427 else:
1428 ev = emarker.get_event()
1429 pmarkers = [
1430 m for m in self.markers
1431 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1433 return emarker, pmarkers
1435 def set_origin(self, location):
1436 for station in self.stations.values():
1437 station.set_event_relative_data(
1438 location,
1439 distance_3d=self.menuitem_distances_3d.isChecked())
1441 self.sortingmode_change()
1443 def distances_3d_changed(self):
1444 ignore = self.menuitem_distances_3d.isChecked()
1445 self.set_event_marker_as_origin(ignore)
1447 def iter_snuffling_modules(self):
1448 pjoin = os.path.join
1449 for path in self.snuffling_paths:
1451 if not os.path.isdir(path):
1452 os.mkdir(path)
1454 for entry in os.listdir(path):
1455 directory = path
1456 fn = entry
1457 d = pjoin(path, entry)
1458 if os.path.isdir(d):
1459 directory = d
1460 if os.path.isfile(
1461 os.path.join(directory, 'snuffling.py')):
1462 fn = 'snuffling.py'
1464 if not fn.endswith('.py'):
1465 continue
1467 name = fn[:-3]
1469 if (directory, name) not in self.snuffling_modules:
1470 self.snuffling_modules[directory, name] = \
1471 pyrocko.gui.snuffler.snuffling.SnufflingModule(
1472 directory, name, self)
1474 yield self.snuffling_modules[directory, name]
1476 def setup_snufflings(self):
1477 # user snufflings
1478 from pyrocko.gui.snuffler.snuffling import BrokenSnufflingModule
1479 for mod in self.iter_snuffling_modules():
1480 try:
1481 mod.load_if_needed()
1482 except BrokenSnufflingModule as e:
1483 logger.warning('Snuffling module "%s" is broken' % e)
1485 # load the default snufflings on first run
1486 if self.default_snufflings is None:
1487 self.default_snufflings = pyrocko.gui.snuffler\
1488 .snufflings.__snufflings__()
1489 for snuffling in self.default_snufflings:
1490 self.add_snuffling(snuffling)
1492 def set_panel_parent(self, panel_parent):
1493 self.panel_parent = panel_parent
1495 def get_panel_parent(self):
1496 return self.panel_parent
1498 def add_snuffling(self, snuffling, reloaded=False):
1499 logger.debug('Adding snuffling %s' % snuffling.get_name())
1500 snuffling.init_gui(
1501 self, self.get_panel_parent(), self, reloaded=reloaded)
1502 self.snufflings.append(snuffling)
1503 self.update()
1505 def remove_snuffling(self, snuffling):
1506 snuffling.delete_gui()
1507 self.update()
1508 self.snufflings.remove(snuffling)
1509 snuffling.pre_destroy()
1511 def add_snuffling_menuitem(self, item):
1512 self.snufflings_menu.addAction(item)
1513 item.setParent(self.snufflings_menu)
1514 sort_actions(self.snufflings_menu)
1516 def remove_snuffling_menuitem(self, item):
1517 self.snufflings_menu.removeAction(item)
1519 def add_snuffling_help_menuitem(self, item):
1520 self.snuffling_help.addAction(item)
1521 item.setParent(self.snuffling_help)
1522 sort_actions(self.snuffling_help)
1524 def remove_snuffling_help_menuitem(self, item):
1525 self.snuffling_help.removeAction(item)
1527 def add_panel_toggler(self, item):
1528 self.toggle_panel_menu.addAction(item)
1529 item.setParent(self.toggle_panel_menu)
1530 sort_actions(self.toggle_panel_menu)
1532 def remove_panel_toggler(self, item):
1533 self.toggle_panel_menu.removeAction(item)
1535 def load(self, paths, regex=None, format='detect',
1536 cache_dir=None, force_cache=False):
1538 if cache_dir is None:
1539 cache_dir = pyrocko.config.config().cache_dir
1540 if isinstance(paths, str):
1541 paths = [paths]
1543 fns = pyrocko.util.select_files(
1544 paths, selector=None, include=regex, show_progress=False)
1546 if not fns:
1547 return
1549 cache = pyrocko.pile.get_cache(cache_dir)
1551 t = [time.time()]
1553 def update_bar(label, value):
1554 pbs = self.parent().get_progressbars()
1555 if label.lower() == 'looking at files':
1556 label = 'Looking at %i files' % len(fns)
1557 else:
1558 label = 'Scanning %i files' % len(fns)
1560 return pbs.set_status(label, value)
1562 def update_progress(label, i, n):
1563 abort = False
1565 qw.qApp.processEvents()
1566 if n != 0:
1567 perc = i*100/n
1568 else:
1569 perc = 100
1570 abort |= update_bar(label, perc)
1571 abort |= self.window().is_closing()
1573 tnow = time.time()
1574 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1575 self.update()
1576 t[0] = tnow
1578 return abort
1580 self.automatic_updates = False
1582 self.pile.load_files(
1583 sorted(fns),
1584 filename_attributes=regex,
1585 cache=cache,
1586 fileformat=format,
1587 show_progress=False,
1588 update_progress=update_progress)
1590 self.automatic_updates = True
1591 self.update()
1593 def load_queued(self):
1594 if not self._paths_to_load:
1595 return
1596 paths = self._paths_to_load
1597 self._paths_to_load = []
1598 self.load(paths)
1600 def load_soon(self, paths):
1601 self._paths_to_load.extend(paths)
1602 qc.QTimer.singleShot(200, self.load_queued)
1604 def open_waveforms(self):
1605 caption = 'Select one or more files to open'
1607 fns, _ = qw.QFileDialog.getOpenFileNames(
1608 self, caption, options=qfiledialog_options)
1610 if fns:
1611 self.load(list(str(fn) for fn in fns))
1613 def open_waveform_directory(self):
1614 caption = 'Select directory to scan for waveform files'
1616 dn = qw.QFileDialog.getExistingDirectory(
1617 self, caption, options=qfiledialog_options)
1619 if dn:
1620 self.load([str(dn)])
1622 def open_stations(self, fns=None):
1623 caption = 'Select one or more Pyrocko station files to open'
1625 if not fns:
1626 fns, _ = qw.QFileDialog.getOpenFileNames(
1627 self, caption, options=qfiledialog_options)
1629 try:
1630 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1631 for stat in stations:
1632 self.add_stations(stat)
1634 except Exception as e:
1635 self.fail('Failed to read station file: %s' % str(e))
1637 def open_stations_xml(self, fns=None):
1638 from pyrocko.io import stationxml
1640 caption = 'Select one or more StationXML files'
1641 if not fns:
1642 fns, _ = qw.QFileDialog.getOpenFileNames(
1643 self, caption, options=qfiledialog_options,
1644 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1645 ';;All files (*)')
1647 try:
1648 stations = [
1649 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1650 for x in fns]
1652 for stat in stations:
1653 self.add_stations(stat)
1655 except Exception as e:
1656 self.fail('Failed to read StationXML file: %s' % str(e))
1658 def add_traces(self, traces):
1659 if traces:
1660 mtf = pyrocko.pile.MemTracesFile(None, traces)
1661 self.pile.add_file(mtf)
1662 ticket = (self.pile, mtf)
1663 return ticket
1664 else:
1665 return (None, None)
1667 def release_data(self, tickets):
1668 for ticket in tickets:
1669 pile, mtf = ticket
1670 if pile is not None:
1671 pile.remove_file(mtf)
1673 def periodical(self):
1674 if self.menuitem_watch.isChecked():
1675 if self.pile.reload_modified():
1676 self.update()
1678 def get_pile(self):
1679 return self.pile
1681 def pile_changed(self, what, content):
1682 self.pile_has_changed = True
1683 self.pile_has_changed_signal.emit()
1684 if self.automatic_updates:
1685 self.update()
1687 def set_gathering(self, gather=None, color=None):
1689 if gather is None:
1690 def gather_func(tr):
1691 return tr.nslc_id
1693 gather = (0, 1, 2, 3)
1695 else:
1696 def gather_func(tr):
1697 return (
1698 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1700 if color is None:
1701 def color(tr):
1702 return tr.location
1704 self.gather = gather_func
1705 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1707 self.color_gather = color
1708 self.color_keys = self.pile.gather_keys(color)
1709 previous_ntracks = self.ntracks
1710 self.set_ntracks(len(keys))
1712 if self.shown_tracks_range is None or \
1713 previous_ntracks == 0 or \
1714 self.show_all:
1716 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1717 key_at_top = None
1718 n = high-low
1720 else:
1721 low, high = self.shown_tracks_range
1722 key_at_top = self.track_keys[low]
1723 n = high-low
1725 self.track_keys = sorted(keys)
1727 track_patterns = []
1728 for k in self.track_keys:
1729 pat = ['*', '*', '*', '*']
1730 for i, j in enumerate(gather):
1731 pat[j] = k[-len(gather)+i]
1733 track_patterns.append(pat)
1735 self.track_patterns = track_patterns
1737 if key_at_top is not None:
1738 try:
1739 ind = self.track_keys.index(key_at_top)
1740 low = ind
1741 high = low+n
1742 except Exception:
1743 pass
1745 self.set_tracks_range((low, high))
1747 self.key_to_row = dict(
1748 [(key, i) for (i, key) in enumerate(self.track_keys)])
1750 def inrange(x, r):
1751 return r[0] <= x and x < r[1]
1753 def trace_selector(trace):
1754 gt = self.gather(trace)
1755 return (
1756 gt in self.key_to_row and
1757 inrange(self.key_to_row[gt], self.shown_tracks_range))
1759 self.trace_selector = lambda x: \
1760 (self.trace_filter is None or self.trace_filter(x)) \
1761 and trace_selector(x)
1763 if self.tmin == working_system_time_range[0] and \
1764 self.tmax == working_system_time_range[1] or \
1765 self.show_all:
1767 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1768 if tmin is not None and tmax is not None:
1769 tlen = (tmax - tmin)
1770 tpad = tlen * 5./self.width()
1771 self.set_time_range(tmin-tpad, tmax+tpad)
1773 def set_time_range(self, tmin, tmax):
1774 if tmin is None:
1775 tmin = initial_time_range[0]
1777 if tmax is None:
1778 tmax = initial_time_range[1]
1780 if tmin > tmax:
1781 tmin, tmax = tmax, tmin
1783 if tmin == tmax:
1784 tmin -= 1.
1785 tmax += 1.
1787 tmin = max(working_system_time_range[0], tmin)
1788 tmax = min(working_system_time_range[1], tmax)
1790 min_deltat = self.content_deltat_range()[0]
1791 if (tmax - tmin < min_deltat):
1792 m = (tmin + tmax) / 2.
1793 tmin = m - min_deltat/2.
1794 tmax = m + min_deltat/2.
1796 self.time_projection.set_in_range(tmin, tmax)
1797 self.tmin, self.tmax = tmin, tmax
1799 def get_time_range(self):
1800 return self.tmin, self.tmax
1802 def ypart(self, y):
1803 if y < self.ax_height:
1804 return -1
1805 elif y > self.height()-self.ax_height:
1806 return 1
1807 else:
1808 return 0
1810 def time_fractional_digits(self):
1811 min_deltat = self.content_deltat_range()[0]
1812 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1814 def write_markers(self, fn=None):
1815 caption = "Choose a file name to write markers"
1816 if not fn:
1817 fn, _ = qw.QFileDialog.getSaveFileName(
1818 self, caption, options=qfiledialog_options)
1819 if fn:
1820 try:
1821 Marker.save_markers(
1822 self.markers, fn,
1823 fdigits=self.time_fractional_digits())
1825 except Exception as e:
1826 self.fail('Failed to write marker file: %s' % str(e))
1828 def write_selected_markers(self, fn=None):
1829 caption = "Choose a file name to write selected markers"
1830 if not fn:
1831 fn, _ = qw.QFileDialog.getSaveFileName(
1832 self, caption, options=qfiledialog_options)
1833 if fn:
1834 try:
1835 Marker.save_markers(
1836 self.iter_selected_markers(),
1837 fn,
1838 fdigits=self.time_fractional_digits())
1840 except Exception as e:
1841 self.fail('Failed to write marker file: %s' % str(e))
1843 def read_events(self, fn=None):
1844 '''
1845 Open QFileDialog to open, read and add
1846 :py:class:`pyrocko.model.Event` instances and their marker
1847 representation to the pile viewer.
1848 '''
1849 caption = "Selet one or more files to open"
1850 if not fn:
1851 fn, _ = qw.QFileDialog.getOpenFileName(
1852 self, caption, options=qfiledialog_options)
1853 if fn:
1854 try:
1855 self.add_events(pyrocko.model.load_events(fn))
1856 self.associate_phases_to_events()
1858 except Exception as e:
1859 self.fail('Failed to read event file: %s' % str(e))
1861 def read_markers(self, fn=None):
1862 '''
1863 Open QFileDialog to open, read and add markers to the pile viewer.
1864 '''
1865 caption = "Selet one or more marker files to open"
1866 if not fn:
1867 fn, _ = qw.QFileDialog.getOpenFileName(
1868 self, caption, options=qfiledialog_options)
1869 if fn:
1870 try:
1871 self.add_markers(Marker.load_markers(fn))
1872 self.associate_phases_to_events()
1874 except Exception as e:
1875 self.fail('Failed to read marker file: %s' % str(e))
1877 def associate_phases_to_events(self):
1878 associate_phases_to_events(self.markers)
1880 def add_marker(self, marker):
1881 # need index to inform QAbstactTableModel about upcoming change,
1882 # but have to restore current state in order to not cause problems
1883 self.markers.insert(marker)
1884 i = self.markers.remove(marker)
1886 self.begin_markers_add.emit(i, i)
1887 self.markers.insert(marker)
1888 self.end_markers_add.emit()
1889 self.markers_deltat_max = max(
1890 self.markers_deltat_max, marker.tmax - marker.tmin)
1892 def add_markers(self, markers):
1893 if not self.markers:
1894 self.begin_markers_add.emit(0, len(markers) - 1)
1895 self.markers.insert_many(markers)
1896 self.end_markers_add.emit()
1897 self.update_markers_deltat_max()
1898 else:
1899 for marker in markers:
1900 self.add_marker(marker)
1902 def update_markers_deltat_max(self):
1903 if self.markers:
1904 self.markers_deltat_max = max(
1905 marker.tmax - marker.tmin for marker in self.markers)
1907 def remove_marker(self, marker):
1908 '''
1909 Remove a ``marker`` from the :py:class:`PileViewer`.
1911 :param marker: :py:class:`Marker` (or subclass) instance
1912 '''
1914 if marker is self.active_event_marker:
1915 self.deactivate_event_marker()
1917 try:
1918 i = self.markers.index(marker)
1919 self.begin_markers_remove.emit(i, i)
1920 self.markers.remove_at(i)
1921 self.end_markers_remove.emit()
1922 except ValueError:
1923 pass
1925 def remove_markers(self, markers):
1926 '''
1927 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1929 :param markers: list of :py:class:`Marker` (or subclass)
1930 instances
1931 '''
1933 if markers is self.markers:
1934 markers = list(markers)
1936 for marker in markers:
1937 self.remove_marker(marker)
1939 self.update_markers_deltat_max()
1941 def remove_selected_markers(self):
1942 def delete_segment(istart, iend):
1943 self.begin_markers_remove.emit(istart, iend-1)
1944 for _ in range(iend - istart):
1945 self.markers.remove_at(istart)
1947 self.end_markers_remove.emit()
1949 istart = None
1950 ipos = 0
1951 markers = self.markers
1952 nmarkers = len(self.markers)
1953 while ipos < nmarkers:
1954 marker = markers[ipos]
1955 if marker.is_selected():
1956 if marker is self.active_event_marker:
1957 self.deactivate_event_marker()
1959 if istart is None:
1960 istart = ipos
1961 else:
1962 if istart is not None:
1963 delete_segment(istart, ipos)
1964 nmarkers -= ipos - istart
1965 ipos = istart - 1
1966 istart = None
1968 ipos += 1
1970 if istart is not None:
1971 delete_segment(istart, ipos)
1973 self.update_markers_deltat_max()
1975 def selected_markers(self):
1976 return [marker for marker in self.markers if marker.is_selected()]
1978 def iter_selected_markers(self):
1979 for marker in self.markers:
1980 if marker.is_selected():
1981 yield marker
1983 def get_markers(self):
1984 return self.markers
1986 def mousePressEvent(self, mouse_ev):
1987 self.show_all = False
1988 point = self.mapFromGlobal(mouse_ev.globalPos())
1990 if mouse_ev.button() == qc.Qt.LeftButton:
1991 marker = self.marker_under_cursor(point.x(), point.y())
1992 if self.picking:
1993 if self.picking_down is None:
1994 self.picking_down = (
1995 self.time_projection.rev(mouse_ev.x()),
1996 mouse_ev.y())
1998 elif marker is not None:
1999 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
2000 self.deselect_all()
2001 marker.selected = True
2002 self.emit_selected_markers()
2003 self.update()
2004 else:
2005 self.track_start = mouse_ev.x(), mouse_ev.y()
2006 self.track_trange = self.tmin, self.tmax
2008 if mouse_ev.button() == qc.Qt.RightButton \
2009 and isinstance(self.menu, qw.QMenu):
2010 self.menu.exec_(qg.QCursor.pos())
2011 self.update_status()
2013 def mouseReleaseEvent(self, mouse_ev):
2014 if self.ignore_releases:
2015 self.ignore_releases -= 1
2016 return
2018 if self.picking:
2019 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2020 self.emit_selected_markers()
2022 if self.track_start:
2023 self.update()
2025 self.track_start = None
2026 self.track_trange = None
2027 self.update_status()
2029 def mouseDoubleClickEvent(self, mouse_ev):
2030 self.show_all = False
2031 self.start_picking(None)
2032 self.ignore_releases = 1
2034 def mouseMoveEvent(self, mouse_ev):
2035 point = self.mapFromGlobal(mouse_ev.globalPos())
2037 if self.picking:
2038 self.update_picking(point.x(), point.y())
2040 elif self.track_start is not None:
2041 x0, y0 = self.track_start
2042 dx = (point.x() - x0)/float(self.width())
2043 dy = (point.y() - y0)/float(self.height())
2044 if self.ypart(y0) == 1:
2045 dy = 0
2047 tmin0, tmax0 = self.track_trange
2049 scale = math.exp(-dy*5.)
2050 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2051 frac = x0/float(self.width())
2052 dt = dx*(tmax0-tmin0)*scale
2054 self.interrupt_following()
2055 self.set_time_range(
2056 tmin0 - dt - dtr*frac,
2057 tmax0 - dt + dtr*(1.-frac))
2059 self.update()
2060 else:
2061 self.hoovering(point.x(), point.y())
2063 self.update_status()
2065 def nslc_ids_under_cursor(self, x, y):
2066 ftrack = self.track_to_screen.rev(y)
2067 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2068 return nslc_ids
2070 def marker_under_cursor(self, x, y):
2071 mouset = self.time_projection.rev(x)
2072 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2073 relevant_nslc_ids = None
2074 for marker in self.markers:
2075 if marker.kind not in self.visible_marker_kinds:
2076 continue
2078 if (abs(mouset-marker.tmin) < deltat or
2079 abs(mouset-marker.tmax) < deltat):
2081 if relevant_nslc_ids is None:
2082 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2084 marker_nslc_ids = marker.get_nslc_ids()
2085 if not marker_nslc_ids:
2086 return marker
2088 for nslc_id in marker_nslc_ids:
2089 if nslc_id in relevant_nslc_ids:
2090 return marker
2092 def hoovering(self, x, y):
2093 mouset = self.time_projection.rev(x)
2094 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2095 needupdate = False
2096 haveone = False
2097 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2098 for marker in self.markers:
2099 if marker.kind not in self.visible_marker_kinds:
2100 continue
2102 state = abs(mouset-marker.tmin) < deltat or \
2103 abs(mouset-marker.tmax) < deltat and not haveone
2105 if state:
2106 xstate = False
2108 marker_nslc_ids = marker.get_nslc_ids()
2109 if not marker_nslc_ids:
2110 xstate = True
2112 for nslc in relevant_nslc_ids:
2113 if marker.match_nslc(nslc):
2114 xstate = True
2116 state = xstate
2118 if state:
2119 haveone = True
2120 oldstate = marker.is_alerted()
2121 if oldstate != state:
2122 needupdate = True
2123 marker.set_alerted(state)
2124 if state:
2125 self.message = marker.hoover_message()
2127 if not haveone:
2128 self.message = None
2130 if needupdate:
2131 self.update()
2133 def event(self, event):
2134 if event.type() == qc.QEvent.KeyPress:
2135 self.keyPressEvent(event)
2136 return True
2137 else:
2138 return base.event(self, event)
2140 def keyPressEvent(self, key_event):
2141 self.show_all = False
2142 dt = self.tmax - self.tmin
2143 tmid = (self.tmin + self.tmax) / 2.
2145 key = key_event.key()
2146 try:
2147 keytext = str(key_event.text())
2148 except UnicodeEncodeError:
2149 return
2151 if key == qc.Qt.Key_Space:
2152 self.interrupt_following()
2153 self.set_time_range(self.tmin+dt, self.tmax+dt)
2155 elif key == qc.Qt.Key_Up:
2156 for m in self.selected_markers():
2157 if isinstance(m, PhaseMarker):
2158 if key_event.modifiers() & qc.Qt.ShiftModifier:
2159 p = 0
2160 else:
2161 p = 1 if m.get_polarity() != 1 else None
2162 m.set_polarity(p)
2164 elif key == qc.Qt.Key_Down:
2165 for m in self.selected_markers():
2166 if isinstance(m, PhaseMarker):
2167 if key_event.modifiers() & qc.Qt.ShiftModifier:
2168 p = 0
2169 else:
2170 p = -1 if m.get_polarity() != -1 else None
2171 m.set_polarity(p)
2173 elif key == qc.Qt.Key_B:
2174 dt = self.tmax - self.tmin
2175 self.interrupt_following()
2176 self.set_time_range(self.tmin-dt, self.tmax-dt)
2178 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2179 self.interrupt_following()
2181 tgo = None
2183 class TraceDummy(object):
2184 def __init__(self, marker):
2185 self._marker = marker
2187 @property
2188 def nslc_id(self):
2189 return self._marker.one_nslc()
2191 def marker_to_itrack(marker):
2192 try:
2193 return self.key_to_row.get(
2194 self.gather(TraceDummy(marker)), -1)
2196 except MarkerOneNSLCRequired:
2197 return -1
2199 emarker, pmarkers = self.get_active_markers()
2200 pmarkers = [
2201 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2202 pmarkers.sort(key=lambda m: (
2203 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2205 if key == qc.Qt.Key_Backtab:
2206 pmarkers.reverse()
2208 smarkers = self.selected_markers()
2209 iselected = []
2210 for sm in smarkers:
2211 try:
2212 iselected.append(pmarkers.index(sm))
2213 except ValueError:
2214 pass
2216 if iselected:
2217 icurrent = max(iselected) + 1
2218 else:
2219 icurrent = 0
2221 if icurrent < len(pmarkers):
2222 self.deselect_all()
2223 cmarker = pmarkers[icurrent]
2224 cmarker.selected = True
2225 tgo = cmarker.tmin
2226 if not self.tmin < tgo < self.tmax:
2227 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2229 itrack = marker_to_itrack(cmarker)
2230 if itrack != -1:
2231 if itrack < self.shown_tracks_range[0]:
2232 self.scroll_tracks(
2233 - (self.shown_tracks_range[0] - itrack))
2234 elif self.shown_tracks_range[1] <= itrack:
2235 self.scroll_tracks(
2236 itrack - self.shown_tracks_range[1]+1)
2238 if itrack not in self.track_to_nslc_ids:
2239 self.go_to_selection()
2241 elif keytext in ('p', 'n', 'P', 'N'):
2242 smarkers = self.selected_markers()
2243 tgo = None
2244 dir = str(keytext)
2245 if smarkers:
2246 tmid = smarkers[0].tmin
2247 for smarker in smarkers:
2248 if dir == 'n':
2249 tmid = max(smarker.tmin, tmid)
2250 else:
2251 tmid = min(smarker.tmin, tmid)
2253 tgo = tmid
2255 if dir.lower() == 'n':
2256 for marker in sorted(
2257 self.markers,
2258 key=operator.attrgetter('tmin')):
2260 t = marker.tmin
2261 if t > tmid and \
2262 marker.kind in self.visible_marker_kinds and \
2263 (dir == 'n' or
2264 isinstance(marker, EventMarker)):
2266 self.deselect_all()
2267 marker.selected = True
2268 tgo = t
2269 break
2270 else:
2271 for marker in sorted(
2272 self.markers,
2273 key=operator.attrgetter('tmin'),
2274 reverse=True):
2276 t = marker.tmin
2277 if t < tmid and \
2278 marker.kind in self.visible_marker_kinds and \
2279 (dir == 'p' or
2280 isinstance(marker, EventMarker)):
2281 self.deselect_all()
2282 marker.selected = True
2283 tgo = t
2284 break
2286 if tgo is not None:
2287 self.interrupt_following()
2288 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2290 elif keytext == 'r':
2291 if self.pile.reload_modified():
2292 self.reloaded = True
2294 elif keytext == 'R':
2295 self.setup_snufflings()
2297 elif key == qc.Qt.Key_Backspace:
2298 self.remove_selected_markers()
2300 elif keytext == 'a':
2301 for marker in self.markers:
2302 if ((self.tmin <= marker.tmin <= self.tmax or
2303 self.tmin <= marker.tmax <= self.tmax) and
2304 marker.kind in self.visible_marker_kinds):
2305 marker.selected = True
2306 else:
2307 marker.selected = False
2309 elif keytext == 'A':
2310 for marker in self.markers:
2311 if marker.kind in self.visible_marker_kinds:
2312 marker.selected = True
2314 elif keytext == 'd':
2315 self.deselect_all()
2317 elif keytext == 'E':
2318 self.deactivate_event_marker()
2320 elif keytext == 'e':
2321 markers = self.selected_markers()
2322 event_markers_in_spe = [
2323 marker for marker in markers
2324 if not isinstance(marker, PhaseMarker)]
2326 phase_markers = [
2327 marker for marker in markers
2328 if isinstance(marker, PhaseMarker)]
2330 if len(event_markers_in_spe) == 1:
2331 event_marker = event_markers_in_spe[0]
2332 if not isinstance(event_marker, EventMarker):
2333 nslcs = list(event_marker.nslc_ids)
2334 lat, lon = 0.0, 0.0
2335 old = self.get_active_event()
2336 if len(nslcs) == 1:
2337 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2338 elif old is not None:
2339 lat, lon = old.lat, old.lon
2341 event_marker.convert_to_event_marker(lat, lon)
2343 self.set_active_event_marker(event_marker)
2344 event = event_marker.get_event()
2345 for marker in phase_markers:
2346 marker.set_event(event)
2348 else:
2349 for marker in event_markers_in_spe:
2350 marker.convert_to_event_marker()
2352 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2353 for marker in self.selected_markers():
2354 marker.set_kind(int(keytext))
2355 self.emit_selected_markers()
2357 elif key in fkey_map:
2358 self.handle_fkeys(key)
2360 elif key == qc.Qt.Key_Escape:
2361 if self.picking:
2362 self.stop_picking(0, 0, abort=True)
2364 elif key == qc.Qt.Key_PageDown:
2365 self.scroll_tracks(
2366 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2368 elif key == qc.Qt.Key_PageUp:
2369 self.scroll_tracks(
2370 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2372 elif key == qc.Qt.Key_Plus:
2373 self.zoom_tracks(0., 1.)
2375 elif key == qc.Qt.Key_Minus:
2376 self.zoom_tracks(0., -1.)
2378 elif key == qc.Qt.Key_Equal:
2379 ntracks_shown = self.shown_tracks_range[1] - \
2380 self.shown_tracks_range[0]
2381 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2382 self.zoom_tracks(0., dtracks)
2384 elif key == qc.Qt.Key_Colon:
2385 self.want_input.emit()
2387 elif keytext == 'f':
2388 self.toggle_fullscreen()
2390 elif keytext == 'g':
2391 self.go_to_selection()
2393 elif keytext == 'G':
2394 self.go_to_selection(tight=True)
2396 elif keytext == 'm':
2397 self.toggle_marker_editor()
2399 elif keytext == 'c':
2400 self.toggle_main_controls()
2402 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2403 dir = 1
2404 amount = 1
2405 if key_event.key() == qc.Qt.Key_Left:
2406 dir = -1
2407 if key_event.modifiers() & qc.Qt.ShiftModifier:
2408 amount = 10
2409 self.nudge_selected_markers(dir*amount)
2410 else:
2411 super().keyPressEvent(key_event)
2413 if keytext != '' and keytext in 'degaApPnN':
2414 self.emit_selected_markers()
2416 self.update()
2417 self.update_status()
2419 def handle_fkeys(self, key):
2420 self.set_phase_kind(
2421 self.selected_markers(),
2422 fkey_map[key] + 1)
2423 self.emit_selected_markers()
2425 def emit_selected_markers(self):
2426 ibounds = []
2427 last_selected = False
2428 for imarker, marker in enumerate(self.markers):
2429 this_selected = marker.is_selected()
2430 if this_selected != last_selected:
2431 ibounds.append(imarker)
2433 last_selected = this_selected
2435 if last_selected:
2436 ibounds.append(len(self.markers))
2438 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2439 self.n_selected_markers = sum(
2440 chunk[1] - chunk[0] for chunk in chunks)
2441 self.marker_selection_changed.emit(chunks)
2443 def toggle_marker_editor(self):
2444 self.panel_parent.toggle_marker_editor()
2446 def toggle_main_controls(self):
2447 self.panel_parent.toggle_main_controls()
2449 def nudge_selected_markers(self, npixels):
2450 a, b = self.time_projection.ur
2451 c, d = self.time_projection.xr
2452 for marker in self.selected_markers():
2453 if not isinstance(marker, EventMarker):
2454 marker.tmin += npixels * (d-c)/b
2455 marker.tmax += npixels * (d-c)/b
2457 def toggle_fullscreen(self):
2458 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2459 self.window().windowState() & qc.Qt.WindowMaximized:
2460 self.window().showNormal()
2461 else:
2462 if is_macos:
2463 self.window().showMaximized()
2464 else:
2465 self.window().showFullScreen()
2467 def about(self):
2468 fn = pyrocko.util.data_file('snuffler.png')
2469 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2470 txt = f.read()
2471 label = qw.QLabel(txt % {'logo': fn})
2472 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2473 self.show_doc('About', [label], target='tab')
2475 def help(self):
2476 class MyScrollArea(qw.QScrollArea):
2478 def sizeHint(self):
2479 s = qc.QSize()
2480 s.setWidth(self.widget().sizeHint().width())
2481 s.setHeight(self.widget().sizeHint().height())
2482 return s
2484 with open(pyrocko.util.data_file(
2485 'snuffler_help.html')) as f:
2486 hcheat = qw.QLabel(f.read())
2488 with open(pyrocko.util.data_file(
2489 'snuffler_help_epilog.html')) as f:
2490 hepilog = qw.QLabel(f.read())
2492 for h in [hcheat, hepilog]:
2493 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2494 h.setWordWrap(True)
2496 self.show_doc('Help', [hcheat, hepilog], target='panel')
2498 def show_doc(self, name, labels, target='panel'):
2499 scroller = qw.QScrollArea()
2500 frame = qw.QFrame(scroller)
2501 frame.setLineWidth(0)
2502 layout = qw.QVBoxLayout()
2503 layout.setContentsMargins(0, 0, 0, 0)
2504 layout.setSpacing(0)
2505 frame.setLayout(layout)
2506 scroller.setWidget(frame)
2507 scroller.setWidgetResizable(True)
2508 frame.setBackgroundRole(qg.QPalette.Base)
2509 for h in labels:
2510 h.setParent(frame)
2511 h.setMargin(3)
2512 h.setTextInteractionFlags(
2513 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2514 h.setBackgroundRole(qg.QPalette.Base)
2515 layout.addWidget(h)
2516 h.linkActivated.connect(
2517 self.open_link)
2519 if self.panel_parent is not None:
2520 if target == 'panel':
2521 self.panel_parent.add_panel(
2522 name, scroller, True, volatile=False)
2523 else:
2524 self.panel_parent.add_tab(name, scroller)
2526 def open_link(self, link):
2527 qg.QDesktopServices.openUrl(qc.QUrl(link))
2529 def wheelEvent(self, wheel_event):
2530 self.wheel_pos += wheel_event.angleDelta().y()
2532 n = self.wheel_pos // 120
2533 self.wheel_pos = self.wheel_pos % 120
2534 if n == 0:
2535 return
2537 amount = max(
2538 1.,
2539 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2540 wdelta = amount * n
2542 trmin, trmax = self.track_to_screen.get_in_range()
2543 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2544 / (trmax-trmin)
2546 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2547 self.zoom_tracks(anchor, wdelta)
2548 else:
2549 self.scroll_tracks(-wdelta)
2551 def dragEnterEvent(self, event):
2552 if event.mimeData().hasUrls():
2553 if any(url.toLocalFile() for url in event.mimeData().urls()):
2554 event.setDropAction(qc.Qt.LinkAction)
2555 event.accept()
2557 def dropEvent(self, event):
2558 if event.mimeData().hasUrls():
2559 paths = list(
2560 str(url.toLocalFile()) for url in event.mimeData().urls())
2561 event.acceptProposedAction()
2562 self.load(paths)
2564 def get_phase_name(self, kind):
2565 return self.config.get_phase_name(kind)
2567 def set_phase_kind(self, markers, kind):
2568 phasename = self.get_phase_name(kind)
2570 for marker in markers:
2571 if isinstance(marker, PhaseMarker):
2572 if kind == 10:
2573 marker.convert_to_marker()
2574 else:
2575 marker.set_phasename(phasename)
2576 marker.set_event(self.get_active_event())
2578 elif isinstance(marker, EventMarker):
2579 pass
2581 else:
2582 if kind != 10:
2583 event = self.get_active_event()
2584 marker.convert_to_phase_marker(
2585 event, phasename, None, False)
2587 def set_ntracks(self, ntracks):
2588 if self.ntracks != ntracks:
2589 self.ntracks = ntracks
2590 if self.shown_tracks_range is not None:
2591 l, h = self.shown_tracks_range
2592 else:
2593 l, h = 0, self.ntracks
2595 self.tracks_range_changed.emit(self.ntracks, l, h)
2597 def set_tracks_range(self, range, start=None):
2599 low, high = range
2600 low = min(self.ntracks-1, low)
2601 high = min(self.ntracks, high)
2602 low = max(0, low)
2603 high = max(1, high)
2605 if start is None:
2606 start = float(low)
2608 if self.shown_tracks_range != (low, high):
2609 self.shown_tracks_range = low, high
2610 self.shown_tracks_start = start
2612 self.tracks_range_changed.emit(self.ntracks, low, high)
2614 def scroll_tracks(self, shift):
2615 shown = self.shown_tracks_range
2616 shiftmin = -shown[0]
2617 shiftmax = self.ntracks-shown[1]
2618 shift = max(shiftmin, shift)
2619 shift = min(shiftmax, shift)
2620 shown = shown[0] + shift, shown[1] + shift
2622 self.set_tracks_range((int(shown[0]), int(shown[1])))
2624 self.update()
2626 def zoom_tracks(self, anchor, delta):
2627 ntracks_shown = self.shown_tracks_range[1] \
2628 - self.shown_tracks_range[0]
2630 if (ntracks_shown == 1 and delta <= 0) or \
2631 (ntracks_shown == self.ntracks and delta >= 0):
2632 return
2634 ntracks_shown += int(round(delta))
2635 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2637 u = self.shown_tracks_start
2638 nu = max(0., u-anchor*delta)
2639 nv = nu + ntracks_shown
2640 if nv > self.ntracks:
2641 nu -= nv - self.ntracks
2642 nv -= nv - self.ntracks
2644 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2646 self.ntracks_shown_max = self.shown_tracks_range[1] \
2647 - self.shown_tracks_range[0]
2649 self.update()
2651 def content_time_range(self):
2652 pile = self.get_pile()
2653 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2654 if tmin is None:
2655 tmin = initial_time_range[0]
2656 if tmax is None:
2657 tmax = initial_time_range[1]
2659 return tmin, tmax
2661 def content_deltat_range(self):
2662 pile = self.get_pile()
2664 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2666 if deltatmin is None:
2667 deltatmin = 0.001
2669 if deltatmax is None:
2670 deltatmax = 1000.0
2672 return deltatmin, deltatmax
2674 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2675 if tmax < tmin:
2676 tmin, tmax = tmax, tmin
2678 deltatmin = self.content_deltat_range()[0]
2679 dt = deltatmin * self.visible_length * 0.95
2681 if dt == 0.0:
2682 dt = 1.0
2684 if tight:
2685 if tmax != tmin:
2686 dtm = tmax - tmin
2687 tmin -= dtm*0.1
2688 tmax += dtm*0.1
2689 return tmin, tmax
2690 else:
2691 tcenter = (tmin + tmax) / 2.
2692 tmin = tcenter - 0.5*dt
2693 tmax = tcenter + 0.5*dt
2694 return tmin, tmax
2696 if tmax-tmin < dt:
2697 vmin, vmax = self.get_time_range()
2698 dt = min(vmax - vmin, dt)
2700 tcenter = (tmin+tmax)/2.
2701 etmin, etmax = tmin, tmax
2702 tmin = min(etmin, tcenter - 0.5*dt)
2703 tmax = max(etmax, tcenter + 0.5*dt)
2704 dtm = tmax-tmin
2705 if etmin == tmin:
2706 tmin -= dtm*0.1
2707 if etmax == tmax:
2708 tmax += dtm*0.1
2710 else:
2711 dtm = tmax-tmin
2712 tmin -= dtm*0.1
2713 tmax += dtm*0.1
2715 return tmin, tmax
2717 def go_to_selection(self, tight=False):
2718 markers = self.selected_markers()
2719 if markers:
2720 tmax, tmin = self.content_time_range()
2721 for marker in markers:
2722 tmin = min(tmin, marker.tmin)
2723 tmax = max(tmax, marker.tmax)
2725 else:
2726 if tight:
2727 vmin, vmax = self.get_time_range()
2728 tmin = tmax = (vmin + vmax) / 2.
2729 else:
2730 tmin, tmax = self.content_time_range()
2732 tmin, tmax = self.make_good_looking_time_range(
2733 tmin, tmax, tight=tight)
2735 self.interrupt_following()
2736 self.set_time_range(tmin, tmax)
2737 self.update()
2739 def go_to_time(self, t, tlen=None):
2740 tmax = t
2741 if tlen is not None:
2742 tmax = t+tlen
2743 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2744 self.interrupt_following()
2745 self.set_time_range(tmin, tmax)
2746 self.update()
2748 def go_to_event_by_name(self, name):
2749 for marker in self.markers:
2750 if isinstance(marker, EventMarker):
2751 event = marker.get_event()
2752 if event.name and event.name.lower() == name.lower():
2753 tmin, tmax = self.make_good_looking_time_range(
2754 event.time, event.time)
2756 self.interrupt_following()
2757 self.set_time_range(tmin, tmax)
2759 def printit(self):
2760 from ..qt_compat import qprint
2761 printer = qprint.QPrinter()
2762 printer.setOrientation(qprint.QPrinter.Landscape)
2764 dialog = qprint.QPrintDialog(printer, self)
2765 dialog.setWindowTitle('Print')
2767 if dialog.exec_() != qw.QDialog.Accepted:
2768 return
2770 painter = qg.QPainter()
2771 painter.begin(printer)
2772 page = printer.pageRect()
2773 self.drawit(
2774 painter, printmode=False, w=page.width(), h=page.height())
2776 painter.end()
2778 def savesvg(self, fn=None):
2780 if not fn:
2781 fn, _ = qw.QFileDialog.getSaveFileName(
2782 self,
2783 'Save as SVG|PNG',
2784 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2785 'SVG|PNG (*.svg *.png)',
2786 options=qfiledialog_options)
2788 if fn == '':
2789 return
2791 fn = str(fn)
2793 if fn.lower().endswith('.svg'):
2794 try:
2795 w, h = 842, 595
2796 margin = 0.025
2797 m = max(w, h)*margin
2799 generator = qsvg.QSvgGenerator()
2800 generator.setFileName(fn)
2801 generator.setSize(qc.QSize(w, h))
2802 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2804 painter = qg.QPainter()
2805 painter.begin(generator)
2806 self.drawit(painter, printmode=False, w=w, h=h)
2807 painter.end()
2809 except Exception as e:
2810 self.fail('Failed to write SVG file: %s' % str(e))
2812 elif fn.lower().endswith('.png'):
2813 pixmap = self.grab()
2815 try:
2816 pixmap.save(fn)
2818 except Exception as e:
2819 self.fail('Failed to write PNG file: %s' % str(e))
2821 else:
2822 self.fail(
2823 'Unsupported file type: filename must end with ".svg" or '
2824 '".png".')
2826 def paintEvent(self, paint_ev):
2827 '''
2828 Called by QT whenever widget needs to be painted.
2829 '''
2830 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2831 # was called twice (by different threads?), causing segfaults.
2832 if self.in_paint_event:
2833 logger.warning('Blocking reentrant call to paintEvent().')
2834 return
2836 self.in_paint_event = True
2838 painter = qg.QPainter(self)
2840 if self.menuitem_antialias.isChecked():
2841 painter.setRenderHint(qg.QPainter.Antialiasing)
2843 self.drawit(painter)
2845 logger.debug(
2846 'Time spent drawing: '
2847 ' user:%.3f sys:%.3f children_user:%.3f'
2848 ' childred_sys:%.3f elapsed:%.3f' %
2849 (self.timer_draw - self.timer_cutout))
2851 logger.debug(
2852 'Time spent processing:'
2853 ' user:%.3f sys:%.3f children_user:%.3f'
2854 ' childred_sys:%.3f elapsed:%.3f' %
2855 self.timer_cutout.get())
2857 self.time_spent_painting = self.timer_draw.get()[-1]
2858 self.time_last_painted = time.time()
2859 self.in_paint_event = False
2861 def determine_box_styles(self):
2863 traces = list(self.pile.iter_traces())
2864 traces.sort(key=operator.attrgetter('full_id'))
2865 istyle = 0
2866 trace_styles = {}
2867 for itr, tr in enumerate(traces):
2868 if itr > 0:
2869 other = traces[itr-1]
2870 if not (
2871 other.nslc_id == tr.nslc_id
2872 and other.deltat == tr.deltat
2873 and abs(other.tmax - tr.tmin)
2874 < gap_lap_tolerance*tr.deltat):
2876 istyle += 1
2878 trace_styles[tr.full_id, tr.deltat] = istyle
2880 self.trace_styles = trace_styles
2882 def draw_trace_boxes(self, p, time_projection, track_projections):
2884 for v_projection in track_projections.values():
2885 v_projection.set_in_range(0., 1.)
2887 def selector(x):
2888 return x.overlaps(*time_projection.get_in_range())
2890 if self.trace_filter is not None:
2891 def tselector(x):
2892 return selector(x) and self.trace_filter(x)
2894 else:
2895 tselector = selector
2897 traces = list(self.pile.iter_traces(
2898 group_selector=selector, trace_selector=tselector))
2900 traces.sort(key=operator.attrgetter('full_id'))
2902 def drawbox(itrack, istyle, traces):
2903 v_projection = track_projections[itrack]
2904 dvmin = v_projection(0.)
2905 dvmax = v_projection(1.)
2906 dtmin = time_projection.clipped(traces[0].tmin, 0)
2907 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2909 style = box_styles[istyle % len(box_styles)]
2910 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2911 p.fillRect(rect, style.fill_brush)
2912 p.setPen(style.frame_pen)
2913 p.drawRect(rect)
2915 traces_by_style = {}
2916 for itr, tr in enumerate(traces):
2917 gt = self.gather(tr)
2918 if gt not in self.key_to_row:
2919 continue
2921 itrack = self.key_to_row[gt]
2922 if itrack not in track_projections:
2923 continue
2925 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2927 if len(traces) < 500:
2928 drawbox(itrack, istyle, [tr])
2929 else:
2930 if (itrack, istyle) not in traces_by_style:
2931 traces_by_style[itrack, istyle] = []
2932 traces_by_style[itrack, istyle].append(tr)
2934 for (itrack, istyle), traces in traces_by_style.items():
2935 drawbox(itrack, istyle, traces)
2937 def draw_visible_markers(
2938 self, p, vcenter_projection, primary_pen):
2940 try:
2941 markers = self.markers.with_key_in_limited(
2942 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2944 except pyrocko.pile.TooMany:
2945 tmin = self.markers[0].tmin
2946 tmax = self.markers[-1].tmax
2947 umin_view, umax_view = self.time_projection.get_out_range()
2948 umin = max(umin_view, self.time_projection(tmin))
2949 umax = min(umax_view, self.time_projection(tmax))
2950 v0, _ = vcenter_projection.get_out_range()
2951 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2953 p.save()
2955 pen = qg.QPen(primary_pen)
2956 pen.setWidth(2)
2957 pen.setStyle(qc.Qt.DotLine)
2958 # pat = [5., 3.]
2959 # pen.setDashPattern(pat)
2960 p.setPen(pen)
2962 if self.n_selected_markers == len(self.markers):
2963 s_selected = ' (all selected)'
2964 elif self.n_selected_markers > 0:
2965 s_selected = ' (%i selected)' % self.n_selected_markers
2966 else:
2967 s_selected = ''
2969 draw_label(
2970 p, umin+10., v0-10.,
2971 '%i Markers' % len(self.markers) + s_selected,
2972 label_bg, 'LB')
2974 line = qc.QLineF(umin, v0, umax, v0)
2975 p.drawLine(line)
2976 p.restore()
2978 return
2980 for marker in markers:
2981 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2982 and marker.kind in self.visible_marker_kinds:
2984 marker.draw(
2985 p, self.time_projection, vcenter_projection,
2986 with_label=True)
2988 def get_squirrel(self):
2989 try:
2990 return self.pile._squirrel
2991 except AttributeError:
2992 return None
2994 def draw_coverage(self, p, time_projection, track_projections):
2995 sq = self.get_squirrel()
2996 if sq is None:
2997 return
2999 def drawbox(itrack, tmin, tmax, style):
3000 v_projection = track_projections[itrack]
3001 dvmin = v_projection(0.)
3002 dvmax = v_projection(1.)
3003 dtmin = time_projection.clipped(tmin, 0)
3004 dtmax = time_projection.clipped(tmax, 1)
3006 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3007 p.fillRect(rect, style.fill_brush)
3008 p.setPen(style.frame_pen)
3009 p.drawRect(rect)
3011 pattern_list = []
3012 pattern_to_itrack = {}
3013 for key in self.track_keys:
3014 itrack = self.key_to_row[key]
3015 if itrack not in track_projections:
3016 continue
3018 pattern = self.track_patterns[itrack]
3019 pattern_to_itrack[tuple(pattern)] = itrack
3020 pattern_list.append(tuple(pattern))
3022 vmin, vmax = self.get_time_range()
3024 for kind in ['waveform', 'waveform_promise']:
3025 for coverage in sq.get_coverage(
3026 kind, vmin, vmax, pattern_list, limit=500):
3027 itrack = pattern_to_itrack[coverage.pattern.nslc]
3029 if coverage.changes is None:
3030 drawbox(
3031 itrack, coverage.tmin, coverage.tmax,
3032 box_styles_coverage[kind][0])
3033 else:
3034 t = None
3035 pcount = 0
3036 for tb, count in coverage.changes:
3037 if t is not None and tb > t:
3038 if pcount > 0:
3039 drawbox(
3040 itrack, t, tb,
3041 box_styles_coverage[kind][
3042 min(len(box_styles_coverage)-1,
3043 pcount)])
3045 t = tb
3046 pcount = count
3048 def drawit(self, p, printmode=False, w=None, h=None):
3049 '''
3050 This performs the actual drawing.
3051 '''
3053 self.timer_draw.start()
3054 show_boxes = self.menuitem_showboxes.isChecked()
3055 sq = self.get_squirrel()
3057 if self.gather is None:
3058 self.set_gathering()
3060 if self.pile_has_changed:
3062 if not self.sortingmode_change_delayed():
3063 self.sortingmode_change()
3065 if show_boxes and sq is None:
3066 self.determine_box_styles()
3068 self.pile_has_changed = False
3070 if h is None:
3071 h = float(self.height())
3072 if w is None:
3073 w = float(self.width())
3075 if printmode:
3076 primary_color = (0, 0, 0)
3077 else:
3078 primary_color = pyrocko.plot.tango_colors['aluminium5']
3080 primary_pen = qg.QPen(qg.QColor(*primary_color))
3082 ax_h = float(self.ax_height)
3084 vbottom_ax_projection = Projection()
3085 vtop_ax_projection = Projection()
3086 vcenter_projection = Projection()
3088 self.time_projection.set_out_range(0., w)
3089 vbottom_ax_projection.set_out_range(h-ax_h, h)
3090 vtop_ax_projection.set_out_range(0., ax_h)
3091 vcenter_projection.set_out_range(ax_h, h-ax_h)
3092 vcenter_projection.set_in_range(0., 1.)
3093 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3095 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3096 track_projections = {}
3097 for i in range(*self.shown_tracks_range):
3098 proj = Projection()
3099 proj.set_out_range(
3100 self.track_to_screen(i+0.05),
3101 self.track_to_screen(i+1.-0.05))
3103 track_projections[i] = proj
3105 if self.tmin > self.tmax:
3106 return
3108 self.time_projection.set_in_range(self.tmin, self.tmax)
3109 vbottom_ax_projection.set_in_range(0, ax_h)
3111 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3113 yscaler = pyrocko.plot.AutoScaler()
3115 p.setPen(primary_pen)
3117 font = qg.QFont()
3118 font.setBold(True)
3120 axannotfont = qg.QFont()
3121 axannotfont.setBold(True)
3122 axannotfont.setPointSize(8)
3124 processed_traces = self.prepare_cutout2(
3125 self.tmin, self.tmax,
3126 trace_selector=self.trace_selector,
3127 degap=self.menuitem_degap.isChecked(),
3128 demean=self.menuitem_demean.isChecked())
3130 if not printmode and show_boxes:
3131 if (self.view_mode is ViewMode.Wiggle) \
3132 or (self.view_mode is ViewMode.Waterfall
3133 and not processed_traces):
3135 if sq is None:
3136 self.draw_trace_boxes(
3137 p, self.time_projection, track_projections)
3139 else:
3140 self.draw_coverage(
3141 p, self.time_projection, track_projections)
3143 p.setFont(font)
3144 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3146 color_lookup = dict(
3147 [(k, i) for (i, k) in enumerate(self.color_keys)])
3149 self.track_to_nslc_ids = {}
3150 nticks = 0
3151 annot_labels = []
3153 if self.view_mode is ViewMode.Waterfall and processed_traces:
3154 waterfall = self.waterfall
3155 waterfall.set_time_range(self.tmin, self.tmax)
3156 waterfall.set_traces(processed_traces)
3157 waterfall.set_cmap(self.waterfall_cmap)
3158 waterfall.set_integrate(self.waterfall_integrate)
3159 waterfall.set_clip(
3160 self.waterfall_clip_min, self.waterfall_clip_max)
3161 waterfall.show_absolute_values(
3162 self.waterfall_show_absolute)
3164 rect = qc.QRectF(
3165 0, self.ax_height,
3166 self.width(), self.height() - self.ax_height*2
3167 )
3168 waterfall.draw_waterfall(p, rect=rect)
3170 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3171 show_scales = self.menuitem_showscalerange.isChecked() \
3172 or self.menuitem_showscaleaxis.isChecked()
3174 fm = qg.QFontMetrics(axannotfont, p.device())
3175 trackheight = self.track_to_screen(1.-0.05) \
3176 - self.track_to_screen(0.05)
3178 nlinesavail = trackheight/float(fm.lineSpacing())
3180 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3181 if self.menuitem_showscaleaxis.isChecked() \
3182 else 15
3184 yscaler = pyrocko.plot.AutoScaler(
3185 no_exp_interval=(-3, 2), approx_ticks=nticks,
3186 snap=show_scales
3187 and not self.menuitem_showscaleaxis.isChecked())
3189 data_ranges = pyrocko.trace.minmax(
3190 processed_traces,
3191 key=self.scaling_key,
3192 mode=self.scaling_base[0],
3193 outer_mode=self.scaling_base[1])
3195 if not self.menuitem_fixscalerange.isChecked():
3196 self.old_data_ranges = data_ranges
3197 else:
3198 data_ranges.update(self.old_data_ranges)
3200 self.apply_scaling_hooks(data_ranges)
3202 trace_to_itrack = {}
3203 track_scaling_keys = {}
3204 track_scaling_colors = {}
3205 for trace in processed_traces:
3206 gt = self.gather(trace)
3207 if gt not in self.key_to_row:
3208 continue
3210 itrack = self.key_to_row[gt]
3211 if itrack not in track_projections:
3212 continue
3214 trace_to_itrack[trace] = itrack
3216 if itrack not in self.track_to_nslc_ids:
3217 self.track_to_nslc_ids[itrack] = set()
3219 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3221 if itrack not in track_scaling_keys:
3222 track_scaling_keys[itrack] = set()
3224 scaling_key = self.scaling_key(trace)
3225 track_scaling_keys[itrack].add(scaling_key)
3227 color = pyrocko.plot.color(
3228 color_lookup[self.color_gather(trace)])
3230 k = itrack, scaling_key
3231 if k not in track_scaling_colors \
3232 and self.menuitem_colortraces.isChecked():
3233 track_scaling_colors[k] = color
3234 else:
3235 track_scaling_colors[k] = primary_color
3237 # y axes, zero lines
3238 trace_projections = {}
3239 for itrack in list(track_projections.keys()):
3240 if itrack not in track_scaling_keys:
3241 continue
3242 uoff = 0
3243 for scaling_key in track_scaling_keys[itrack]:
3244 data_range = data_ranges[scaling_key]
3245 dymin, dymax = data_range
3246 ymin, ymax, yinc = yscaler.make_scale(
3247 (dymin/self.gain, dymax/self.gain))
3248 iexp = yscaler.make_exp(yinc)
3249 factor = 10**iexp
3250 trace_projection = track_projections[itrack].copy()
3251 trace_projection.set_in_range(ymax, ymin)
3252 trace_projections[itrack, scaling_key] = \
3253 trace_projection
3254 umin, umax = self.time_projection.get_out_range()
3255 vmin, vmax = trace_projection.get_out_range()
3256 umax_zeroline = umax
3257 uoffnext = uoff
3259 if show_scales:
3260 pen = qg.QPen(primary_pen)
3261 k = itrack, scaling_key
3262 if k in track_scaling_colors:
3263 c = qg.QColor(*track_scaling_colors[
3264 itrack, scaling_key])
3266 pen.setColor(c)
3268 p.setPen(pen)
3269 if nlinesavail > 3:
3270 if self.menuitem_showscaleaxis.isChecked():
3271 ymin_annot = math.ceil(ymin/yinc)*yinc
3272 ny_annot = int(
3273 math.floor(ymax/yinc)
3274 - math.ceil(ymin/yinc)) + 1
3276 for iy_annot in range(ny_annot):
3277 y = ymin_annot + iy_annot*yinc
3278 v = trace_projection(y)
3279 line = qc.QLineF(
3280 umax-10-uoff, v, umax-uoff, v)
3282 p.drawLine(line)
3283 if iy_annot == ny_annot - 1 \
3284 and iexp != 0:
3285 sexp = ' × ' \
3286 '10<sup>%i</sup>' % iexp
3287 else:
3288 sexp = ''
3290 snum = num_to_html(y/factor)
3291 lab = Label(
3292 p,
3293 umax-20-uoff,
3294 v, '%s%s' % (snum, sexp),
3295 label_bg=None,
3296 anchor='MR',
3297 font=axannotfont,
3298 color=c)
3300 uoffnext = max(
3301 lab.rect.width()+30., uoffnext)
3303 annot_labels.append(lab)
3304 if y == 0.:
3305 umax_zeroline = \
3306 umax - 20 \
3307 - lab.rect.width() - 10 \
3308 - uoff
3309 else:
3310 if not show_boxes:
3311 qpoints = make_QPolygonF(
3312 [umax-20-uoff,
3313 umax-10-uoff,
3314 umax-10-uoff,
3315 umax-20-uoff],
3316 [vmax, vmax, vmin, vmin])
3317 p.drawPolyline(qpoints)
3319 snum = num_to_html(ymin)
3320 labmin = Label(
3321 p, umax-15-uoff, vmax, snum,
3322 label_bg=None,
3323 anchor='BR',
3324 font=axannotfont,
3325 color=c)
3327 annot_labels.append(labmin)
3328 snum = num_to_html(ymax)
3329 labmax = Label(
3330 p, umax-15-uoff, vmin, snum,
3331 label_bg=None,
3332 anchor='TR',
3333 font=axannotfont,
3334 color=c)
3336 annot_labels.append(labmax)
3338 for lab in (labmin, labmax):
3339 uoffnext = max(
3340 lab.rect.width()+10., uoffnext)
3342 if self.menuitem_showzeroline.isChecked():
3343 v = trace_projection(0.)
3344 if vmin <= v <= vmax:
3345 line = qc.QLineF(umin, v, umax_zeroline, v)
3346 p.drawLine(line)
3348 uoff = uoffnext
3350 p.setFont(font)
3351 p.setPen(primary_pen)
3352 for trace in processed_traces:
3353 if self.view_mode is not ViewMode.Wiggle:
3354 break
3356 if trace not in trace_to_itrack:
3357 continue
3359 itrack = trace_to_itrack[trace]
3360 scaling_key = self.scaling_key(trace)
3361 trace_projection = trace_projections[
3362 itrack, scaling_key]
3364 vdata = trace_projection(trace.get_ydata())
3366 udata_min = float(self.time_projection(trace.tmin))
3367 udata_max = float(self.time_projection(
3368 trace.tmin+trace.deltat*(vdata.size-1)))
3369 udata = num.linspace(udata_min, udata_max, vdata.size)
3371 qpoints = make_QPolygonF(udata, vdata)
3373 umin, umax = self.time_projection.get_out_range()
3374 vmin, vmax = trace_projection.get_out_range()
3376 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3378 if self.menuitem_cliptraces.isChecked():
3379 p.setClipRect(trackrect)
3381 if self.menuitem_colortraces.isChecked():
3382 color = pyrocko.plot.color(
3383 color_lookup[self.color_gather(trace)])
3384 pen = qg.QPen(qg.QColor(*color), 1)
3385 p.setPen(pen)
3387 p.drawPolyline(qpoints)
3389 if self.floating_marker:
3390 self.floating_marker.draw_trace(
3391 self, p, trace,
3392 self.time_projection, trace_projection, 1.0)
3394 for marker in self.markers.with_key_in(
3395 self.tmin - self.markers_deltat_max,
3396 self.tmax):
3398 if marker.tmin < self.tmax \
3399 and self.tmin < marker.tmax \
3400 and marker.kind \
3401 in self.visible_marker_kinds:
3402 marker.draw_trace(
3403 self, p, trace, self.time_projection,
3404 trace_projection, 1.0)
3406 p.setPen(primary_pen)
3408 if self.menuitem_cliptraces.isChecked():
3409 p.setClipRect(0, 0, int(w), int(h))
3411 if self.floating_marker:
3412 self.floating_marker.draw(
3413 p, self.time_projection, vcenter_projection)
3415 self.draw_visible_markers(
3416 p, vcenter_projection, primary_pen)
3418 p.setPen(primary_pen)
3419 while font.pointSize() > 2:
3420 fm = qg.QFontMetrics(font, p.device())
3421 trackheight = self.track_to_screen(1.-0.05) \
3422 - self.track_to_screen(0.05)
3423 nlinesavail = trackheight/float(fm.lineSpacing())
3424 if nlinesavail > 1:
3425 break
3427 font.setPointSize(font.pointSize()-1)
3429 p.setFont(font)
3430 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3432 for key in self.track_keys:
3433 itrack = self.key_to_row[key]
3434 if itrack in track_projections:
3435 plabel = ' '.join(
3436 [str(x) for x in key if x is not None])
3437 lx = 10
3438 ly = self.track_to_screen(itrack+0.5)
3440 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3441 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3442 continue
3444 contains_cursor = \
3445 self.track_to_screen(itrack) \
3446 < mouse_pos.y() \
3447 < self.track_to_screen(itrack+1)
3449 if not contains_cursor:
3450 continue
3452 font_large = p.font()
3453 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3454 p.setFont(font_large)
3455 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3456 p.setFont(font)
3458 for lab in annot_labels:
3459 lab.draw()
3461 self.timer_draw.stop()
3463 def see_data_params(self):
3465 min_deltat = self.content_deltat_range()[0]
3467 # determine padding and downampling requirements
3468 if self.lowpass is not None:
3469 deltat_target = 1./self.lowpass * 0.25
3470 ndecimate = min(
3471 50,
3472 max(1, int(round(deltat_target / min_deltat))))
3473 tpad = 1./self.lowpass * 2.
3474 else:
3475 ndecimate = 1
3476 tpad = min_deltat*5.
3478 if self.highpass is not None:
3479 tpad = max(1./self.highpass * 2., tpad)
3481 nsee_points_per_trace = 5000*10
3482 tsee = ndecimate*nsee_points_per_trace*min_deltat
3484 return ndecimate, tpad, tsee
3486 def clean_update(self):
3487 self.cached_processed_traces = None
3488 self.update()
3490 def get_adequate_tpad(self):
3491 tpad = 0.
3492 for f in [self.highpass, self.lowpass]:
3493 if f is not None:
3494 tpad = max(tpad, 1.0/f)
3496 for snuffling in self.snufflings:
3497 if snuffling._post_process_hook_enabled \
3498 or snuffling._pre_process_hook_enabled:
3500 tpad = max(tpad, snuffling.get_tpad())
3502 return tpad
3504 def prepare_cutout2(
3505 self, tmin, tmax, trace_selector=None, degap=True,
3506 demean=True, nmax=6000):
3508 if self.pile.is_empty():
3509 return []
3511 nmax = self.visible_length
3513 self.timer_cutout.start()
3515 tsee = tmax-tmin
3516 min_deltat_wo_decimate = tsee/nmax
3517 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3519 min_deltat_allow = min_deltat_wo_decimate
3520 if self.lowpass is not None:
3521 target_deltat_lp = 0.25/self.lowpass
3522 if target_deltat_lp > min_deltat_wo_decimate:
3523 min_deltat_allow = min_deltat_w_decimate
3525 min_deltat_allow = math.exp(
3526 int(math.floor(math.log(min_deltat_allow))))
3528 tmin_ = tmin
3529 tmax_ = tmax
3531 # fetch more than needed?
3532 if self.menuitem_liberal_fetch.isChecked():
3533 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3534 tmin = math.floor(tmin/tlen) * tlen
3535 tmax = math.ceil(tmax/tlen) * tlen
3537 fft_filtering = self.menuitem_fft_filtering.isChecked()
3538 lphp = self.menuitem_lphp.isChecked()
3539 ads = self.menuitem_allowdownsampling.isChecked()
3541 tpad = self.get_adequate_tpad()
3542 tpad = max(tpad, tsee)
3544 # state vector to decide if cached traces can be used
3545 vec = (
3546 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3547 self.highpass, fft_filtering, lphp,
3548 min_deltat_allow, self.rotate, self.shown_tracks_range,
3549 ads, self.pile.get_update_count())
3551 if (self.cached_vec
3552 and self.cached_vec[0] <= vec[0]
3553 and vec[1] <= self.cached_vec[1]
3554 and vec[2:] == self.cached_vec[2:]
3555 and not (self.reloaded or self.menuitem_watch.isChecked())
3556 and self.cached_processed_traces is not None):
3558 logger.debug('Using cached traces')
3559 processed_traces = self.cached_processed_traces
3561 else:
3562 processed_traces = []
3563 if self.pile.deltatmax >= min_deltat_allow:
3565 if isinstance(self.pile, pyrocko.pile.Pile):
3566 def group_selector(gr):
3567 return gr.deltatmax >= min_deltat_allow
3569 kwargs = dict(group_selector=group_selector)
3570 else:
3571 kwargs = {}
3573 if trace_selector is not None:
3574 def trace_selectorx(tr):
3575 return tr.deltat >= min_deltat_allow \
3576 and trace_selector(tr)
3577 else:
3578 def trace_selectorx(tr):
3579 return tr.deltat >= min_deltat_allow
3581 for traces in self.pile.chopper(
3582 tmin=tmin, tmax=tmax, tpad=tpad,
3583 want_incomplete=True,
3584 degap=degap,
3585 maxgap=gap_lap_tolerance,
3586 maxlap=gap_lap_tolerance,
3587 keep_current_files_open=True,
3588 trace_selector=trace_selectorx,
3589 accessor_id=id(self),
3590 snap=(math.floor, math.ceil),
3591 include_last=True, **kwargs):
3593 if demean:
3594 for tr in traces:
3595 if (tr.meta and tr.meta.get('tabu', False)):
3596 continue
3597 y = tr.get_ydata()
3598 tr.set_ydata(y - num.mean(y))
3600 traces = self.pre_process_hooks(traces)
3602 for trace in traces:
3604 if not (trace.meta
3605 and trace.meta.get('tabu', False)):
3607 if fft_filtering:
3608 but = pyrocko.response.ButterworthResponse
3609 multres = pyrocko.response.MultiplyResponse
3610 if self.lowpass is not None \
3611 or self.highpass is not None:
3613 it = num.arange(
3614 trace.data_len(), dtype=float)
3615 detr_data, m, b = detrend(
3616 it, trace.get_ydata())
3618 trace.set_ydata(detr_data)
3620 freqs, fdata = trace.spectrum(
3621 pad_to_pow2=True, tfade=None)
3623 nfreqs = fdata.size
3625 key = (trace.deltat, nfreqs)
3627 if key not in self.tf_cache:
3628 resps = []
3629 if self.lowpass is not None:
3630 resps.append(but(
3631 order=4,
3632 corner=self.lowpass,
3633 type='low'))
3635 if self.highpass is not None:
3636 resps.append(but(
3637 order=4,
3638 corner=self.highpass,
3639 type='high'))
3641 resp = multres(resps)
3642 self.tf_cache[key] = \
3643 resp.evaluate(freqs)
3645 filtered_data = num.fft.irfft(
3646 fdata*self.tf_cache[key]
3647 )[:trace.data_len()]
3649 retrended_data = retrend(
3650 it, filtered_data, m, b)
3652 trace.set_ydata(retrended_data)
3654 else:
3656 if ads and self.lowpass is not None:
3657 while trace.deltat \
3658 < min_deltat_wo_decimate:
3660 trace.downsample(2, demean=False)
3662 fmax = 0.5/trace.deltat
3663 if not lphp and (
3664 self.lowpass is not None
3665 and self.highpass is not None
3666 and self.lowpass < fmax
3667 and self.highpass < fmax
3668 and self.highpass < self.lowpass):
3670 trace.bandpass(
3671 2, self.highpass, self.lowpass)
3672 else:
3673 if self.lowpass is not None:
3674 if self.lowpass < 0.5/trace.deltat:
3675 trace.lowpass(
3676 4, self.lowpass,
3677 demean=False)
3679 if self.highpass is not None:
3680 if self.lowpass is None \
3681 or self.highpass \
3682 < self.lowpass:
3684 if self.highpass < \
3685 0.5/trace.deltat:
3686 trace.highpass(
3687 4, self.highpass,
3688 demean=False)
3690 processed_traces.append(trace)
3692 if self.rotate != 0.0:
3693 phi = self.rotate/180.*math.pi
3694 cphi = math.cos(phi)
3695 sphi = math.sin(phi)
3696 for a in processed_traces:
3697 for b in processed_traces:
3698 if (a.network == b.network
3699 and a.station == b.station
3700 and a.location == b.location
3701 and ((a.channel.lower().endswith('n')
3702 and b.channel.lower().endswith('e'))
3703 or (a.channel.endswith('1')
3704 and b.channel.endswith('2')))
3705 and abs(a.deltat-b.deltat) < a.deltat*0.001
3706 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3707 len(a.get_ydata()) == len(b.get_ydata())):
3709 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3710 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3711 a.set_ydata(aydata)
3712 b.set_ydata(bydata)
3714 processed_traces = self.post_process_hooks(processed_traces)
3716 self.cached_processed_traces = processed_traces
3717 self.cached_vec = vec
3719 chopped_traces = []
3720 for trace in processed_traces:
3721 chop_tmin = tmin_ - trace.deltat*4
3722 chop_tmax = tmax_ + trace.deltat*4
3724 try:
3725 ctrace = trace.chop(
3726 chop_tmin, chop_tmax,
3727 inplace=False)
3729 except pyrocko.trace.NoData:
3730 continue
3732 if ctrace.data_len() < 2:
3733 continue
3735 chopped_traces.append(ctrace)
3737 self.timer_cutout.stop()
3738 return chopped_traces
3740 def pre_process_hooks(self, traces):
3741 for snuffling in self.snufflings:
3742 if snuffling._pre_process_hook_enabled:
3743 traces = snuffling.pre_process_hook(traces)
3745 return traces
3747 def post_process_hooks(self, traces):
3748 for snuffling in self.snufflings:
3749 if snuffling._post_process_hook_enabled:
3750 traces = snuffling.post_process_hook(traces)
3752 return traces
3754 def visible_length_change(self, ignore=None):
3755 for menuitem, vlen in self.menuitems_visible_length:
3756 if menuitem.isChecked():
3757 self.visible_length = vlen
3759 def scaling_base_change(self, ignore=None):
3760 for menuitem, scaling_base in self.menuitems_scaling_base:
3761 if menuitem.isChecked():
3762 self.scaling_base = scaling_base
3764 def scalingmode_change(self, ignore=None):
3765 for menuitem, scaling_key in self.menuitems_scaling:
3766 if menuitem.isChecked():
3767 self.scaling_key = scaling_key
3768 self.update()
3770 def apply_scaling_hooks(self, data_ranges):
3771 for k in sorted(self.scaling_hooks.keys()):
3772 hook = self.scaling_hooks[k]
3773 hook(data_ranges)
3775 def viewmode_change(self, ignore=True):
3776 for item, mode in self.menuitems_viewmode:
3777 if item.isChecked():
3778 self.view_mode = mode
3779 break
3780 else:
3781 raise AttributeError('unknown view mode')
3783 items_waterfall_disabled = (
3784 self.menuitem_showscaleaxis,
3785 self.menuitem_showscalerange,
3786 self.menuitem_showzeroline,
3787 self.menuitem_colortraces,
3788 self.menuitem_cliptraces,
3789 *(itm[0] for itm in self.menuitems_visible_length)
3790 )
3792 if self.view_mode is ViewMode.Waterfall:
3793 self.parent().show_colorbar_ctrl(True)
3794 self.parent().show_gain_ctrl(False)
3796 for item in items_waterfall_disabled:
3797 item.setDisabled(True)
3799 self.visible_length = 180.
3800 else:
3801 self.parent().show_colorbar_ctrl(False)
3802 self.parent().show_gain_ctrl(True)
3804 for item in items_waterfall_disabled:
3805 item.setDisabled(False)
3807 self.visible_length_change()
3808 self.update()
3810 def set_scaling_hook(self, k, hook):
3811 self.scaling_hooks[k] = hook
3813 def remove_scaling_hook(self, k):
3814 del self.scaling_hooks[k]
3816 def remove_scaling_hooks(self):
3817 self.scaling_hooks = {}
3819 def s_sortingmode_change(self, ignore=None):
3820 for menuitem, valfunc in self.menuitems_ssorting:
3821 if menuitem.isChecked():
3822 self._ssort = valfunc
3824 self.sortingmode_change()
3826 def sortingmode_change(self, ignore=None):
3827 for menuitem, (gather, color) in self.menuitems_sorting:
3828 if menuitem.isChecked():
3829 self.set_gathering(gather, color)
3831 self.sortingmode_change_time = time.time()
3833 def lowpass_change(self, value, ignore=None):
3834 self.lowpass = value
3835 self.passband_check()
3836 self.tf_cache = {}
3837 self.update()
3839 def highpass_change(self, value, ignore=None):
3840 self.highpass = value
3841 self.passband_check()
3842 self.tf_cache = {}
3843 self.update()
3845 def passband_check(self):
3846 if self.highpass and self.lowpass \
3847 and self.highpass >= self.lowpass:
3849 self.message = 'Corner frequency of highpass larger than ' \
3850 'corner frequency of lowpass! I will now ' \
3851 'deactivate the highpass.'
3853 self.update_status()
3854 else:
3855 oldmess = self.message
3856 self.message = None
3857 if oldmess is not None:
3858 self.update_status()
3860 def gain_change(self, value, ignore):
3861 self.gain = value
3862 self.update()
3864 def rot_change(self, value, ignore):
3865 self.rotate = value
3866 self.update()
3868 def waterfall_cmap_change(self, cmap):
3869 self.waterfall_cmap = cmap
3870 self.update()
3872 def waterfall_clip_change(self, clip_min, clip_max):
3873 self.waterfall_clip_min = clip_min
3874 self.waterfall_clip_max = clip_max
3875 self.update()
3877 def waterfall_show_absolute_change(self, toggle):
3878 self.waterfall_show_absolute = toggle
3879 self.update()
3881 def waterfall_set_integrate(self, toggle):
3882 self.waterfall_integrate = toggle
3883 self.update()
3885 def set_selected_markers(self, markers):
3886 '''
3887 Set a list of markers selected
3889 :param markers: list of markers
3890 '''
3891 self.deselect_all()
3892 for m in markers:
3893 m.selected = True
3895 self.update()
3897 def deselect_all(self):
3898 for marker in self.markers:
3899 marker.selected = False
3901 def animate_picking(self):
3902 point = self.mapFromGlobal(qg.QCursor.pos())
3903 self.update_picking(point.x(), point.y(), doshift=True)
3905 def get_nslc_ids_for_track(self, ftrack):
3906 itrack = int(ftrack)
3907 return self.track_to_nslc_ids.get(itrack, [])
3909 def stop_picking(self, x, y, abort=False):
3910 if self.picking:
3911 self.update_picking(x, y, doshift=False)
3912 self.picking = None
3913 self.picking_down = None
3914 self.picking_timer.stop()
3915 self.picking_timer = None
3916 if not abort:
3917 self.add_marker(self.floating_marker)
3918 self.floating_marker.selected = True
3919 self.emit_selected_markers()
3921 self.floating_marker = None
3923 def start_picking(self, ignore):
3925 if not self.picking:
3926 self.deselect_all()
3927 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3928 point = self.mapFromGlobal(qg.QCursor.pos())
3930 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3931 self.picking.setGeometry(
3932 gpoint.x(), gpoint.y(), 1, self.height())
3933 t = self.time_projection.rev(point.x())
3935 ftrack = self.track_to_screen.rev(point.y())
3936 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3937 self.floating_marker = Marker(nslc_ids, t, t)
3938 self.floating_marker.selected = True
3940 self.picking_timer = qc.QTimer()
3941 self.picking_timer.timeout.connect(
3942 self.animate_picking)
3944 self.picking_timer.setInterval(50)
3945 self.picking_timer.start()
3947 def update_picking(self, x, y, doshift=False):
3948 if self.picking:
3949 mouset = self.time_projection.rev(x)
3950 dt = 0.0
3951 if mouset < self.tmin or mouset > self.tmax:
3952 if mouset < self.tmin:
3953 dt = -(self.tmin - mouset)
3954 else:
3955 dt = mouset - self.tmax
3956 ddt = self.tmax-self.tmin
3957 dt = max(dt, -ddt/10.)
3958 dt = min(dt, ddt/10.)
3960 x0 = x
3961 if self.picking_down is not None:
3962 x0 = self.time_projection(self.picking_down[0])
3964 w = abs(x-x0)
3965 x0 = min(x0, x)
3967 tmin, tmax = (
3968 self.time_projection.rev(x0),
3969 self.time_projection.rev(x0+w))
3971 tmin, tmax = (
3972 max(working_system_time_range[0], tmin),
3973 min(working_system_time_range[1], tmax))
3975 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3977 self.picking.setGeometry(
3978 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3980 ftrack = self.track_to_screen.rev(y)
3981 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3982 self.floating_marker.set(nslc_ids, tmin, tmax)
3984 if dt != 0.0 and doshift:
3985 self.interrupt_following()
3986 self.set_time_range(self.tmin+dt, self.tmax+dt)
3988 self.update()
3990 def update_status(self):
3992 if self.message is None:
3993 point = self.mapFromGlobal(qg.QCursor.pos())
3995 mouse_t = self.time_projection.rev(point.x())
3996 if not is_working_time(mouse_t):
3997 return
3999 if self.floating_marker:
4000 tmi, tma = (
4001 self.floating_marker.tmin,
4002 self.floating_marker.tmax)
4004 tt, ms = gmtime_x(tmi)
4006 if tmi == tma:
4007 message = mystrftime(
4008 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4009 tt=tt, milliseconds=ms)
4010 else:
4011 srange = '%g s' % (tma-tmi)
4012 message = mystrftime(
4013 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4014 tt=tt, milliseconds=ms)
4015 else:
4016 tt, ms = gmtime_x(mouse_t)
4018 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4019 else:
4020 message = self.message
4022 sb = self.window().statusBar()
4023 sb.clearMessage()
4024 sb.showMessage(message)
4026 def set_sortingmode_change_delay_time(self, dt):
4027 self.sortingmode_change_delay_time = dt
4029 def sortingmode_change_delayed(self):
4030 now = time.time()
4031 return (
4032 self.sortingmode_change_delay_time is not None
4033 and now - self.sortingmode_change_time
4034 < self.sortingmode_change_delay_time)
4036 def set_visible_marker_kinds(self, kinds):
4037 self.deselect_all()
4038 self.visible_marker_kinds = tuple(kinds)
4039 self.emit_selected_markers()
4041 def following(self):
4042 return self.follow_timer is not None \
4043 and not self.following_interrupted()
4045 def interrupt_following(self):
4046 self.interactive_range_change_time = time.time()
4048 def following_interrupted(self, now=None):
4049 if now is None:
4050 now = time.time()
4051 return now - self.interactive_range_change_time \
4052 < self.interactive_range_change_delay_time
4054 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4055 if tmax_start is None:
4056 tmax_start = time.time()
4057 self.show_all = False
4058 self.follow_time = tlen
4059 self.follow_timer = qc.QTimer(self)
4060 self.follow_timer.timeout.connect(
4061 self.follow_update)
4062 self.follow_timer.setInterval(interval)
4063 self.follow_timer.start()
4064 self.follow_started = time.time()
4065 self.follow_lapse = lapse
4066 self.follow_tshift = self.follow_started - tmax_start
4067 self.interactive_range_change_time = 0.0
4069 def unfollow(self):
4070 if self.follow_timer is not None:
4071 self.follow_timer.stop()
4072 self.follow_timer = None
4073 self.interactive_range_change_time = 0.0
4075 def follow_update(self):
4076 rnow = time.time()
4077 if self.follow_lapse is None:
4078 now = rnow
4079 else:
4080 now = self.follow_started + (rnow - self.follow_started) \
4081 * self.follow_lapse
4083 if self.following_interrupted(rnow):
4084 return
4085 self.set_time_range(
4086 now-self.follow_time-self.follow_tshift,
4087 now-self.follow_tshift)
4089 self.update()
4091 def myclose(self, return_tag=''):
4092 self.return_tag = return_tag
4093 self.window().close()
4095 def cleanup(self):
4096 self.about_to_close.emit()
4097 self.timer.stop()
4098 if self.follow_timer is not None:
4099 self.follow_timer.stop()
4101 for snuffling in list(self.snufflings):
4102 self.remove_snuffling(snuffling)
4104 def set_error_message(self, key, value):
4105 if value is None:
4106 if key in self.error_messages:
4107 del self.error_messages[key]
4108 else:
4109 self.error_messages[key] = value
4111 def inputline_changed(self, text):
4112 pass
4114 def inputline_finished(self, text):
4115 line = str(text)
4117 toks = line.split()
4118 clearit, hideit, error = False, True, None
4119 if len(toks) >= 1:
4120 command = toks[0].lower()
4122 try:
4123 quick_filter_commands = {
4124 'n': '%s.*.*.*',
4125 's': '*.%s.*.*',
4126 'l': '*.*.%s.*',
4127 'c': '*.*.*.%s'}
4129 if command in quick_filter_commands:
4130 if len(toks) >= 2:
4131 patterns = [
4132 quick_filter_commands[toks[0]] % pat
4133 for pat in toks[1:]]
4134 self.set_quick_filter_patterns(patterns, line)
4135 else:
4136 self.set_quick_filter_patterns(None)
4138 self.update()
4140 elif command in ('hide', 'unhide'):
4141 if len(toks) >= 2:
4142 patterns = []
4143 if len(toks) == 2:
4144 patterns = [toks[1]]
4145 elif len(toks) >= 3:
4146 x = {
4147 'n': '%s.*.*.*',
4148 's': '*.%s.*.*',
4149 'l': '*.*.%s.*',
4150 'c': '*.*.*.%s'}
4152 if toks[1] in x:
4153 patterns.extend(
4154 x[toks[1]] % tok for tok in toks[2:])
4156 for pattern in patterns:
4157 if command == 'hide':
4158 self.add_blacklist_pattern(pattern)
4159 else:
4160 self.remove_blacklist_pattern(pattern)
4162 elif command == 'unhide' and len(toks) == 1:
4163 self.clear_blacklist()
4165 clearit = True
4167 self.update()
4169 elif command == 'markers':
4170 if len(toks) == 2:
4171 if toks[1] == 'all':
4172 kinds = self.all_marker_kinds
4173 else:
4174 kinds = []
4175 for x in toks[1]:
4176 try:
4177 kinds.append(int(x))
4178 except Exception:
4179 pass
4181 self.set_visible_marker_kinds(kinds)
4183 elif len(toks) == 1:
4184 self.set_visible_marker_kinds(())
4186 self.update()
4188 elif command == 'scaling':
4189 if len(toks) == 2:
4190 hideit = False
4191 error = 'wrong number of arguments'
4193 if len(toks) >= 3:
4194 vmin, vmax = [
4195 pyrocko.model.float_or_none(x)
4196 for x in toks[-2:]]
4198 def upd(d, k, vmin, vmax):
4199 if k in d:
4200 if vmin is not None:
4201 d[k] = vmin, d[k][1]
4202 if vmax is not None:
4203 d[k] = d[k][0], vmax
4205 if len(toks) == 1:
4206 self.remove_scaling_hooks()
4208 elif len(toks) == 3:
4209 def hook(data_ranges):
4210 for k in data_ranges:
4211 upd(data_ranges, k, vmin, vmax)
4213 self.set_scaling_hook('_', hook)
4215 elif len(toks) == 4:
4216 pattern = toks[1]
4218 def hook(data_ranges):
4219 for k in pyrocko.util.match_nslcs(
4220 pattern, list(data_ranges.keys())):
4222 upd(data_ranges, k, vmin, vmax)
4224 self.set_scaling_hook(pattern, hook)
4226 elif command == 'goto':
4227 toks2 = line.split(None, 1)
4228 if len(toks2) == 2:
4229 arg = toks2[1]
4230 m = re.match(
4231 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4232 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4233 if m:
4234 tlen = None
4235 if not m.group(1):
4236 tlen = 12*32*24*60*60
4237 elif not m.group(2):
4238 tlen = 32*24*60*60
4239 elif not m.group(3):
4240 tlen = 24*60*60
4241 elif not m.group(4):
4242 tlen = 60*60
4243 elif not m.group(5):
4244 tlen = 60
4246 supl = '1970-01-01 00:00:00'
4247 if len(supl) > len(arg):
4248 arg = arg + supl[-(len(supl)-len(arg)):]
4249 t = pyrocko.util.str_to_time(arg)
4250 self.go_to_time(t, tlen=tlen)
4252 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4253 supl = '00:00:00'
4254 if len(supl) > len(arg):
4255 arg = arg + supl[-(len(supl)-len(arg)):]
4256 tmin, tmax = self.get_time_range()
4257 sdate = pyrocko.util.time_to_str(
4258 tmin/2.+tmax/2., format='%Y-%m-%d')
4259 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4260 self.go_to_time(t)
4262 elif arg == 'today':
4263 self.go_to_time(
4264 day_start(
4265 time.time()), tlen=24*60*60)
4267 elif arg == 'yesterday':
4268 self.go_to_time(
4269 day_start(
4270 time.time()-24*60*60), tlen=24*60*60)
4272 else:
4273 self.go_to_event_by_name(arg)
4275 else:
4276 raise PileViewerMainException(
4277 'No such command: %s' % command)
4279 except PileViewerMainException as e:
4280 error = str(e)
4281 hideit = False
4283 return clearit, hideit, error
4285 return PileViewerMain
4288PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4289GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4292class LineEditWithAbort(qw.QLineEdit):
4294 aborted = qc.pyqtSignal()
4295 history_down = qc.pyqtSignal()
4296 history_up = qc.pyqtSignal()
4298 def keyPressEvent(self, key_event):
4299 if key_event.key() == qc.Qt.Key_Escape:
4300 self.aborted.emit()
4301 elif key_event.key() == qc.Qt.Key_Down:
4302 self.history_down.emit()
4303 elif key_event.key() == qc.Qt.Key_Up:
4304 self.history_up.emit()
4305 else:
4306 return qw.QLineEdit.keyPressEvent(self, key_event)
4309class PileViewer(qw.QFrame):
4310 '''
4311 PileViewerMain + Controls + Inputline
4312 '''
4314 def __init__(
4315 self, pile,
4316 ntracks_shown_max=20,
4317 marker_editor_sortable=True,
4318 use_opengl=None,
4319 panel_parent=None,
4320 *args):
4322 qw.QFrame.__init__(self, *args)
4324 layout = qw.QGridLayout()
4325 layout.setContentsMargins(0, 0, 0, 0)
4326 layout.setSpacing(0)
4328 self.menu = PileViewerMenuBar(self)
4330 if use_opengl is None:
4331 use_opengl = is_macos
4333 if use_opengl:
4334 self.viewer = GLPileViewerMain(
4335 pile,
4336 ntracks_shown_max=ntracks_shown_max,
4337 panel_parent=panel_parent,
4338 menu=self.menu)
4339 else:
4340 self.viewer = PileViewerMain(
4341 pile,
4342 ntracks_shown_max=ntracks_shown_max,
4343 panel_parent=panel_parent,
4344 menu=self.menu)
4346 self.marker_editor_sortable = marker_editor_sortable
4348 # self.setFrameShape(qw.QFrame.StyledPanel)
4349 # self.setFrameShadow(qw.QFrame.Sunken)
4351 self.input_area = qw.QFrame(self)
4352 ia_layout = qw.QGridLayout()
4353 ia_layout.setContentsMargins(11, 11, 11, 11)
4354 self.input_area.setLayout(ia_layout)
4356 self.inputline = LineEditWithAbort(self.input_area)
4357 self.inputline.returnPressed.connect(
4358 self.inputline_returnpressed)
4359 self.inputline.editingFinished.connect(
4360 self.inputline_finished)
4361 self.inputline.aborted.connect(
4362 self.inputline_aborted)
4364 self.inputline.history_down.connect(
4365 lambda: self.step_through_history(1))
4366 self.inputline.history_up.connect(
4367 lambda: self.step_through_history(-1))
4369 self.inputline.textEdited.connect(
4370 self.inputline_changed)
4372 self.inputline.setPlaceholderText(
4373 u'Quick commands: e.g. \'c HH?\' to select channels. '
4374 u'Use ↑ or ↓ to navigate.')
4375 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4376 self.input_area.hide()
4377 self.history = None
4379 self.inputline_error_str = None
4381 self.inputline_error = qw.QLabel()
4382 self.inputline_error.hide()
4384 ia_layout.addWidget(self.inputline, 0, 0)
4385 ia_layout.addWidget(self.inputline_error, 1, 0)
4386 layout.addWidget(self.input_area, 0, 0, 1, 2)
4387 layout.addWidget(self.viewer, 1, 0)
4389 pb = Progressbars(self)
4390 layout.addWidget(pb, 2, 0, 1, 2)
4391 self.progressbars = pb
4393 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4394 self.scrollbar = scrollbar
4395 layout.addWidget(scrollbar, 1, 1)
4396 self.scrollbar.valueChanged.connect(
4397 self.scrollbar_changed)
4399 self.block_scrollbar_changes = False
4401 self.viewer.want_input.connect(
4402 self.inputline_show)
4403 self.viewer.tracks_range_changed.connect(
4404 self.tracks_range_changed)
4405 self.viewer.pile_has_changed_signal.connect(
4406 self.adjust_controls)
4407 self.viewer.about_to_close.connect(
4408 self.save_inputline_history)
4410 self.setLayout(layout)
4412 def cleanup(self):
4413 self.viewer.cleanup()
4415 def get_progressbars(self):
4416 return self.progressbars
4418 def inputline_show(self):
4419 if not self.history:
4420 self.load_inputline_history()
4422 self.input_area.show()
4423 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4424 self.inputline.selectAll()
4426 def inputline_set_error(self, string):
4427 self.inputline_error_str = string
4428 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4429 self.inputline.selectAll()
4430 self.inputline_error.setText(string)
4431 self.input_area.show()
4432 self.inputline_error.show()
4434 def inputline_clear_error(self):
4435 if self.inputline_error_str:
4436 self.inputline.setPalette(qw.QApplication.palette())
4437 self.inputline_error_str = None
4438 self.inputline_error.clear()
4439 self.inputline_error.hide()
4441 def inputline_changed(self, line):
4442 self.viewer.inputline_changed(str(line))
4443 self.inputline_clear_error()
4445 def inputline_returnpressed(self):
4446 line = str(self.inputline.text())
4447 clearit, hideit, error = self.viewer.inputline_finished(line)
4449 if error:
4450 self.inputline_set_error(error)
4452 line = line.strip()
4454 if line != '' and not error:
4455 if not (len(self.history) >= 1 and line == self.history[-1]):
4456 self.history.append(line)
4458 if clearit:
4460 self.inputline.blockSignals(True)
4461 qpat, qinp = self.viewer.get_quick_filter_patterns()
4462 if qpat is None:
4463 self.inputline.clear()
4464 else:
4465 self.inputline.setText(qinp)
4466 self.inputline.blockSignals(False)
4468 if hideit and not error:
4469 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4470 self.input_area.hide()
4472 self.hist_ind = len(self.history)
4474 def inputline_aborted(self):
4475 '''
4476 Hide the input line.
4477 '''
4478 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4479 self.hist_ind = len(self.history)
4480 self.input_area.hide()
4482 def save_inputline_history(self):
4483 '''
4484 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4485 '''
4486 if not self.history:
4487 return
4489 conf = pyrocko.config
4490 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4491 with open(fn_hist, 'w') as f:
4492 i = min(100, len(self.history))
4493 for c in self.history[-i:]:
4494 f.write('%s\n' % c)
4496 def load_inputline_history(self):
4497 '''
4498 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4499 '''
4500 conf = pyrocko.config
4501 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4502 if not os.path.exists(fn_hist):
4503 with open(fn_hist, 'w+') as f:
4504 f.write('\n')
4506 with open(fn_hist, 'r') as f:
4507 self.history = [line.strip() for line in f.readlines()]
4509 self.hist_ind = len(self.history)
4511 def step_through_history(self, ud=1):
4512 '''
4513 Step through input line history and set the input line text.
4514 '''
4515 n = len(self.history)
4516 self.hist_ind += ud
4517 self.hist_ind %= (n + 1)
4518 if len(self.history) != 0 and self.hist_ind != n:
4519 self.inputline.setText(self.history[self.hist_ind])
4520 else:
4521 self.inputline.setText('')
4523 def inputline_finished(self):
4524 pass
4526 def tracks_range_changed(self, ntracks, ilo, ihi):
4527 if self.block_scrollbar_changes:
4528 return
4530 self.scrollbar.blockSignals(True)
4531 self.scrollbar.setPageStep(ihi-ilo)
4532 vmax = max(0, ntracks-(ihi-ilo))
4533 self.scrollbar.setRange(0, vmax)
4534 self.scrollbar.setValue(ilo)
4535 self.scrollbar.setHidden(vmax == 0)
4536 self.scrollbar.blockSignals(False)
4538 def scrollbar_changed(self, value):
4539 self.block_scrollbar_changes = True
4540 ilo = value
4541 ihi = ilo + self.scrollbar.pageStep()
4542 self.viewer.set_tracks_range((ilo, ihi))
4543 self.block_scrollbar_changes = False
4544 self.update_contents()
4546 def controls(self):
4547 frame = qw.QFrame(self)
4548 layout = qw.QGridLayout()
4549 frame.setLayout(layout)
4551 minfreq = 0.001
4552 maxfreq = 1000.0
4553 self.lowpass_control = ValControl(high_is_none=True)
4554 self.lowpass_control.setup(
4555 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4556 self.highpass_control = ValControl(low_is_none=True)
4557 self.highpass_control.setup(
4558 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4559 self.gain_control = ValControl()
4560 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4561 self.rot_control = LinValControl()
4562 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4563 self.colorbar_control = ColorbarControl(self)
4565 self.lowpass_control.valchange.connect(
4566 self.viewer.lowpass_change)
4567 self.highpass_control.valchange.connect(
4568 self.viewer.highpass_change)
4569 self.gain_control.valchange.connect(
4570 self.viewer.gain_change)
4571 self.rot_control.valchange.connect(
4572 self.viewer.rot_change)
4573 self.colorbar_control.cmap_changed.connect(
4574 self.viewer.waterfall_cmap_change
4575 )
4576 self.colorbar_control.clip_changed.connect(
4577 self.viewer.waterfall_clip_change
4578 )
4579 self.colorbar_control.show_absolute_toggled.connect(
4580 self.viewer.waterfall_show_absolute_change
4581 )
4582 self.colorbar_control.show_integrate_toggled.connect(
4583 self.viewer.waterfall_set_integrate
4584 )
4586 for icontrol, control in enumerate((
4587 self.highpass_control,
4588 self.lowpass_control,
4589 self.gain_control,
4590 self.rot_control,
4591 self.colorbar_control)):
4593 for iwidget, widget in enumerate(control.widgets()):
4594 layout.addWidget(widget, icontrol, iwidget)
4596 spacer = qw.QSpacerItem(
4597 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4598 layout.addItem(spacer, 4, 0, 1, 3)
4600 self.adjust_controls()
4601 self.viewer.viewmode_change(ViewMode.Wiggle)
4602 return frame
4604 def marker_editor(self):
4605 editor = pyrocko.gui.snuffler.marker_editor.MarkerEditor(
4606 self, sortable=self.marker_editor_sortable)
4608 editor.set_viewer(self.get_view())
4609 editor.get_marker_model().dataChanged.connect(
4610 self.update_contents)
4611 return editor
4613 def adjust_controls(self):
4614 dtmin, dtmax = self.viewer.content_deltat_range()
4615 maxfreq = 0.5/dtmin
4616 minfreq = (0.5/dtmax)*0.0001
4617 self.lowpass_control.set_range(minfreq, maxfreq)
4618 self.highpass_control.set_range(minfreq, maxfreq)
4620 def setup_snufflings(self):
4621 self.viewer.setup_snufflings()
4623 def get_view(self):
4624 return self.viewer
4626 def update_contents(self):
4627 self.viewer.update()
4629 def get_pile(self):
4630 return self.viewer.get_pile()
4632 def show_colorbar_ctrl(self, show):
4633 for w in self.colorbar_control.widgets():
4634 w.setVisible(show)
4636 def show_gain_ctrl(self, show):
4637 for w in self.gain_control.widgets():
4638 w.setVisible(show)