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()
1182 self.pile.add_listener(self)
1183 self.trace_styles = {}
1184 if self.get_squirrel() is None:
1185 self.determine_box_styles()
1187 self.setMouseTracking(True)
1189 user_home_dir = os.path.expanduser('~')
1190 self.snuffling_modules = {}
1191 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1192 self.default_snufflings = None
1193 self.snufflings = []
1195 self.stations = {}
1197 self.timer_draw = Timer()
1198 self.timer_cutout = Timer()
1199 self.time_spent_painting = 0.0
1200 self.time_last_painted = time.time()
1202 self.interactive_range_change_time = 0.0
1203 self.interactive_range_change_delay_time = 10.0
1204 self.follow_timer = None
1206 self.sortingmode_change_time = 0.0
1207 self.sortingmode_change_delay_time = None
1209 self.old_data_ranges = {}
1211 self.error_messages = {}
1212 self.return_tag = None
1213 self.wheel_pos = 60
1215 self.setAcceptDrops(True)
1216 self._paths_to_load = []
1218 self.tf_cache = {}
1220 self.waterfall = TraceWaterfall()
1221 self.waterfall_cmap = 'viridis'
1222 self.waterfall_clip_min = 0.
1223 self.waterfall_clip_max = 1.
1224 self.waterfall_show_absolute = False
1225 self.waterfall_integrate = False
1226 self.view_mode = ViewMode.Wiggle
1228 self.automatic_updates = True
1230 self.closing = False
1231 self.in_paint_event = False
1233 def fail(self, reason):
1234 box = qw.QMessageBox(self)
1235 box.setText(reason)
1236 box.exec_()
1238 def set_trace_filter(self, filter_func):
1239 self.trace_filter = filter_func
1240 self.sortingmode_change()
1242 def update_trace_filter(self):
1243 if self.blacklist:
1245 def blacklist_func(tr):
1246 return not pyrocko.util.match_nslc(
1247 self.blacklist, tr.nslc_id)
1249 else:
1250 blacklist_func = None
1252 if self.quick_filter is None and blacklist_func is None:
1253 self.set_trace_filter(None)
1254 elif self.quick_filter is None:
1255 self.set_trace_filter(blacklist_func)
1256 elif blacklist_func is None:
1257 self.set_trace_filter(self.quick_filter)
1258 else:
1259 self.set_trace_filter(
1260 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1262 def set_quick_filter(self, filter_func):
1263 self.quick_filter = filter_func
1264 self.update_trace_filter()
1266 def set_quick_filter_patterns(self, patterns, inputline=None):
1267 if patterns is not None:
1268 self.set_quick_filter(
1269 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1270 else:
1271 self.set_quick_filter(None)
1273 self.quick_filter_patterns = patterns, inputline
1275 def get_quick_filter_patterns(self):
1276 return self.quick_filter_patterns
1278 def add_blacklist_pattern(self, pattern):
1279 if pattern == 'empty':
1280 keys = set(self.pile.nslc_ids)
1281 trs = self.pile.all(
1282 tmin=self.tmin,
1283 tmax=self.tmax,
1284 load_data=False,
1285 degap=False)
1287 for tr in trs:
1288 if tr.nslc_id in keys:
1289 keys.remove(tr.nslc_id)
1291 for key in keys:
1292 xpattern = '.'.join(key)
1293 if xpattern not in self.blacklist:
1294 self.blacklist.append(xpattern)
1296 else:
1297 if pattern in self.blacklist:
1298 self.blacklist.remove(pattern)
1300 self.blacklist.append(pattern)
1302 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1303 self.update_trace_filter()
1305 def remove_blacklist_pattern(self, pattern):
1306 if pattern in self.blacklist:
1307 self.blacklist.remove(pattern)
1308 else:
1309 raise PileViewerMainException(
1310 'Pattern not found in blacklist.')
1312 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1313 self.update_trace_filter()
1315 def clear_blacklist(self):
1316 self.blacklist = []
1317 self.update_trace_filter()
1319 def ssort(self, tr):
1320 return self._ssort(tr)
1322 def station_key(self, x):
1323 return x.network, x.station
1325 def station_keys(self, x):
1326 return [
1327 (x.network, x.station, x.location),
1328 (x.network, x.station)]
1330 def station_attrib(self, tr, getter, default_getter):
1331 for sk in self.station_keys(tr):
1332 if sk in self.stations:
1333 station = self.stations[sk]
1334 return getter(station)
1336 return default_getter(tr)
1338 def get_station(self, sk):
1339 return self.stations[sk]
1341 def has_station(self, station):
1342 for sk in self.station_keys(station):
1343 if sk in self.stations:
1344 return True
1346 return False
1348 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1349 return self.station_attrib(
1350 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1352 def set_stations(self, stations):
1353 self.stations = {}
1354 self.add_stations(stations)
1356 def add_stations(self, stations):
1357 for station in stations:
1358 for sk in self.station_keys(station):
1359 self.stations[sk] = station
1361 ev = self.get_active_event()
1362 if ev:
1363 self.set_origin(ev)
1365 def add_event(self, event):
1366 marker = EventMarker(event)
1367 self.add_marker(marker)
1369 def add_events(self, events):
1370 markers = [EventMarker(e) for e in events]
1371 self.add_markers(markers)
1373 def set_event_marker_as_origin(self, ignore=None):
1374 selected = self.selected_markers()
1375 if not selected:
1376 self.fail('An event marker must be selected.')
1377 return
1379 m = selected[0]
1380 if not isinstance(m, EventMarker):
1381 self.fail('Selected marker is not an event.')
1382 return
1384 self.set_active_event_marker(m)
1386 def deactivate_event_marker(self):
1387 if self.active_event_marker:
1388 self.active_event_marker.active = False
1390 self.active_event_marker_changed.emit()
1391 self.active_event_marker = None
1393 def set_active_event_marker(self, event_marker):
1394 if self.active_event_marker:
1395 self.active_event_marker.active = False
1397 self.active_event_marker = event_marker
1398 event_marker.active = True
1399 event = event_marker.get_event()
1400 self.set_origin(event)
1401 self.active_event_marker_changed.emit()
1403 def set_active_event(self, event):
1404 for marker in self.markers:
1405 if isinstance(marker, EventMarker):
1406 if marker.get_event() is event:
1407 self.set_active_event_marker(marker)
1409 def get_active_event_marker(self):
1410 return self.active_event_marker
1412 def get_active_event(self):
1413 m = self.get_active_event_marker()
1414 if m is not None:
1415 return m.get_event()
1416 else:
1417 return None
1419 def get_active_markers(self):
1420 emarker = self.get_active_event_marker()
1421 if emarker is None:
1422 return None, []
1424 else:
1425 ev = emarker.get_event()
1426 pmarkers = [
1427 m for m in self.markers
1428 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1430 return emarker, pmarkers
1432 def set_origin(self, location):
1433 for station in self.stations.values():
1434 station.set_event_relative_data(
1435 location,
1436 distance_3d=self.menuitem_distances_3d.isChecked())
1438 self.sortingmode_change()
1440 def distances_3d_changed(self):
1441 ignore = self.menuitem_distances_3d.isChecked()
1442 self.set_event_marker_as_origin(ignore)
1444 def iter_snuffling_modules(self):
1445 pjoin = os.path.join
1446 for path in self.snuffling_paths:
1448 if not os.path.isdir(path):
1449 os.mkdir(path)
1451 for entry in os.listdir(path):
1452 directory = path
1453 fn = entry
1454 d = pjoin(path, entry)
1455 if os.path.isdir(d):
1456 directory = d
1457 if os.path.isfile(
1458 os.path.join(directory, 'snuffling.py')):
1459 fn = 'snuffling.py'
1461 if not fn.endswith('.py'):
1462 continue
1464 name = fn[:-3]
1466 if (directory, name) not in self.snuffling_modules:
1467 self.snuffling_modules[directory, name] = \
1468 pyrocko.gui.snuffler.snuffling.SnufflingModule(
1469 directory, name, self)
1471 yield self.snuffling_modules[directory, name]
1473 def setup_snufflings(self):
1474 # user snufflings
1475 from pyrocko.gui.snuffler.snuffling import BrokenSnufflingModule
1476 for mod in self.iter_snuffling_modules():
1477 try:
1478 mod.load_if_needed()
1479 except BrokenSnufflingModule as e:
1480 logger.warning('Snuffling module "%s" is broken' % e)
1482 # load the default snufflings on first run
1483 if self.default_snufflings is None:
1484 self.default_snufflings = pyrocko.gui.snuffler\
1485 .snufflings.__snufflings__()
1486 for snuffling in self.default_snufflings:
1487 self.add_snuffling(snuffling)
1489 def set_panel_parent(self, panel_parent):
1490 self.panel_parent = panel_parent
1492 def get_panel_parent(self):
1493 return self.panel_parent
1495 def add_snuffling(self, snuffling, reloaded=False):
1496 logger.debug('Adding snuffling %s' % snuffling.get_name())
1497 snuffling.init_gui(
1498 self, self.get_panel_parent(), self, reloaded=reloaded)
1499 self.snufflings.append(snuffling)
1500 self.update()
1502 def remove_snuffling(self, snuffling):
1503 snuffling.delete_gui()
1504 self.update()
1505 self.snufflings.remove(snuffling)
1506 snuffling.pre_destroy()
1508 def add_snuffling_menuitem(self, item):
1509 self.snufflings_menu.addAction(item)
1510 item.setParent(self.snufflings_menu)
1511 sort_actions(self.snufflings_menu)
1513 def remove_snuffling_menuitem(self, item):
1514 self.snufflings_menu.removeAction(item)
1516 def add_snuffling_help_menuitem(self, item):
1517 self.snuffling_help.addAction(item)
1518 item.setParent(self.snuffling_help)
1519 sort_actions(self.snuffling_help)
1521 def remove_snuffling_help_menuitem(self, item):
1522 self.snuffling_help.removeAction(item)
1524 def add_panel_toggler(self, item):
1525 self.toggle_panel_menu.addAction(item)
1526 item.setParent(self.toggle_panel_menu)
1527 sort_actions(self.toggle_panel_menu)
1529 def remove_panel_toggler(self, item):
1530 self.toggle_panel_menu.removeAction(item)
1532 def load(self, paths, regex=None, format='detect',
1533 cache_dir=None, force_cache=False):
1535 if cache_dir is None:
1536 cache_dir = pyrocko.config.config().cache_dir
1537 if isinstance(paths, str):
1538 paths = [paths]
1540 fns = pyrocko.util.select_files(
1541 paths, selector=None, include=regex, show_progress=False)
1543 if not fns:
1544 return
1546 cache = pyrocko.pile.get_cache(cache_dir)
1548 t = [time.time()]
1550 def update_bar(label, value):
1551 pbs = self.parent().get_progressbars()
1552 if label.lower() == 'looking at files':
1553 label = 'Looking at %i files' % len(fns)
1554 else:
1555 label = 'Scanning %i files' % len(fns)
1557 return pbs.set_status(label, value)
1559 def update_progress(label, i, n):
1560 abort = False
1562 qw.qApp.processEvents()
1563 if n != 0:
1564 perc = i*100/n
1565 else:
1566 perc = 100
1567 abort |= update_bar(label, perc)
1568 abort |= self.window().is_closing()
1570 tnow = time.time()
1571 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1572 self.update()
1573 t[0] = tnow
1575 return abort
1577 self.automatic_updates = False
1579 self.pile.load_files(
1580 sorted(fns),
1581 filename_attributes=regex,
1582 cache=cache,
1583 fileformat=format,
1584 show_progress=False,
1585 update_progress=update_progress)
1587 self.automatic_updates = True
1588 self.update()
1590 def load_queued(self):
1591 if not self._paths_to_load:
1592 return
1593 paths = self._paths_to_load
1594 self._paths_to_load = []
1595 self.load(paths)
1597 def load_soon(self, paths):
1598 self._paths_to_load.extend(paths)
1599 qc.QTimer.singleShot(200, self.load_queued)
1601 def open_waveforms(self):
1602 caption = 'Select one or more files to open'
1604 fns, _ = qw.QFileDialog.getOpenFileNames(
1605 self, caption, options=qfiledialog_options)
1607 if fns:
1608 self.load(list(str(fn) for fn in fns))
1610 def open_waveform_directory(self):
1611 caption = 'Select directory to scan for waveform files'
1613 dn = qw.QFileDialog.getExistingDirectory(
1614 self, caption, options=qfiledialog_options)
1616 if dn:
1617 self.load([str(dn)])
1619 def open_stations(self, fns=None):
1620 caption = 'Select one or more Pyrocko station files to open'
1622 if not fns:
1623 fns, _ = qw.QFileDialog.getOpenFileNames(
1624 self, caption, options=qfiledialog_options)
1626 try:
1627 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1628 for stat in stations:
1629 self.add_stations(stat)
1631 except Exception as e:
1632 self.fail('Failed to read station file: %s' % str(e))
1634 def open_stations_xml(self, fns=None):
1635 from pyrocko.io import stationxml
1637 caption = 'Select one or more StationXML files'
1638 if not fns:
1639 fns, _ = qw.QFileDialog.getOpenFileNames(
1640 self, caption, options=qfiledialog_options,
1641 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1642 ';;All files (*)')
1644 try:
1645 stations = [
1646 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1647 for x in fns]
1649 for stat in stations:
1650 self.add_stations(stat)
1652 except Exception as e:
1653 self.fail('Failed to read StationXML file: %s' % str(e))
1655 def add_traces(self, traces):
1656 if traces:
1657 mtf = pyrocko.pile.MemTracesFile(None, traces)
1658 self.pile.add_file(mtf)
1659 ticket = (self.pile, mtf)
1660 return ticket
1661 else:
1662 return (None, None)
1664 def release_data(self, tickets):
1665 for ticket in tickets:
1666 pile, mtf = ticket
1667 if pile is not None:
1668 pile.remove_file(mtf)
1670 def periodical(self):
1671 if self.menuitem_watch.isChecked():
1672 if self.pile.reload_modified():
1673 self.update()
1675 def get_pile(self):
1676 return self.pile
1678 def pile_changed(self, what):
1679 self.pile_has_changed = True
1680 self.pile_has_changed_signal.emit()
1681 if self.automatic_updates:
1682 self.update()
1684 def set_gathering(self, gather=None, color=None):
1686 if gather is None:
1687 def gather_func(tr):
1688 return tr.nslc_id
1690 gather = (0, 1, 2, 3)
1692 else:
1693 def gather_func(tr):
1694 return (
1695 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1697 if color is None:
1698 def color(tr):
1699 return tr.location
1701 self.gather = gather_func
1702 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1704 self.color_gather = color
1705 self.color_keys = self.pile.gather_keys(color)
1706 previous_ntracks = self.ntracks
1707 self.set_ntracks(len(keys))
1709 if self.shown_tracks_range is None or \
1710 previous_ntracks == 0 or \
1711 self.show_all:
1713 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1714 key_at_top = None
1715 n = high-low
1717 else:
1718 low, high = self.shown_tracks_range
1719 key_at_top = self.track_keys[low]
1720 n = high-low
1722 self.track_keys = sorted(keys)
1724 track_patterns = []
1725 for k in self.track_keys:
1726 pat = ['*', '*', '*', '*']
1727 for i, j in enumerate(gather):
1728 pat[j] = k[-len(gather)+i]
1730 track_patterns.append(pat)
1732 self.track_patterns = track_patterns
1734 if key_at_top is not None:
1735 try:
1736 ind = self.track_keys.index(key_at_top)
1737 low = ind
1738 high = low+n
1739 except Exception:
1740 pass
1742 self.set_tracks_range((low, high))
1744 self.key_to_row = dict(
1745 [(key, i) for (i, key) in enumerate(self.track_keys)])
1747 def inrange(x, r):
1748 return r[0] <= x and x < r[1]
1750 def trace_selector(trace):
1751 gt = self.gather(trace)
1752 return (
1753 gt in self.key_to_row and
1754 inrange(self.key_to_row[gt], self.shown_tracks_range))
1756 self.trace_selector = lambda x: \
1757 (self.trace_filter is None or self.trace_filter(x)) \
1758 and trace_selector(x)
1760 if self.tmin == working_system_time_range[0] and \
1761 self.tmax == working_system_time_range[1] or \
1762 self.show_all:
1764 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1765 if tmin is not None and tmax is not None:
1766 tlen = (tmax - tmin)
1767 tpad = tlen * 5./self.width()
1768 self.set_time_range(tmin-tpad, tmax+tpad)
1770 def set_time_range(self, tmin, tmax):
1771 if tmin is None:
1772 tmin = initial_time_range[0]
1774 if tmax is None:
1775 tmax = initial_time_range[1]
1777 if tmin > tmax:
1778 tmin, tmax = tmax, tmin
1780 if tmin == tmax:
1781 tmin -= 1.
1782 tmax += 1.
1784 tmin = max(working_system_time_range[0], tmin)
1785 tmax = min(working_system_time_range[1], tmax)
1787 min_deltat = self.content_deltat_range()[0]
1788 if (tmax - tmin < min_deltat):
1789 m = (tmin + tmax) / 2.
1790 tmin = m - min_deltat/2.
1791 tmax = m + min_deltat/2.
1793 self.time_projection.set_in_range(tmin, tmax)
1794 self.tmin, self.tmax = tmin, tmax
1796 def get_time_range(self):
1797 return self.tmin, self.tmax
1799 def ypart(self, y):
1800 if y < self.ax_height:
1801 return -1
1802 elif y > self.height()-self.ax_height:
1803 return 1
1804 else:
1805 return 0
1807 def time_fractional_digits(self):
1808 min_deltat = self.content_deltat_range()[0]
1809 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1811 def write_markers(self, fn=None):
1812 caption = "Choose a file name to write markers"
1813 if not fn:
1814 fn, _ = qw.QFileDialog.getSaveFileName(
1815 self, caption, options=qfiledialog_options)
1816 if fn:
1817 try:
1818 Marker.save_markers(
1819 self.markers, fn,
1820 fdigits=self.time_fractional_digits())
1822 except Exception as e:
1823 self.fail('Failed to write marker file: %s' % str(e))
1825 def write_selected_markers(self, fn=None):
1826 caption = "Choose a file name to write selected markers"
1827 if not fn:
1828 fn, _ = qw.QFileDialog.getSaveFileName(
1829 self, caption, options=qfiledialog_options)
1830 if fn:
1831 try:
1832 Marker.save_markers(
1833 self.iter_selected_markers(),
1834 fn,
1835 fdigits=self.time_fractional_digits())
1837 except Exception as e:
1838 self.fail('Failed to write marker file: %s' % str(e))
1840 def read_events(self, fn=None):
1841 '''
1842 Open QFileDialog to open, read and add
1843 :py:class:`pyrocko.model.Event` instances and their marker
1844 representation to the pile viewer.
1845 '''
1846 caption = "Selet one or more files to open"
1847 if not fn:
1848 fn, _ = qw.QFileDialog.getOpenFileName(
1849 self, caption, options=qfiledialog_options)
1850 if fn:
1851 try:
1852 self.add_events(pyrocko.model.load_events(fn))
1853 self.associate_phases_to_events()
1855 except Exception as e:
1856 self.fail('Failed to read event file: %s' % str(e))
1858 def read_markers(self, fn=None):
1859 '''
1860 Open QFileDialog to open, read and add markers to the pile viewer.
1861 '''
1862 caption = "Selet one or more marker files to open"
1863 if not fn:
1864 fn, _ = qw.QFileDialog.getOpenFileName(
1865 self, caption, options=qfiledialog_options)
1866 if fn:
1867 try:
1868 self.add_markers(Marker.load_markers(fn))
1869 self.associate_phases_to_events()
1871 except Exception as e:
1872 self.fail('Failed to read marker file: %s' % str(e))
1874 def associate_phases_to_events(self):
1875 associate_phases_to_events(self.markers)
1877 def add_marker(self, marker):
1878 # need index to inform QAbstactTableModel about upcoming change,
1879 # but have to restore current state in order to not cause problems
1880 self.markers.insert(marker)
1881 i = self.markers.remove(marker)
1883 self.begin_markers_add.emit(i, i)
1884 self.markers.insert(marker)
1885 self.end_markers_add.emit()
1886 self.markers_deltat_max = max(
1887 self.markers_deltat_max, marker.tmax - marker.tmin)
1889 def add_markers(self, markers):
1890 if not self.markers:
1891 self.begin_markers_add.emit(0, len(markers) - 1)
1892 self.markers.insert_many(markers)
1893 self.end_markers_add.emit()
1894 self.update_markers_deltat_max()
1895 else:
1896 for marker in markers:
1897 self.add_marker(marker)
1899 def update_markers_deltat_max(self):
1900 if self.markers:
1901 self.markers_deltat_max = max(
1902 marker.tmax - marker.tmin for marker in self.markers)
1904 def remove_marker(self, marker):
1905 '''
1906 Remove a ``marker`` from the :py:class:`PileViewer`.
1908 :param marker: :py:class:`Marker` (or subclass) instance
1909 '''
1911 if marker is self.active_event_marker:
1912 self.deactivate_event_marker()
1914 try:
1915 i = self.markers.index(marker)
1916 self.begin_markers_remove.emit(i, i)
1917 self.markers.remove_at(i)
1918 self.end_markers_remove.emit()
1919 except ValueError:
1920 pass
1922 def remove_markers(self, markers):
1923 '''
1924 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1926 :param markers: list of :py:class:`Marker` (or subclass)
1927 instances
1928 '''
1930 if markers is self.markers:
1931 markers = list(markers)
1933 for marker in markers:
1934 self.remove_marker(marker)
1936 self.update_markers_deltat_max()
1938 def remove_selected_markers(self):
1939 def delete_segment(istart, iend):
1940 self.begin_markers_remove.emit(istart, iend-1)
1941 for _ in range(iend - istart):
1942 self.markers.remove_at(istart)
1944 self.end_markers_remove.emit()
1946 istart = None
1947 ipos = 0
1948 markers = self.markers
1949 nmarkers = len(self.markers)
1950 while ipos < nmarkers:
1951 marker = markers[ipos]
1952 if marker.is_selected():
1953 if marker is self.active_event_marker:
1954 self.deactivate_event_marker()
1956 if istart is None:
1957 istart = ipos
1958 else:
1959 if istart is not None:
1960 delete_segment(istart, ipos)
1961 nmarkers -= ipos - istart
1962 ipos = istart - 1
1963 istart = None
1965 ipos += 1
1967 if istart is not None:
1968 delete_segment(istart, ipos)
1970 self.update_markers_deltat_max()
1972 def selected_markers(self):
1973 return [marker for marker in self.markers if marker.is_selected()]
1975 def iter_selected_markers(self):
1976 for marker in self.markers:
1977 if marker.is_selected():
1978 yield marker
1980 def get_markers(self):
1981 return self.markers
1983 def mousePressEvent(self, mouse_ev):
1984 self.show_all = False
1985 point = self.mapFromGlobal(mouse_ev.globalPos())
1987 if mouse_ev.button() == qc.Qt.LeftButton:
1988 marker = self.marker_under_cursor(point.x(), point.y())
1989 if self.picking:
1990 if self.picking_down is None:
1991 self.picking_down = (
1992 self.time_projection.rev(mouse_ev.x()),
1993 mouse_ev.y())
1995 elif marker is not None:
1996 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1997 self.deselect_all()
1998 marker.selected = True
1999 self.emit_selected_markers()
2000 self.update()
2001 else:
2002 self.track_start = mouse_ev.x(), mouse_ev.y()
2003 self.track_trange = self.tmin, self.tmax
2005 if mouse_ev.button() == qc.Qt.RightButton \
2006 and isinstance(self.menu, qw.QMenu):
2007 self.menu.exec_(qg.QCursor.pos())
2008 self.update_status()
2010 def mouseReleaseEvent(self, mouse_ev):
2011 if self.ignore_releases:
2012 self.ignore_releases -= 1
2013 return
2015 if self.picking:
2016 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2017 self.emit_selected_markers()
2019 if self.track_start:
2020 self.update()
2022 self.track_start = None
2023 self.track_trange = None
2024 self.update_status()
2026 def mouseDoubleClickEvent(self, mouse_ev):
2027 self.show_all = False
2028 self.start_picking(None)
2029 self.ignore_releases = 1
2031 def mouseMoveEvent(self, mouse_ev):
2032 point = self.mapFromGlobal(mouse_ev.globalPos())
2034 if self.picking:
2035 self.update_picking(point.x(), point.y())
2037 elif self.track_start is not None:
2038 x0, y0 = self.track_start
2039 dx = (point.x() - x0)/float(self.width())
2040 dy = (point.y() - y0)/float(self.height())
2041 if self.ypart(y0) == 1:
2042 dy = 0
2044 tmin0, tmax0 = self.track_trange
2046 scale = math.exp(-dy*5.)
2047 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2048 frac = x0/float(self.width())
2049 dt = dx*(tmax0-tmin0)*scale
2051 self.interrupt_following()
2052 self.set_time_range(
2053 tmin0 - dt - dtr*frac,
2054 tmax0 - dt + dtr*(1.-frac))
2056 self.update()
2057 else:
2058 self.hoovering(point.x(), point.y())
2060 self.update_status()
2062 def nslc_ids_under_cursor(self, x, y):
2063 ftrack = self.track_to_screen.rev(y)
2064 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2065 return nslc_ids
2067 def marker_under_cursor(self, x, y):
2068 mouset = self.time_projection.rev(x)
2069 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2070 relevant_nslc_ids = None
2071 for marker in self.markers:
2072 if marker.kind not in self.visible_marker_kinds:
2073 continue
2075 if (abs(mouset-marker.tmin) < deltat or
2076 abs(mouset-marker.tmax) < deltat):
2078 if relevant_nslc_ids is None:
2079 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2081 marker_nslc_ids = marker.get_nslc_ids()
2082 if not marker_nslc_ids:
2083 return marker
2085 for nslc_id in marker_nslc_ids:
2086 if nslc_id in relevant_nslc_ids:
2087 return marker
2089 def hoovering(self, x, y):
2090 mouset = self.time_projection.rev(x)
2091 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2092 needupdate = False
2093 haveone = False
2094 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2095 for marker in self.markers:
2096 if marker.kind not in self.visible_marker_kinds:
2097 continue
2099 state = abs(mouset-marker.tmin) < deltat or \
2100 abs(mouset-marker.tmax) < deltat and not haveone
2102 if state:
2103 xstate = False
2105 marker_nslc_ids = marker.get_nslc_ids()
2106 if not marker_nslc_ids:
2107 xstate = True
2109 for nslc in relevant_nslc_ids:
2110 if marker.match_nslc(nslc):
2111 xstate = True
2113 state = xstate
2115 if state:
2116 haveone = True
2117 oldstate = marker.is_alerted()
2118 if oldstate != state:
2119 needupdate = True
2120 marker.set_alerted(state)
2121 if state:
2122 self.message = marker.hoover_message()
2124 if not haveone:
2125 self.message = None
2127 if needupdate:
2128 self.update()
2130 def event(self, event):
2131 if event.type() == qc.QEvent.KeyPress:
2132 self.keyPressEvent(event)
2133 return True
2134 else:
2135 return base.event(self, event)
2137 def keyPressEvent(self, key_event):
2138 self.show_all = False
2139 dt = self.tmax - self.tmin
2140 tmid = (self.tmin + self.tmax) / 2.
2142 key = key_event.key()
2143 try:
2144 keytext = str(key_event.text())
2145 except UnicodeEncodeError:
2146 return
2148 if key == qc.Qt.Key_Space:
2149 self.interrupt_following()
2150 self.set_time_range(self.tmin+dt, self.tmax+dt)
2152 elif key == qc.Qt.Key_Up:
2153 for m in self.selected_markers():
2154 if isinstance(m, PhaseMarker):
2155 if key_event.modifiers() & qc.Qt.ShiftModifier:
2156 p = 0
2157 else:
2158 p = 1 if m.get_polarity() != 1 else None
2159 m.set_polarity(p)
2161 elif key == qc.Qt.Key_Down:
2162 for m in self.selected_markers():
2163 if isinstance(m, PhaseMarker):
2164 if key_event.modifiers() & qc.Qt.ShiftModifier:
2165 p = 0
2166 else:
2167 p = -1 if m.get_polarity() != -1 else None
2168 m.set_polarity(p)
2170 elif key == qc.Qt.Key_B:
2171 dt = self.tmax - self.tmin
2172 self.interrupt_following()
2173 self.set_time_range(self.tmin-dt, self.tmax-dt)
2175 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2176 self.interrupt_following()
2178 tgo = None
2180 class TraceDummy(object):
2181 def __init__(self, marker):
2182 self._marker = marker
2184 @property
2185 def nslc_id(self):
2186 return self._marker.one_nslc()
2188 def marker_to_itrack(marker):
2189 try:
2190 return self.key_to_row.get(
2191 self.gather(TraceDummy(marker)), -1)
2193 except MarkerOneNSLCRequired:
2194 return -1
2196 emarker, pmarkers = self.get_active_markers()
2197 pmarkers = [
2198 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2199 pmarkers.sort(key=lambda m: (
2200 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2202 if key == qc.Qt.Key_Backtab:
2203 pmarkers.reverse()
2205 smarkers = self.selected_markers()
2206 iselected = []
2207 for sm in smarkers:
2208 try:
2209 iselected.append(pmarkers.index(sm))
2210 except ValueError:
2211 pass
2213 if iselected:
2214 icurrent = max(iselected) + 1
2215 else:
2216 icurrent = 0
2218 if icurrent < len(pmarkers):
2219 self.deselect_all()
2220 cmarker = pmarkers[icurrent]
2221 cmarker.selected = True
2222 tgo = cmarker.tmin
2223 if not self.tmin < tgo < self.tmax:
2224 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2226 itrack = marker_to_itrack(cmarker)
2227 if itrack != -1:
2228 if itrack < self.shown_tracks_range[0]:
2229 self.scroll_tracks(
2230 - (self.shown_tracks_range[0] - itrack))
2231 elif self.shown_tracks_range[1] <= itrack:
2232 self.scroll_tracks(
2233 itrack - self.shown_tracks_range[1]+1)
2235 if itrack not in self.track_to_nslc_ids:
2236 self.go_to_selection()
2238 elif keytext in ('p', 'n', 'P', 'N'):
2239 smarkers = self.selected_markers()
2240 tgo = None
2241 dir = str(keytext)
2242 if smarkers:
2243 tmid = smarkers[0].tmin
2244 for smarker in smarkers:
2245 if dir == 'n':
2246 tmid = max(smarker.tmin, tmid)
2247 else:
2248 tmid = min(smarker.tmin, tmid)
2250 tgo = tmid
2252 if dir.lower() == 'n':
2253 for marker in sorted(
2254 self.markers,
2255 key=operator.attrgetter('tmin')):
2257 t = marker.tmin
2258 if t > tmid and \
2259 marker.kind in self.visible_marker_kinds and \
2260 (dir == 'n' or
2261 isinstance(marker, EventMarker)):
2263 self.deselect_all()
2264 marker.selected = True
2265 tgo = t
2266 break
2267 else:
2268 for marker in sorted(
2269 self.markers,
2270 key=operator.attrgetter('tmin'),
2271 reverse=True):
2273 t = marker.tmin
2274 if t < tmid and \
2275 marker.kind in self.visible_marker_kinds and \
2276 (dir == 'p' or
2277 isinstance(marker, EventMarker)):
2278 self.deselect_all()
2279 marker.selected = True
2280 tgo = t
2281 break
2283 if tgo is not None:
2284 self.interrupt_following()
2285 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2287 elif keytext == 'r':
2288 if self.pile.reload_modified():
2289 self.reloaded = True
2291 elif keytext == 'R':
2292 self.setup_snufflings()
2294 elif key == qc.Qt.Key_Backspace:
2295 self.remove_selected_markers()
2297 elif keytext == 'a':
2298 for marker in self.markers:
2299 if ((self.tmin <= marker.tmin <= self.tmax or
2300 self.tmin <= marker.tmax <= self.tmax) and
2301 marker.kind in self.visible_marker_kinds):
2302 marker.selected = True
2303 else:
2304 marker.selected = False
2306 elif keytext == 'A':
2307 for marker in self.markers:
2308 if marker.kind in self.visible_marker_kinds:
2309 marker.selected = True
2311 elif keytext == 'd':
2312 self.deselect_all()
2314 elif keytext == 'E':
2315 self.deactivate_event_marker()
2317 elif keytext == 'e':
2318 markers = self.selected_markers()
2319 event_markers_in_spe = [
2320 marker for marker in markers
2321 if not isinstance(marker, PhaseMarker)]
2323 phase_markers = [
2324 marker for marker in markers
2325 if isinstance(marker, PhaseMarker)]
2327 if len(event_markers_in_spe) == 1:
2328 event_marker = event_markers_in_spe[0]
2329 if not isinstance(event_marker, EventMarker):
2330 nslcs = list(event_marker.nslc_ids)
2331 lat, lon = 0.0, 0.0
2332 old = self.get_active_event()
2333 if len(nslcs) == 1:
2334 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2335 elif old is not None:
2336 lat, lon = old.lat, old.lon
2338 event_marker.convert_to_event_marker(lat, lon)
2340 self.set_active_event_marker(event_marker)
2341 event = event_marker.get_event()
2342 for marker in phase_markers:
2343 marker.set_event(event)
2345 else:
2346 for marker in event_markers_in_spe:
2347 marker.convert_to_event_marker()
2349 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2350 for marker in self.selected_markers():
2351 marker.set_kind(int(keytext))
2352 self.emit_selected_markers()
2354 elif key in fkey_map:
2355 self.handle_fkeys(key)
2357 elif key == qc.Qt.Key_Escape:
2358 if self.picking:
2359 self.stop_picking(0, 0, abort=True)
2361 elif key == qc.Qt.Key_PageDown:
2362 self.scroll_tracks(
2363 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2365 elif key == qc.Qt.Key_PageUp:
2366 self.scroll_tracks(
2367 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2369 elif key == qc.Qt.Key_Plus:
2370 self.zoom_tracks(0., 1.)
2372 elif key == qc.Qt.Key_Minus:
2373 self.zoom_tracks(0., -1.)
2375 elif key == qc.Qt.Key_Equal:
2376 ntracks_shown = self.shown_tracks_range[1] - \
2377 self.shown_tracks_range[0]
2378 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2379 self.zoom_tracks(0., dtracks)
2381 elif key == qc.Qt.Key_Colon:
2382 self.want_input.emit()
2384 elif keytext == 'f':
2385 self.toggle_fullscreen()
2387 elif keytext == 'g':
2388 self.go_to_selection()
2390 elif keytext == 'G':
2391 self.go_to_selection(tight=True)
2393 elif keytext == 'm':
2394 self.toggle_marker_editor()
2396 elif keytext == 'c':
2397 self.toggle_main_controls()
2399 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2400 dir = 1
2401 amount = 1
2402 if key_event.key() == qc.Qt.Key_Left:
2403 dir = -1
2404 if key_event.modifiers() & qc.Qt.ShiftModifier:
2405 amount = 10
2406 self.nudge_selected_markers(dir*amount)
2407 else:
2408 super().keyPressEvent(key_event)
2410 if keytext != '' and keytext in 'degaApPnN':
2411 self.emit_selected_markers()
2413 self.update()
2414 self.update_status()
2416 def handle_fkeys(self, key):
2417 self.set_phase_kind(
2418 self.selected_markers(),
2419 fkey_map[key] + 1)
2420 self.emit_selected_markers()
2422 def emit_selected_markers(self):
2423 ibounds = []
2424 last_selected = False
2425 for imarker, marker in enumerate(self.markers):
2426 this_selected = marker.is_selected()
2427 if this_selected != last_selected:
2428 ibounds.append(imarker)
2430 last_selected = this_selected
2432 if last_selected:
2433 ibounds.append(len(self.markers))
2435 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2436 self.n_selected_markers = sum(
2437 chunk[1] - chunk[0] for chunk in chunks)
2438 self.marker_selection_changed.emit(chunks)
2440 def toggle_marker_editor(self):
2441 self.panel_parent.toggle_marker_editor()
2443 def toggle_main_controls(self):
2444 self.panel_parent.toggle_main_controls()
2446 def nudge_selected_markers(self, npixels):
2447 a, b = self.time_projection.ur
2448 c, d = self.time_projection.xr
2449 for marker in self.selected_markers():
2450 if not isinstance(marker, EventMarker):
2451 marker.tmin += npixels * (d-c)/b
2452 marker.tmax += npixels * (d-c)/b
2454 def toggle_fullscreen(self):
2455 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2456 self.window().windowState() & qc.Qt.WindowMaximized:
2457 self.window().showNormal()
2458 else:
2459 if is_macos:
2460 self.window().showMaximized()
2461 else:
2462 self.window().showFullScreen()
2464 def about(self):
2465 fn = pyrocko.util.data_file('snuffler.png')
2466 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2467 txt = f.read()
2468 label = qw.QLabel(txt % {'logo': fn})
2469 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2470 self.show_doc('About', [label], target='tab')
2472 def help(self):
2473 class MyScrollArea(qw.QScrollArea):
2475 def sizeHint(self):
2476 s = qc.QSize()
2477 s.setWidth(self.widget().sizeHint().width())
2478 s.setHeight(self.widget().sizeHint().height())
2479 return s
2481 with open(pyrocko.util.data_file(
2482 'snuffler_help.html')) as f:
2483 hcheat = qw.QLabel(f.read())
2485 with open(pyrocko.util.data_file(
2486 'snuffler_help_epilog.html')) as f:
2487 hepilog = qw.QLabel(f.read())
2489 for h in [hcheat, hepilog]:
2490 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2491 h.setWordWrap(True)
2493 self.show_doc('Help', [hcheat, hepilog], target='panel')
2495 def show_doc(self, name, labels, target='panel'):
2496 scroller = qw.QScrollArea()
2497 frame = qw.QFrame(scroller)
2498 frame.setLineWidth(0)
2499 layout = qw.QVBoxLayout()
2500 layout.setContentsMargins(0, 0, 0, 0)
2501 layout.setSpacing(0)
2502 frame.setLayout(layout)
2503 scroller.setWidget(frame)
2504 scroller.setWidgetResizable(True)
2505 frame.setBackgroundRole(qg.QPalette.Base)
2506 for h in labels:
2507 h.setParent(frame)
2508 h.setMargin(3)
2509 h.setTextInteractionFlags(
2510 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2511 h.setBackgroundRole(qg.QPalette.Base)
2512 layout.addWidget(h)
2513 h.linkActivated.connect(
2514 self.open_link)
2516 if self.panel_parent is not None:
2517 if target == 'panel':
2518 self.panel_parent.add_panel(
2519 name, scroller, True, volatile=False)
2520 else:
2521 self.panel_parent.add_tab(name, scroller)
2523 def open_link(self, link):
2524 qg.QDesktopServices.openUrl(qc.QUrl(link))
2526 def wheelEvent(self, wheel_event):
2527 self.wheel_pos += wheel_event.angleDelta().y()
2529 n = self.wheel_pos // 120
2530 self.wheel_pos = self.wheel_pos % 120
2531 if n == 0:
2532 return
2534 amount = max(
2535 1.,
2536 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2537 wdelta = amount * n
2539 trmin, trmax = self.track_to_screen.get_in_range()
2540 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2541 / (trmax-trmin)
2543 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2544 self.zoom_tracks(anchor, wdelta)
2545 else:
2546 self.scroll_tracks(-wdelta)
2548 def dragEnterEvent(self, event):
2549 if event.mimeData().hasUrls():
2550 if any(url.toLocalFile() for url in event.mimeData().urls()):
2551 event.setDropAction(qc.Qt.LinkAction)
2552 event.accept()
2554 def dropEvent(self, event):
2555 if event.mimeData().hasUrls():
2556 paths = list(
2557 str(url.toLocalFile()) for url in event.mimeData().urls())
2558 event.acceptProposedAction()
2559 self.load(paths)
2561 def get_phase_name(self, kind):
2562 return self.config.get_phase_name(kind)
2564 def set_phase_kind(self, markers, kind):
2565 phasename = self.get_phase_name(kind)
2567 for marker in markers:
2568 if isinstance(marker, PhaseMarker):
2569 if kind == 10:
2570 marker.convert_to_marker()
2571 else:
2572 marker.set_phasename(phasename)
2573 marker.set_event(self.get_active_event())
2575 elif isinstance(marker, EventMarker):
2576 pass
2578 else:
2579 if kind != 10:
2580 event = self.get_active_event()
2581 marker.convert_to_phase_marker(
2582 event, phasename, None, False)
2584 def set_ntracks(self, ntracks):
2585 if self.ntracks != ntracks:
2586 self.ntracks = ntracks
2587 if self.shown_tracks_range is not None:
2588 l, h = self.shown_tracks_range
2589 else:
2590 l, h = 0, self.ntracks
2592 self.tracks_range_changed.emit(self.ntracks, l, h)
2594 def set_tracks_range(self, range, start=None):
2596 low, high = range
2597 low = min(self.ntracks-1, low)
2598 high = min(self.ntracks, high)
2599 low = max(0, low)
2600 high = max(1, high)
2602 if start is None:
2603 start = float(low)
2605 if self.shown_tracks_range != (low, high):
2606 self.shown_tracks_range = low, high
2607 self.shown_tracks_start = start
2609 self.tracks_range_changed.emit(self.ntracks, low, high)
2611 def scroll_tracks(self, shift):
2612 shown = self.shown_tracks_range
2613 shiftmin = -shown[0]
2614 shiftmax = self.ntracks-shown[1]
2615 shift = max(shiftmin, shift)
2616 shift = min(shiftmax, shift)
2617 shown = shown[0] + shift, shown[1] + shift
2619 self.set_tracks_range((int(shown[0]), int(shown[1])))
2621 self.update()
2623 def zoom_tracks(self, anchor, delta):
2624 ntracks_shown = self.shown_tracks_range[1] \
2625 - self.shown_tracks_range[0]
2627 if (ntracks_shown == 1 and delta <= 0) or \
2628 (ntracks_shown == self.ntracks and delta >= 0):
2629 return
2631 ntracks_shown += int(round(delta))
2632 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2634 u = self.shown_tracks_start
2635 nu = max(0., u-anchor*delta)
2636 nv = nu + ntracks_shown
2637 if nv > self.ntracks:
2638 nu -= nv - self.ntracks
2639 nv -= nv - self.ntracks
2641 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2643 self.ntracks_shown_max = self.shown_tracks_range[1] \
2644 - self.shown_tracks_range[0]
2646 self.update()
2648 def content_time_range(self):
2649 pile = self.get_pile()
2650 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2651 if tmin is None:
2652 tmin = initial_time_range[0]
2653 if tmax is None:
2654 tmax = initial_time_range[1]
2656 return tmin, tmax
2658 def content_deltat_range(self):
2659 pile = self.get_pile()
2661 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2663 if deltatmin is None:
2664 deltatmin = 0.001
2666 if deltatmax is None:
2667 deltatmax = 1000.0
2669 return deltatmin, deltatmax
2671 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2672 if tmax < tmin:
2673 tmin, tmax = tmax, tmin
2675 deltatmin = self.content_deltat_range()[0]
2676 dt = deltatmin * self.visible_length * 0.95
2678 if dt == 0.0:
2679 dt = 1.0
2681 if tight:
2682 if tmax != tmin:
2683 dtm = tmax - tmin
2684 tmin -= dtm*0.1
2685 tmax += dtm*0.1
2686 return tmin, tmax
2687 else:
2688 tcenter = (tmin + tmax) / 2.
2689 tmin = tcenter - 0.5*dt
2690 tmax = tcenter + 0.5*dt
2691 return tmin, tmax
2693 if tmax-tmin < dt:
2694 vmin, vmax = self.get_time_range()
2695 dt = min(vmax - vmin, dt)
2697 tcenter = (tmin+tmax)/2.
2698 etmin, etmax = tmin, tmax
2699 tmin = min(etmin, tcenter - 0.5*dt)
2700 tmax = max(etmax, tcenter + 0.5*dt)
2701 dtm = tmax-tmin
2702 if etmin == tmin:
2703 tmin -= dtm*0.1
2704 if etmax == tmax:
2705 tmax += dtm*0.1
2707 else:
2708 dtm = tmax-tmin
2709 tmin -= dtm*0.1
2710 tmax += dtm*0.1
2712 return tmin, tmax
2714 def go_to_selection(self, tight=False):
2715 markers = self.selected_markers()
2716 if markers:
2717 tmax, tmin = self.content_time_range()
2718 for marker in markers:
2719 tmin = min(tmin, marker.tmin)
2720 tmax = max(tmax, marker.tmax)
2722 else:
2723 if tight:
2724 vmin, vmax = self.get_time_range()
2725 tmin = tmax = (vmin + vmax) / 2.
2726 else:
2727 tmin, tmax = self.content_time_range()
2729 tmin, tmax = self.make_good_looking_time_range(
2730 tmin, tmax, tight=tight)
2732 self.interrupt_following()
2733 self.set_time_range(tmin, tmax)
2734 self.update()
2736 def go_to_time(self, t, tlen=None):
2737 tmax = t
2738 if tlen is not None:
2739 tmax = t+tlen
2740 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2741 self.interrupt_following()
2742 self.set_time_range(tmin, tmax)
2743 self.update()
2745 def go_to_event_by_name(self, name):
2746 for marker in self.markers:
2747 if isinstance(marker, EventMarker):
2748 event = marker.get_event()
2749 if event.name and event.name.lower() == name.lower():
2750 tmin, tmax = self.make_good_looking_time_range(
2751 event.time, event.time)
2753 self.interrupt_following()
2754 self.set_time_range(tmin, tmax)
2756 def printit(self):
2757 from ..qt_compat import qprint
2758 printer = qprint.QPrinter()
2759 printer.setOrientation(qprint.QPrinter.Landscape)
2761 dialog = qprint.QPrintDialog(printer, self)
2762 dialog.setWindowTitle('Print')
2764 if dialog.exec_() != qw.QDialog.Accepted:
2765 return
2767 painter = qg.QPainter()
2768 painter.begin(printer)
2769 page = printer.pageRect()
2770 self.drawit(
2771 painter, printmode=False, w=page.width(), h=page.height())
2773 painter.end()
2775 def savesvg(self, fn=None):
2777 if not fn:
2778 fn, _ = qw.QFileDialog.getSaveFileName(
2779 self,
2780 'Save as SVG|PNG',
2781 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2782 'SVG|PNG (*.svg *.png)',
2783 options=qfiledialog_options)
2785 if fn == '':
2786 return
2788 fn = str(fn)
2790 if fn.lower().endswith('.svg'):
2791 try:
2792 w, h = 842, 595
2793 margin = 0.025
2794 m = max(w, h)*margin
2796 generator = qsvg.QSvgGenerator()
2797 generator.setFileName(fn)
2798 generator.setSize(qc.QSize(w, h))
2799 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2801 painter = qg.QPainter()
2802 painter.begin(generator)
2803 self.drawit(painter, printmode=False, w=w, h=h)
2804 painter.end()
2806 except Exception as e:
2807 self.fail('Failed to write SVG file: %s' % str(e))
2809 elif fn.lower().endswith('.png'):
2810 pixmap = self.grab()
2812 try:
2813 pixmap.save(fn)
2815 except Exception as e:
2816 self.fail('Failed to write PNG file: %s' % str(e))
2818 else:
2819 self.fail(
2820 'Unsupported file type: filename must end with ".svg" or '
2821 '".png".')
2823 def paintEvent(self, paint_ev):
2824 '''
2825 Called by QT whenever widget needs to be painted.
2826 '''
2827 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2828 # was called twice (by different threads?), causing segfaults.
2829 if self.in_paint_event:
2830 logger.warning('Blocking reentrant call to paintEvent().')
2831 return
2833 self.in_paint_event = True
2835 painter = qg.QPainter(self)
2837 if self.menuitem_antialias.isChecked():
2838 painter.setRenderHint(qg.QPainter.Antialiasing)
2840 self.drawit(painter)
2842 logger.debug(
2843 'Time spent drawing: '
2844 ' user:%.3f sys:%.3f children_user:%.3f'
2845 ' childred_sys:%.3f elapsed:%.3f' %
2846 (self.timer_draw - self.timer_cutout))
2848 logger.debug(
2849 'Time spent processing:'
2850 ' user:%.3f sys:%.3f children_user:%.3f'
2851 ' childred_sys:%.3f elapsed:%.3f' %
2852 self.timer_cutout.get())
2854 self.time_spent_painting = self.timer_draw.get()[-1]
2855 self.time_last_painted = time.time()
2856 self.in_paint_event = False
2858 def determine_box_styles(self):
2860 traces = list(self.pile.iter_traces())
2861 traces.sort(key=operator.attrgetter('full_id'))
2862 istyle = 0
2863 trace_styles = {}
2864 for itr, tr in enumerate(traces):
2865 if itr > 0:
2866 other = traces[itr-1]
2867 if not (
2868 other.nslc_id == tr.nslc_id
2869 and other.deltat == tr.deltat
2870 and abs(other.tmax - tr.tmin)
2871 < gap_lap_tolerance*tr.deltat):
2873 istyle += 1
2875 trace_styles[tr.full_id, tr.deltat] = istyle
2877 self.trace_styles = trace_styles
2879 def draw_trace_boxes(self, p, time_projection, track_projections):
2881 for v_projection in track_projections.values():
2882 v_projection.set_in_range(0., 1.)
2884 def selector(x):
2885 return x.overlaps(*time_projection.get_in_range())
2887 if self.trace_filter is not None:
2888 def tselector(x):
2889 return selector(x) and self.trace_filter(x)
2891 else:
2892 tselector = selector
2894 traces = list(self.pile.iter_traces(
2895 group_selector=selector, trace_selector=tselector))
2897 traces.sort(key=operator.attrgetter('full_id'))
2899 def drawbox(itrack, istyle, traces):
2900 v_projection = track_projections[itrack]
2901 dvmin = v_projection(0.)
2902 dvmax = v_projection(1.)
2903 dtmin = time_projection.clipped(traces[0].tmin, 0)
2904 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2906 style = box_styles[istyle % len(box_styles)]
2907 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2908 p.fillRect(rect, style.fill_brush)
2909 p.setPen(style.frame_pen)
2910 p.drawRect(rect)
2912 traces_by_style = {}
2913 for itr, tr in enumerate(traces):
2914 gt = self.gather(tr)
2915 if gt not in self.key_to_row:
2916 continue
2918 itrack = self.key_to_row[gt]
2919 if itrack not in track_projections:
2920 continue
2922 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2924 if len(traces) < 500:
2925 drawbox(itrack, istyle, [tr])
2926 else:
2927 if (itrack, istyle) not in traces_by_style:
2928 traces_by_style[itrack, istyle] = []
2929 traces_by_style[itrack, istyle].append(tr)
2931 for (itrack, istyle), traces in traces_by_style.items():
2932 drawbox(itrack, istyle, traces)
2934 def draw_visible_markers(
2935 self, p, vcenter_projection, primary_pen):
2937 try:
2938 markers = self.markers.with_key_in_limited(
2939 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2941 except pyrocko.pile.TooMany:
2942 tmin = self.markers[0].tmin
2943 tmax = self.markers[-1].tmax
2944 umin_view, umax_view = self.time_projection.get_out_range()
2945 umin = max(umin_view, self.time_projection(tmin))
2946 umax = min(umax_view, self.time_projection(tmax))
2947 v0, _ = vcenter_projection.get_out_range()
2948 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2950 p.save()
2952 pen = qg.QPen(primary_pen)
2953 pen.setWidth(2)
2954 pen.setStyle(qc.Qt.DotLine)
2955 # pat = [5., 3.]
2956 # pen.setDashPattern(pat)
2957 p.setPen(pen)
2959 if self.n_selected_markers == len(self.markers):
2960 s_selected = ' (all selected)'
2961 elif self.n_selected_markers > 0:
2962 s_selected = ' (%i selected)' % self.n_selected_markers
2963 else:
2964 s_selected = ''
2966 draw_label(
2967 p, umin+10., v0-10.,
2968 '%i Markers' % len(self.markers) + s_selected,
2969 label_bg, 'LB')
2971 line = qc.QLineF(umin, v0, umax, v0)
2972 p.drawLine(line)
2973 p.restore()
2975 return
2977 for marker in markers:
2978 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2979 and marker.kind in self.visible_marker_kinds:
2981 marker.draw(
2982 p, self.time_projection, vcenter_projection,
2983 with_label=True)
2985 def get_squirrel(self):
2986 try:
2987 return self.pile._squirrel
2988 except AttributeError:
2989 return None
2991 def draw_coverage(self, p, time_projection, track_projections):
2992 sq = self.get_squirrel()
2993 if sq is None:
2994 return
2996 def drawbox(itrack, tmin, tmax, style):
2997 v_projection = track_projections[itrack]
2998 dvmin = v_projection(0.)
2999 dvmax = v_projection(1.)
3000 dtmin = time_projection.clipped(tmin, 0)
3001 dtmax = time_projection.clipped(tmax, 1)
3003 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3004 p.fillRect(rect, style.fill_brush)
3005 p.setPen(style.frame_pen)
3006 p.drawRect(rect)
3008 pattern_list = []
3009 pattern_to_itrack = {}
3010 for key in self.track_keys:
3011 itrack = self.key_to_row[key]
3012 if itrack not in track_projections:
3013 continue
3015 pattern = self.track_patterns[itrack]
3016 pattern_to_itrack[tuple(pattern)] = itrack
3017 pattern_list.append(tuple(pattern))
3019 vmin, vmax = self.get_time_range()
3021 for kind in ['waveform', 'waveform_promise']:
3022 for coverage in sq.get_coverage(
3023 kind, vmin, vmax, pattern_list, limit=500):
3024 itrack = pattern_to_itrack[coverage.pattern.nslc]
3026 if coverage.changes is None:
3027 drawbox(
3028 itrack, coverage.tmin, coverage.tmax,
3029 box_styles_coverage[kind][0])
3030 else:
3031 t = None
3032 pcount = 0
3033 for tb, count in coverage.changes:
3034 if t is not None and tb > t:
3035 if pcount > 0:
3036 drawbox(
3037 itrack, t, tb,
3038 box_styles_coverage[kind][
3039 min(len(box_styles_coverage)-1,
3040 pcount)])
3042 t = tb
3043 pcount = count
3045 def drawit(self, p, printmode=False, w=None, h=None):
3046 '''
3047 This performs the actual drawing.
3048 '''
3050 self.timer_draw.start()
3051 show_boxes = self.menuitem_showboxes.isChecked()
3052 sq = self.get_squirrel()
3054 if self.gather is None:
3055 self.set_gathering()
3057 if self.pile_has_changed:
3059 if not self.sortingmode_change_delayed():
3060 self.sortingmode_change()
3062 if show_boxes and sq is None:
3063 self.determine_box_styles()
3065 self.pile_has_changed = False
3067 if h is None:
3068 h = float(self.height())
3069 if w is None:
3070 w = float(self.width())
3072 if printmode:
3073 primary_color = (0, 0, 0)
3074 else:
3075 primary_color = pyrocko.plot.tango_colors['aluminium5']
3077 primary_pen = qg.QPen(qg.QColor(*primary_color))
3079 ax_h = float(self.ax_height)
3081 vbottom_ax_projection = Projection()
3082 vtop_ax_projection = Projection()
3083 vcenter_projection = Projection()
3085 self.time_projection.set_out_range(0., w)
3086 vbottom_ax_projection.set_out_range(h-ax_h, h)
3087 vtop_ax_projection.set_out_range(0., ax_h)
3088 vcenter_projection.set_out_range(ax_h, h-ax_h)
3089 vcenter_projection.set_in_range(0., 1.)
3090 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3092 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3093 track_projections = {}
3094 for i in range(*self.shown_tracks_range):
3095 proj = Projection()
3096 proj.set_out_range(
3097 self.track_to_screen(i+0.05),
3098 self.track_to_screen(i+1.-0.05))
3100 track_projections[i] = proj
3102 if self.tmin > self.tmax:
3103 return
3105 self.time_projection.set_in_range(self.tmin, self.tmax)
3106 vbottom_ax_projection.set_in_range(0, ax_h)
3108 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3110 yscaler = pyrocko.plot.AutoScaler()
3112 p.setPen(primary_pen)
3114 font = qg.QFont()
3115 font.setBold(True)
3117 axannotfont = qg.QFont()
3118 axannotfont.setBold(True)
3119 axannotfont.setPointSize(8)
3121 processed_traces = self.prepare_cutout2(
3122 self.tmin, self.tmax,
3123 trace_selector=self.trace_selector,
3124 degap=self.menuitem_degap.isChecked(),
3125 demean=self.menuitem_demean.isChecked())
3127 if not printmode and show_boxes:
3128 if (self.view_mode is ViewMode.Wiggle) \
3129 or (self.view_mode is ViewMode.Waterfall
3130 and not processed_traces):
3132 if sq is None:
3133 self.draw_trace_boxes(
3134 p, self.time_projection, track_projections)
3136 else:
3137 self.draw_coverage(
3138 p, self.time_projection, track_projections)
3140 p.setFont(font)
3141 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3143 color_lookup = dict(
3144 [(k, i) for (i, k) in enumerate(self.color_keys)])
3146 self.track_to_nslc_ids = {}
3147 nticks = 0
3148 annot_labels = []
3150 if self.view_mode is ViewMode.Waterfall and processed_traces:
3151 waterfall = self.waterfall
3152 waterfall.set_time_range(self.tmin, self.tmax)
3153 waterfall.set_traces(processed_traces)
3154 waterfall.set_cmap(self.waterfall_cmap)
3155 waterfall.set_integrate(self.waterfall_integrate)
3156 waterfall.set_clip(
3157 self.waterfall_clip_min, self.waterfall_clip_max)
3158 waterfall.show_absolute_values(
3159 self.waterfall_show_absolute)
3161 rect = qc.QRectF(
3162 0, self.ax_height,
3163 self.width(), self.height() - self.ax_height*2
3164 )
3165 waterfall.draw_waterfall(p, rect=rect)
3167 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3168 show_scales = self.menuitem_showscalerange.isChecked() \
3169 or self.menuitem_showscaleaxis.isChecked()
3171 fm = qg.QFontMetrics(axannotfont, p.device())
3172 trackheight = self.track_to_screen(1.-0.05) \
3173 - self.track_to_screen(0.05)
3175 nlinesavail = trackheight/float(fm.lineSpacing())
3177 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3178 if self.menuitem_showscaleaxis.isChecked() \
3179 else 15
3181 yscaler = pyrocko.plot.AutoScaler(
3182 no_exp_interval=(-3, 2), approx_ticks=nticks,
3183 snap=show_scales
3184 and not self.menuitem_showscaleaxis.isChecked())
3186 data_ranges = pyrocko.trace.minmax(
3187 processed_traces,
3188 key=self.scaling_key,
3189 mode=self.scaling_base[0],
3190 outer_mode=self.scaling_base[1])
3192 if not self.menuitem_fixscalerange.isChecked():
3193 self.old_data_ranges = data_ranges
3194 else:
3195 data_ranges.update(self.old_data_ranges)
3197 self.apply_scaling_hooks(data_ranges)
3199 trace_to_itrack = {}
3200 track_scaling_keys = {}
3201 track_scaling_colors = {}
3202 for trace in processed_traces:
3203 gt = self.gather(trace)
3204 if gt not in self.key_to_row:
3205 continue
3207 itrack = self.key_to_row[gt]
3208 if itrack not in track_projections:
3209 continue
3211 trace_to_itrack[trace] = itrack
3213 if itrack not in self.track_to_nslc_ids:
3214 self.track_to_nslc_ids[itrack] = set()
3216 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3218 if itrack not in track_scaling_keys:
3219 track_scaling_keys[itrack] = set()
3221 scaling_key = self.scaling_key(trace)
3222 track_scaling_keys[itrack].add(scaling_key)
3224 color = pyrocko.plot.color(
3225 color_lookup[self.color_gather(trace)])
3227 k = itrack, scaling_key
3228 if k not in track_scaling_colors \
3229 and self.menuitem_colortraces.isChecked():
3230 track_scaling_colors[k] = color
3231 else:
3232 track_scaling_colors[k] = primary_color
3234 # y axes, zero lines
3235 trace_projections = {}
3236 for itrack in list(track_projections.keys()):
3237 if itrack not in track_scaling_keys:
3238 continue
3239 uoff = 0
3240 for scaling_key in track_scaling_keys[itrack]:
3241 data_range = data_ranges[scaling_key]
3242 dymin, dymax = data_range
3243 ymin, ymax, yinc = yscaler.make_scale(
3244 (dymin/self.gain, dymax/self.gain))
3245 iexp = yscaler.make_exp(yinc)
3246 factor = 10**iexp
3247 trace_projection = track_projections[itrack].copy()
3248 trace_projection.set_in_range(ymax, ymin)
3249 trace_projections[itrack, scaling_key] = \
3250 trace_projection
3251 umin, umax = self.time_projection.get_out_range()
3252 vmin, vmax = trace_projection.get_out_range()
3253 umax_zeroline = umax
3254 uoffnext = uoff
3256 if show_scales:
3257 pen = qg.QPen(primary_pen)
3258 k = itrack, scaling_key
3259 if k in track_scaling_colors:
3260 c = qg.QColor(*track_scaling_colors[
3261 itrack, scaling_key])
3263 pen.setColor(c)
3265 p.setPen(pen)
3266 if nlinesavail > 3:
3267 if self.menuitem_showscaleaxis.isChecked():
3268 ymin_annot = math.ceil(ymin/yinc)*yinc
3269 ny_annot = int(
3270 math.floor(ymax/yinc)
3271 - math.ceil(ymin/yinc)) + 1
3273 for iy_annot in range(ny_annot):
3274 y = ymin_annot + iy_annot*yinc
3275 v = trace_projection(y)
3276 line = qc.QLineF(
3277 umax-10-uoff, v, umax-uoff, v)
3279 p.drawLine(line)
3280 if iy_annot == ny_annot - 1 \
3281 and iexp != 0:
3282 sexp = ' × ' \
3283 '10<sup>%i</sup>' % iexp
3284 else:
3285 sexp = ''
3287 snum = num_to_html(y/factor)
3288 lab = Label(
3289 p,
3290 umax-20-uoff,
3291 v, '%s%s' % (snum, sexp),
3292 label_bg=None,
3293 anchor='MR',
3294 font=axannotfont,
3295 color=c)
3297 uoffnext = max(
3298 lab.rect.width()+30., uoffnext)
3300 annot_labels.append(lab)
3301 if y == 0.:
3302 umax_zeroline = \
3303 umax - 20 \
3304 - lab.rect.width() - 10 \
3305 - uoff
3306 else:
3307 if not show_boxes:
3308 qpoints = make_QPolygonF(
3309 [umax-20-uoff,
3310 umax-10-uoff,
3311 umax-10-uoff,
3312 umax-20-uoff],
3313 [vmax, vmax, vmin, vmin])
3314 p.drawPolyline(qpoints)
3316 snum = num_to_html(ymin)
3317 labmin = Label(
3318 p, umax-15-uoff, vmax, snum,
3319 label_bg=None,
3320 anchor='BR',
3321 font=axannotfont,
3322 color=c)
3324 annot_labels.append(labmin)
3325 snum = num_to_html(ymax)
3326 labmax = Label(
3327 p, umax-15-uoff, vmin, snum,
3328 label_bg=None,
3329 anchor='TR',
3330 font=axannotfont,
3331 color=c)
3333 annot_labels.append(labmax)
3335 for lab in (labmin, labmax):
3336 uoffnext = max(
3337 lab.rect.width()+10., uoffnext)
3339 if self.menuitem_showzeroline.isChecked():
3340 v = trace_projection(0.)
3341 if vmin <= v <= vmax:
3342 line = qc.QLineF(umin, v, umax_zeroline, v)
3343 p.drawLine(line)
3345 uoff = uoffnext
3347 p.setFont(font)
3348 p.setPen(primary_pen)
3349 for trace in processed_traces:
3350 if self.view_mode is not ViewMode.Wiggle:
3351 break
3353 if trace not in trace_to_itrack:
3354 continue
3356 itrack = trace_to_itrack[trace]
3357 scaling_key = self.scaling_key(trace)
3358 trace_projection = trace_projections[
3359 itrack, scaling_key]
3361 vdata = trace_projection(trace.get_ydata())
3363 udata_min = float(self.time_projection(trace.tmin))
3364 udata_max = float(self.time_projection(
3365 trace.tmin+trace.deltat*(vdata.size-1)))
3366 udata = num.linspace(udata_min, udata_max, vdata.size)
3368 qpoints = make_QPolygonF(udata, vdata)
3370 umin, umax = self.time_projection.get_out_range()
3371 vmin, vmax = trace_projection.get_out_range()
3373 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3375 if self.menuitem_cliptraces.isChecked():
3376 p.setClipRect(trackrect)
3378 if self.menuitem_colortraces.isChecked():
3379 color = pyrocko.plot.color(
3380 color_lookup[self.color_gather(trace)])
3381 pen = qg.QPen(qg.QColor(*color), 1)
3382 p.setPen(pen)
3384 p.drawPolyline(qpoints)
3386 if self.floating_marker:
3387 self.floating_marker.draw_trace(
3388 self, p, trace,
3389 self.time_projection, trace_projection, 1.0)
3391 for marker in self.markers.with_key_in(
3392 self.tmin - self.markers_deltat_max,
3393 self.tmax):
3395 if marker.tmin < self.tmax \
3396 and self.tmin < marker.tmax \
3397 and marker.kind \
3398 in self.visible_marker_kinds:
3399 marker.draw_trace(
3400 self, p, trace, self.time_projection,
3401 trace_projection, 1.0)
3403 p.setPen(primary_pen)
3405 if self.menuitem_cliptraces.isChecked():
3406 p.setClipRect(0, 0, int(w), int(h))
3408 if self.floating_marker:
3409 self.floating_marker.draw(
3410 p, self.time_projection, vcenter_projection)
3412 self.draw_visible_markers(
3413 p, vcenter_projection, primary_pen)
3415 p.setPen(primary_pen)
3416 while font.pointSize() > 2:
3417 fm = qg.QFontMetrics(font, p.device())
3418 trackheight = self.track_to_screen(1.-0.05) \
3419 - self.track_to_screen(0.05)
3420 nlinesavail = trackheight/float(fm.lineSpacing())
3421 if nlinesavail > 1:
3422 break
3424 font.setPointSize(font.pointSize()-1)
3426 p.setFont(font)
3427 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3429 for key in self.track_keys:
3430 itrack = self.key_to_row[key]
3431 if itrack in track_projections:
3432 plabel = ' '.join(
3433 [str(x) for x in key if x is not None])
3434 lx = 10
3435 ly = self.track_to_screen(itrack+0.5)
3437 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3438 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3439 continue
3441 contains_cursor = \
3442 self.track_to_screen(itrack) \
3443 < mouse_pos.y() \
3444 < self.track_to_screen(itrack+1)
3446 if not contains_cursor:
3447 continue
3449 font_large = p.font()
3450 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3451 p.setFont(font_large)
3452 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3453 p.setFont(font)
3455 for lab in annot_labels:
3456 lab.draw()
3458 self.timer_draw.stop()
3460 def see_data_params(self):
3462 min_deltat = self.content_deltat_range()[0]
3464 # determine padding and downampling requirements
3465 if self.lowpass is not None:
3466 deltat_target = 1./self.lowpass * 0.25
3467 ndecimate = min(
3468 50,
3469 max(1, int(round(deltat_target / min_deltat))))
3470 tpad = 1./self.lowpass * 2.
3471 else:
3472 ndecimate = 1
3473 tpad = min_deltat*5.
3475 if self.highpass is not None:
3476 tpad = max(1./self.highpass * 2., tpad)
3478 nsee_points_per_trace = 5000*10
3479 tsee = ndecimate*nsee_points_per_trace*min_deltat
3481 return ndecimate, tpad, tsee
3483 def clean_update(self):
3484 self.cached_processed_traces = None
3485 self.update()
3487 def get_adequate_tpad(self):
3488 tpad = 0.
3489 for f in [self.highpass, self.lowpass]:
3490 if f is not None:
3491 tpad = max(tpad, 1.0/f)
3493 for snuffling in self.snufflings:
3494 if snuffling._post_process_hook_enabled \
3495 or snuffling._pre_process_hook_enabled:
3497 tpad = max(tpad, snuffling.get_tpad())
3499 return tpad
3501 def prepare_cutout2(
3502 self, tmin, tmax, trace_selector=None, degap=True,
3503 demean=True, nmax=6000):
3505 if self.pile.is_empty():
3506 return []
3508 nmax = self.visible_length
3510 self.timer_cutout.start()
3512 tsee = tmax-tmin
3513 min_deltat_wo_decimate = tsee/nmax
3514 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3516 min_deltat_allow = min_deltat_wo_decimate
3517 if self.lowpass is not None:
3518 target_deltat_lp = 0.25/self.lowpass
3519 if target_deltat_lp > min_deltat_wo_decimate:
3520 min_deltat_allow = min_deltat_w_decimate
3522 min_deltat_allow = math.exp(
3523 int(math.floor(math.log(min_deltat_allow))))
3525 tmin_ = tmin
3526 tmax_ = tmax
3528 # fetch more than needed?
3529 if self.menuitem_liberal_fetch.isChecked():
3530 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3531 tmin = math.floor(tmin/tlen) * tlen
3532 tmax = math.ceil(tmax/tlen) * tlen
3534 fft_filtering = self.menuitem_fft_filtering.isChecked()
3535 lphp = self.menuitem_lphp.isChecked()
3536 ads = self.menuitem_allowdownsampling.isChecked()
3538 tpad = self.get_adequate_tpad()
3539 tpad = max(tpad, tsee)
3541 # state vector to decide if cached traces can be used
3542 vec = (
3543 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3544 self.highpass, fft_filtering, lphp,
3545 min_deltat_allow, self.rotate, self.shown_tracks_range,
3546 ads, self.pile.get_update_count())
3548 if (self.cached_vec
3549 and self.cached_vec[0] <= vec[0]
3550 and vec[1] <= self.cached_vec[1]
3551 and vec[2:] == self.cached_vec[2:]
3552 and not (self.reloaded or self.menuitem_watch.isChecked())
3553 and self.cached_processed_traces is not None):
3555 logger.debug('Using cached traces')
3556 processed_traces = self.cached_processed_traces
3558 else:
3559 processed_traces = []
3560 if self.pile.deltatmax >= min_deltat_allow:
3562 if isinstance(self.pile, pyrocko.pile.Pile):
3563 def group_selector(gr):
3564 return gr.deltatmax >= min_deltat_allow
3566 kwargs = dict(group_selector=group_selector)
3567 else:
3568 kwargs = {}
3570 if trace_selector is not None:
3571 def trace_selectorx(tr):
3572 return tr.deltat >= min_deltat_allow \
3573 and trace_selector(tr)
3574 else:
3575 def trace_selectorx(tr):
3576 return tr.deltat >= min_deltat_allow
3578 for traces in self.pile.chopper(
3579 tmin=tmin, tmax=tmax, tpad=tpad,
3580 want_incomplete=True,
3581 degap=degap,
3582 maxgap=gap_lap_tolerance,
3583 maxlap=gap_lap_tolerance,
3584 keep_current_files_open=True,
3585 trace_selector=trace_selectorx,
3586 accessor_id=id(self),
3587 snap=(math.floor, math.ceil),
3588 include_last=True, **kwargs):
3590 if demean:
3591 for tr in traces:
3592 if (tr.meta and tr.meta.get('tabu', False)):
3593 continue
3594 y = tr.get_ydata()
3595 tr.set_ydata(y - num.mean(y))
3597 traces = self.pre_process_hooks(traces)
3599 for trace in traces:
3601 if not (trace.meta
3602 and trace.meta.get('tabu', False)):
3604 if fft_filtering:
3605 but = pyrocko.response.ButterworthResponse
3606 multres = pyrocko.response.MultiplyResponse
3607 if self.lowpass is not None \
3608 or self.highpass is not None:
3610 it = num.arange(
3611 trace.data_len(), dtype=float)
3612 detr_data, m, b = detrend(
3613 it, trace.get_ydata())
3615 trace.set_ydata(detr_data)
3617 freqs, fdata = trace.spectrum(
3618 pad_to_pow2=True, tfade=None)
3620 nfreqs = fdata.size
3622 key = (trace.deltat, nfreqs)
3624 if key not in self.tf_cache:
3625 resps = []
3626 if self.lowpass is not None:
3627 resps.append(but(
3628 order=4,
3629 corner=self.lowpass,
3630 type='low'))
3632 if self.highpass is not None:
3633 resps.append(but(
3634 order=4,
3635 corner=self.highpass,
3636 type='high'))
3638 resp = multres(resps)
3639 self.tf_cache[key] = \
3640 resp.evaluate(freqs)
3642 filtered_data = num.fft.irfft(
3643 fdata*self.tf_cache[key]
3644 )[:trace.data_len()]
3646 retrended_data = retrend(
3647 it, filtered_data, m, b)
3649 trace.set_ydata(retrended_data)
3651 else:
3653 if ads and self.lowpass is not None:
3654 while trace.deltat \
3655 < min_deltat_wo_decimate:
3657 trace.downsample(2, demean=False)
3659 fmax = 0.5/trace.deltat
3660 if not lphp and (
3661 self.lowpass is not None
3662 and self.highpass is not None
3663 and self.lowpass < fmax
3664 and self.highpass < fmax
3665 and self.highpass < self.lowpass):
3667 trace.bandpass(
3668 2, self.highpass, self.lowpass)
3669 else:
3670 if self.lowpass is not None:
3671 if self.lowpass < 0.5/trace.deltat:
3672 trace.lowpass(
3673 4, self.lowpass,
3674 demean=False)
3676 if self.highpass is not None:
3677 if self.lowpass is None \
3678 or self.highpass \
3679 < self.lowpass:
3681 if self.highpass < \
3682 0.5/trace.deltat:
3683 trace.highpass(
3684 4, self.highpass,
3685 demean=False)
3687 processed_traces.append(trace)
3689 if self.rotate != 0.0:
3690 phi = self.rotate/180.*math.pi
3691 cphi = math.cos(phi)
3692 sphi = math.sin(phi)
3693 for a in processed_traces:
3694 for b in processed_traces:
3695 if (a.network == b.network
3696 and a.station == b.station
3697 and a.location == b.location
3698 and ((a.channel.lower().endswith('n')
3699 and b.channel.lower().endswith('e'))
3700 or (a.channel.endswith('1')
3701 and b.channel.endswith('2')))
3702 and abs(a.deltat-b.deltat) < a.deltat*0.001
3703 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3704 len(a.get_ydata()) == len(b.get_ydata())):
3706 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3707 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3708 a.set_ydata(aydata)
3709 b.set_ydata(bydata)
3711 processed_traces = self.post_process_hooks(processed_traces)
3713 self.cached_processed_traces = processed_traces
3714 self.cached_vec = vec
3716 chopped_traces = []
3717 for trace in processed_traces:
3718 chop_tmin = tmin_ - trace.deltat*4
3719 chop_tmax = tmax_ + trace.deltat*4
3721 try:
3722 ctrace = trace.chop(
3723 chop_tmin, chop_tmax,
3724 inplace=False)
3726 except pyrocko.trace.NoData:
3727 continue
3729 if ctrace.data_len() < 2:
3730 continue
3732 chopped_traces.append(ctrace)
3734 self.timer_cutout.stop()
3735 return chopped_traces
3737 def pre_process_hooks(self, traces):
3738 for snuffling in self.snufflings:
3739 if snuffling._pre_process_hook_enabled:
3740 traces = snuffling.pre_process_hook(traces)
3742 return traces
3744 def post_process_hooks(self, traces):
3745 for snuffling in self.snufflings:
3746 if snuffling._post_process_hook_enabled:
3747 traces = snuffling.post_process_hook(traces)
3749 return traces
3751 def visible_length_change(self, ignore=None):
3752 for menuitem, vlen in self.menuitems_visible_length:
3753 if menuitem.isChecked():
3754 self.visible_length = vlen
3756 def scaling_base_change(self, ignore=None):
3757 for menuitem, scaling_base in self.menuitems_scaling_base:
3758 if menuitem.isChecked():
3759 self.scaling_base = scaling_base
3761 def scalingmode_change(self, ignore=None):
3762 for menuitem, scaling_key in self.menuitems_scaling:
3763 if menuitem.isChecked():
3764 self.scaling_key = scaling_key
3765 self.update()
3767 def apply_scaling_hooks(self, data_ranges):
3768 for k in sorted(self.scaling_hooks.keys()):
3769 hook = self.scaling_hooks[k]
3770 hook(data_ranges)
3772 def viewmode_change(self, ignore=True):
3773 for item, mode in self.menuitems_viewmode:
3774 if item.isChecked():
3775 self.view_mode = mode
3776 break
3777 else:
3778 raise AttributeError('unknown view mode')
3780 items_waterfall_disabled = (
3781 self.menuitem_showscaleaxis,
3782 self.menuitem_showscalerange,
3783 self.menuitem_showzeroline,
3784 self.menuitem_colortraces,
3785 self.menuitem_cliptraces,
3786 *(itm[0] for itm in self.menuitems_visible_length)
3787 )
3789 if self.view_mode is ViewMode.Waterfall:
3790 self.parent().show_colorbar_ctrl(True)
3791 self.parent().show_gain_ctrl(False)
3793 for item in items_waterfall_disabled:
3794 item.setDisabled(True)
3796 self.visible_length = 180.
3797 else:
3798 self.parent().show_colorbar_ctrl(False)
3799 self.parent().show_gain_ctrl(True)
3801 for item in items_waterfall_disabled:
3802 item.setDisabled(False)
3804 self.visible_length_change()
3805 self.update()
3807 def set_scaling_hook(self, k, hook):
3808 self.scaling_hooks[k] = hook
3810 def remove_scaling_hook(self, k):
3811 del self.scaling_hooks[k]
3813 def remove_scaling_hooks(self):
3814 self.scaling_hooks = {}
3816 def s_sortingmode_change(self, ignore=None):
3817 for menuitem, valfunc in self.menuitems_ssorting:
3818 if menuitem.isChecked():
3819 self._ssort = valfunc
3821 self.sortingmode_change()
3823 def sortingmode_change(self, ignore=None):
3824 for menuitem, (gather, color) in self.menuitems_sorting:
3825 if menuitem.isChecked():
3826 self.set_gathering(gather, color)
3828 self.sortingmode_change_time = time.time()
3830 def lowpass_change(self, value, ignore=None):
3831 self.lowpass = value
3832 self.passband_check()
3833 self.tf_cache = {}
3834 self.update()
3836 def highpass_change(self, value, ignore=None):
3837 self.highpass = value
3838 self.passband_check()
3839 self.tf_cache = {}
3840 self.update()
3842 def passband_check(self):
3843 if self.highpass and self.lowpass \
3844 and self.highpass >= self.lowpass:
3846 self.message = 'Corner frequency of highpass larger than ' \
3847 'corner frequency of lowpass! I will now ' \
3848 'deactivate the highpass.'
3850 self.update_status()
3851 else:
3852 oldmess = self.message
3853 self.message = None
3854 if oldmess is not None:
3855 self.update_status()
3857 def gain_change(self, value, ignore):
3858 self.gain = value
3859 self.update()
3861 def rot_change(self, value, ignore):
3862 self.rotate = value
3863 self.update()
3865 def waterfall_cmap_change(self, cmap):
3866 self.waterfall_cmap = cmap
3867 self.update()
3869 def waterfall_clip_change(self, clip_min, clip_max):
3870 self.waterfall_clip_min = clip_min
3871 self.waterfall_clip_max = clip_max
3872 self.update()
3874 def waterfall_show_absolute_change(self, toggle):
3875 self.waterfall_show_absolute = toggle
3876 self.update()
3878 def waterfall_set_integrate(self, toggle):
3879 self.waterfall_integrate = toggle
3880 self.update()
3882 def set_selected_markers(self, markers):
3883 '''
3884 Set a list of markers selected
3886 :param markers: list of markers
3887 '''
3888 self.deselect_all()
3889 for m in markers:
3890 m.selected = True
3892 self.update()
3894 def deselect_all(self):
3895 for marker in self.markers:
3896 marker.selected = False
3898 def animate_picking(self):
3899 point = self.mapFromGlobal(qg.QCursor.pos())
3900 self.update_picking(point.x(), point.y(), doshift=True)
3902 def get_nslc_ids_for_track(self, ftrack):
3903 itrack = int(ftrack)
3904 return self.track_to_nslc_ids.get(itrack, [])
3906 def stop_picking(self, x, y, abort=False):
3907 if self.picking:
3908 self.update_picking(x, y, doshift=False)
3909 self.picking = None
3910 self.picking_down = None
3911 self.picking_timer.stop()
3912 self.picking_timer = None
3913 if not abort:
3914 self.add_marker(self.floating_marker)
3915 self.floating_marker.selected = True
3916 self.emit_selected_markers()
3918 self.floating_marker = None
3920 def start_picking(self, ignore):
3922 if not self.picking:
3923 self.deselect_all()
3924 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3925 point = self.mapFromGlobal(qg.QCursor.pos())
3927 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3928 self.picking.setGeometry(
3929 gpoint.x(), gpoint.y(), 1, self.height())
3930 t = self.time_projection.rev(point.x())
3932 ftrack = self.track_to_screen.rev(point.y())
3933 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3934 self.floating_marker = Marker(nslc_ids, t, t)
3935 self.floating_marker.selected = True
3937 self.picking_timer = qc.QTimer()
3938 self.picking_timer.timeout.connect(
3939 self.animate_picking)
3941 self.picking_timer.setInterval(50)
3942 self.picking_timer.start()
3944 def update_picking(self, x, y, doshift=False):
3945 if self.picking:
3946 mouset = self.time_projection.rev(x)
3947 dt = 0.0
3948 if mouset < self.tmin or mouset > self.tmax:
3949 if mouset < self.tmin:
3950 dt = -(self.tmin - mouset)
3951 else:
3952 dt = mouset - self.tmax
3953 ddt = self.tmax-self.tmin
3954 dt = max(dt, -ddt/10.)
3955 dt = min(dt, ddt/10.)
3957 x0 = x
3958 if self.picking_down is not None:
3959 x0 = self.time_projection(self.picking_down[0])
3961 w = abs(x-x0)
3962 x0 = min(x0, x)
3964 tmin, tmax = (
3965 self.time_projection.rev(x0),
3966 self.time_projection.rev(x0+w))
3968 tmin, tmax = (
3969 max(working_system_time_range[0], tmin),
3970 min(working_system_time_range[1], tmax))
3972 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3974 self.picking.setGeometry(
3975 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3977 ftrack = self.track_to_screen.rev(y)
3978 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3979 self.floating_marker.set(nslc_ids, tmin, tmax)
3981 if dt != 0.0 and doshift:
3982 self.interrupt_following()
3983 self.set_time_range(self.tmin+dt, self.tmax+dt)
3985 self.update()
3987 def update_status(self):
3989 if self.message is None:
3990 point = self.mapFromGlobal(qg.QCursor.pos())
3992 mouse_t = self.time_projection.rev(point.x())
3993 if not is_working_time(mouse_t):
3994 return
3996 if self.floating_marker:
3997 tmi, tma = (
3998 self.floating_marker.tmin,
3999 self.floating_marker.tmax)
4001 tt, ms = gmtime_x(tmi)
4003 if tmi == tma:
4004 message = mystrftime(
4005 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4006 tt=tt, milliseconds=ms)
4007 else:
4008 srange = '%g s' % (tma-tmi)
4009 message = mystrftime(
4010 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4011 tt=tt, milliseconds=ms)
4012 else:
4013 tt, ms = gmtime_x(mouse_t)
4015 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4016 else:
4017 message = self.message
4019 sb = self.window().statusBar()
4020 sb.clearMessage()
4021 sb.showMessage(message)
4023 def set_sortingmode_change_delay_time(self, dt):
4024 self.sortingmode_change_delay_time = dt
4026 def sortingmode_change_delayed(self):
4027 now = time.time()
4028 return (
4029 self.sortingmode_change_delay_time is not None
4030 and now - self.sortingmode_change_time
4031 < self.sortingmode_change_delay_time)
4033 def set_visible_marker_kinds(self, kinds):
4034 self.deselect_all()
4035 self.visible_marker_kinds = tuple(kinds)
4036 self.emit_selected_markers()
4038 def following(self):
4039 return self.follow_timer is not None \
4040 and not self.following_interrupted()
4042 def interrupt_following(self):
4043 self.interactive_range_change_time = time.time()
4045 def following_interrupted(self, now=None):
4046 if now is None:
4047 now = time.time()
4048 return now - self.interactive_range_change_time \
4049 < self.interactive_range_change_delay_time
4051 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4052 if tmax_start is None:
4053 tmax_start = time.time()
4054 self.show_all = False
4055 self.follow_time = tlen
4056 self.follow_timer = qc.QTimer(self)
4057 self.follow_timer.timeout.connect(
4058 self.follow_update)
4059 self.follow_timer.setInterval(interval)
4060 self.follow_timer.start()
4061 self.follow_started = time.time()
4062 self.follow_lapse = lapse
4063 self.follow_tshift = self.follow_started - tmax_start
4064 self.interactive_range_change_time = 0.0
4066 def unfollow(self):
4067 if self.follow_timer is not None:
4068 self.follow_timer.stop()
4069 self.follow_timer = None
4070 self.interactive_range_change_time = 0.0
4072 def follow_update(self):
4073 rnow = time.time()
4074 if self.follow_lapse is None:
4075 now = rnow
4076 else:
4077 now = self.follow_started + (rnow - self.follow_started) \
4078 * self.follow_lapse
4080 if self.following_interrupted(rnow):
4081 return
4082 self.set_time_range(
4083 now-self.follow_time-self.follow_tshift,
4084 now-self.follow_tshift)
4086 self.update()
4088 def myclose(self, return_tag=''):
4089 self.return_tag = return_tag
4090 self.window().close()
4092 def cleanup(self):
4093 self.about_to_close.emit()
4094 self.timer.stop()
4095 if self.follow_timer is not None:
4096 self.follow_timer.stop()
4098 for snuffling in list(self.snufflings):
4099 self.remove_snuffling(snuffling)
4101 def set_error_message(self, key, value):
4102 if value is None:
4103 if key in self.error_messages:
4104 del self.error_messages[key]
4105 else:
4106 self.error_messages[key] = value
4108 def inputline_changed(self, text):
4109 pass
4111 def inputline_finished(self, text):
4112 line = str(text)
4114 toks = line.split()
4115 clearit, hideit, error = False, True, None
4116 if len(toks) >= 1:
4117 command = toks[0].lower()
4119 try:
4120 quick_filter_commands = {
4121 'n': '%s.*.*.*',
4122 's': '*.%s.*.*',
4123 'l': '*.*.%s.*',
4124 'c': '*.*.*.%s'}
4126 if command in quick_filter_commands:
4127 if len(toks) >= 2:
4128 patterns = [
4129 quick_filter_commands[toks[0]] % pat
4130 for pat in toks[1:]]
4131 self.set_quick_filter_patterns(patterns, line)
4132 else:
4133 self.set_quick_filter_patterns(None)
4135 self.update()
4137 elif command in ('hide', 'unhide'):
4138 if len(toks) >= 2:
4139 patterns = []
4140 if len(toks) == 2:
4141 patterns = [toks[1]]
4142 elif len(toks) >= 3:
4143 x = {
4144 'n': '%s.*.*.*',
4145 's': '*.%s.*.*',
4146 'l': '*.*.%s.*',
4147 'c': '*.*.*.%s'}
4149 if toks[1] in x:
4150 patterns.extend(
4151 x[toks[1]] % tok for tok in toks[2:])
4153 for pattern in patterns:
4154 if command == 'hide':
4155 self.add_blacklist_pattern(pattern)
4156 else:
4157 self.remove_blacklist_pattern(pattern)
4159 elif command == 'unhide' and len(toks) == 1:
4160 self.clear_blacklist()
4162 clearit = True
4164 self.update()
4166 elif command == 'markers':
4167 if len(toks) == 2:
4168 if toks[1] == 'all':
4169 kinds = self.all_marker_kinds
4170 else:
4171 kinds = []
4172 for x in toks[1]:
4173 try:
4174 kinds.append(int(x))
4175 except Exception:
4176 pass
4178 self.set_visible_marker_kinds(kinds)
4180 elif len(toks) == 1:
4181 self.set_visible_marker_kinds(())
4183 self.update()
4185 elif command == 'scaling':
4186 if len(toks) == 2:
4187 hideit = False
4188 error = 'wrong number of arguments'
4190 if len(toks) >= 3:
4191 vmin, vmax = [
4192 pyrocko.model.float_or_none(x)
4193 for x in toks[-2:]]
4195 def upd(d, k, vmin, vmax):
4196 if k in d:
4197 if vmin is not None:
4198 d[k] = vmin, d[k][1]
4199 if vmax is not None:
4200 d[k] = d[k][0], vmax
4202 if len(toks) == 1:
4203 self.remove_scaling_hooks()
4205 elif len(toks) == 3:
4206 def hook(data_ranges):
4207 for k in data_ranges:
4208 upd(data_ranges, k, vmin, vmax)
4210 self.set_scaling_hook('_', hook)
4212 elif len(toks) == 4:
4213 pattern = toks[1]
4215 def hook(data_ranges):
4216 for k in pyrocko.util.match_nslcs(
4217 pattern, list(data_ranges.keys())):
4219 upd(data_ranges, k, vmin, vmax)
4221 self.set_scaling_hook(pattern, hook)
4223 elif command == 'goto':
4224 toks2 = line.split(None, 1)
4225 if len(toks2) == 2:
4226 arg = toks2[1]
4227 m = re.match(
4228 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4229 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4230 if m:
4231 tlen = None
4232 if not m.group(1):
4233 tlen = 12*32*24*60*60
4234 elif not m.group(2):
4235 tlen = 32*24*60*60
4236 elif not m.group(3):
4237 tlen = 24*60*60
4238 elif not m.group(4):
4239 tlen = 60*60
4240 elif not m.group(5):
4241 tlen = 60
4243 supl = '1970-01-01 00:00:00'
4244 if len(supl) > len(arg):
4245 arg = arg + supl[-(len(supl)-len(arg)):]
4246 t = pyrocko.util.str_to_time(arg)
4247 self.go_to_time(t, tlen=tlen)
4249 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4250 supl = '00:00:00'
4251 if len(supl) > len(arg):
4252 arg = arg + supl[-(len(supl)-len(arg)):]
4253 tmin, tmax = self.get_time_range()
4254 sdate = pyrocko.util.time_to_str(
4255 tmin/2.+tmax/2., format='%Y-%m-%d')
4256 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4257 self.go_to_time(t)
4259 elif arg == 'today':
4260 self.go_to_time(
4261 day_start(
4262 time.time()), tlen=24*60*60)
4264 elif arg == 'yesterday':
4265 self.go_to_time(
4266 day_start(
4267 time.time()-24*60*60), tlen=24*60*60)
4269 else:
4270 self.go_to_event_by_name(arg)
4272 else:
4273 raise PileViewerMainException(
4274 'No such command: %s' % command)
4276 except PileViewerMainException as e:
4277 error = str(e)
4278 hideit = False
4280 return clearit, hideit, error
4282 return PileViewerMain
4285PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4286GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4289class LineEditWithAbort(qw.QLineEdit):
4291 aborted = qc.pyqtSignal()
4292 history_down = qc.pyqtSignal()
4293 history_up = qc.pyqtSignal()
4295 def keyPressEvent(self, key_event):
4296 if key_event.key() == qc.Qt.Key_Escape:
4297 self.aborted.emit()
4298 elif key_event.key() == qc.Qt.Key_Down:
4299 self.history_down.emit()
4300 elif key_event.key() == qc.Qt.Key_Up:
4301 self.history_up.emit()
4302 else:
4303 return qw.QLineEdit.keyPressEvent(self, key_event)
4306class PileViewer(qw.QFrame):
4307 '''
4308 PileViewerMain + Controls + Inputline
4309 '''
4311 def __init__(
4312 self, pile,
4313 ntracks_shown_max=20,
4314 marker_editor_sortable=True,
4315 use_opengl=None,
4316 panel_parent=None,
4317 *args):
4319 qw.QFrame.__init__(self, *args)
4321 layout = qw.QGridLayout()
4322 layout.setContentsMargins(0, 0, 0, 0)
4323 layout.setSpacing(0)
4325 self.menu = PileViewerMenuBar(self)
4327 if use_opengl is None:
4328 use_opengl = is_macos
4330 if use_opengl:
4331 self.viewer = GLPileViewerMain(
4332 pile,
4333 ntracks_shown_max=ntracks_shown_max,
4334 panel_parent=panel_parent,
4335 menu=self.menu)
4336 else:
4337 self.viewer = PileViewerMain(
4338 pile,
4339 ntracks_shown_max=ntracks_shown_max,
4340 panel_parent=panel_parent,
4341 menu=self.menu)
4343 self.marker_editor_sortable = marker_editor_sortable
4345 # self.setFrameShape(qw.QFrame.StyledPanel)
4346 # self.setFrameShadow(qw.QFrame.Sunken)
4348 self.input_area = qw.QFrame(self)
4349 ia_layout = qw.QGridLayout()
4350 ia_layout.setContentsMargins(11, 11, 11, 11)
4351 self.input_area.setLayout(ia_layout)
4353 self.inputline = LineEditWithAbort(self.input_area)
4354 self.inputline.returnPressed.connect(
4355 self.inputline_returnpressed)
4356 self.inputline.editingFinished.connect(
4357 self.inputline_finished)
4358 self.inputline.aborted.connect(
4359 self.inputline_aborted)
4361 self.inputline.history_down.connect(
4362 lambda: self.step_through_history(1))
4363 self.inputline.history_up.connect(
4364 lambda: self.step_through_history(-1))
4366 self.inputline.textEdited.connect(
4367 self.inputline_changed)
4369 self.inputline.setPlaceholderText(
4370 u'Quick commands: e.g. \'c HH?\' to select channels. '
4371 u'Use ↑ or ↓ to navigate.')
4372 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4373 self.input_area.hide()
4374 self.history = None
4376 self.inputline_error_str = None
4378 self.inputline_error = qw.QLabel()
4379 self.inputline_error.hide()
4381 ia_layout.addWidget(self.inputline, 0, 0)
4382 ia_layout.addWidget(self.inputline_error, 1, 0)
4383 layout.addWidget(self.input_area, 0, 0, 1, 2)
4384 layout.addWidget(self.viewer, 1, 0)
4386 pb = Progressbars(self)
4387 layout.addWidget(pb, 2, 0, 1, 2)
4388 self.progressbars = pb
4390 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4391 self.scrollbar = scrollbar
4392 layout.addWidget(scrollbar, 1, 1)
4393 self.scrollbar.valueChanged.connect(
4394 self.scrollbar_changed)
4396 self.block_scrollbar_changes = False
4398 self.viewer.want_input.connect(
4399 self.inputline_show)
4400 self.viewer.tracks_range_changed.connect(
4401 self.tracks_range_changed)
4402 self.viewer.pile_has_changed_signal.connect(
4403 self.adjust_controls)
4404 self.viewer.about_to_close.connect(
4405 self.save_inputline_history)
4407 self.setLayout(layout)
4409 def cleanup(self):
4410 self.viewer.cleanup()
4412 def get_progressbars(self):
4413 return self.progressbars
4415 def inputline_show(self):
4416 if not self.history:
4417 self.load_inputline_history()
4419 self.input_area.show()
4420 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4421 self.inputline.selectAll()
4423 def inputline_set_error(self, string):
4424 self.inputline_error_str = string
4425 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4426 self.inputline.selectAll()
4427 self.inputline_error.setText(string)
4428 self.input_area.show()
4429 self.inputline_error.show()
4431 def inputline_clear_error(self):
4432 if self.inputline_error_str:
4433 self.inputline.setPalette(qw.QApplication.palette())
4434 self.inputline_error_str = None
4435 self.inputline_error.clear()
4436 self.inputline_error.hide()
4438 def inputline_changed(self, line):
4439 self.viewer.inputline_changed(str(line))
4440 self.inputline_clear_error()
4442 def inputline_returnpressed(self):
4443 line = str(self.inputline.text())
4444 clearit, hideit, error = self.viewer.inputline_finished(line)
4446 if error:
4447 self.inputline_set_error(error)
4449 line = line.strip()
4451 if line != '' and not error:
4452 if not (len(self.history) >= 1 and line == self.history[-1]):
4453 self.history.append(line)
4455 if clearit:
4457 self.inputline.blockSignals(True)
4458 qpat, qinp = self.viewer.get_quick_filter_patterns()
4459 if qpat is None:
4460 self.inputline.clear()
4461 else:
4462 self.inputline.setText(qinp)
4463 self.inputline.blockSignals(False)
4465 if hideit and not error:
4466 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4467 self.input_area.hide()
4469 self.hist_ind = len(self.history)
4471 def inputline_aborted(self):
4472 '''
4473 Hide the input line.
4474 '''
4475 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4476 self.hist_ind = len(self.history)
4477 self.input_area.hide()
4479 def save_inputline_history(self):
4480 '''
4481 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4482 '''
4483 if not self.history:
4484 return
4486 conf = pyrocko.config
4487 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4488 with open(fn_hist, 'w') as f:
4489 i = min(100, len(self.history))
4490 for c in self.history[-i:]:
4491 f.write('%s\n' % c)
4493 def load_inputline_history(self):
4494 '''
4495 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4496 '''
4497 conf = pyrocko.config
4498 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4499 if not os.path.exists(fn_hist):
4500 with open(fn_hist, 'w+') as f:
4501 f.write('\n')
4503 with open(fn_hist, 'r') as f:
4504 self.history = [line.strip() for line in f.readlines()]
4506 self.hist_ind = len(self.history)
4508 def step_through_history(self, ud=1):
4509 '''
4510 Step through input line history and set the input line text.
4511 '''
4512 n = len(self.history)
4513 self.hist_ind += ud
4514 self.hist_ind %= (n + 1)
4515 if len(self.history) != 0 and self.hist_ind != n:
4516 self.inputline.setText(self.history[self.hist_ind])
4517 else:
4518 self.inputline.setText('')
4520 def inputline_finished(self):
4521 pass
4523 def tracks_range_changed(self, ntracks, ilo, ihi):
4524 if self.block_scrollbar_changes:
4525 return
4527 self.scrollbar.blockSignals(True)
4528 self.scrollbar.setPageStep(ihi-ilo)
4529 vmax = max(0, ntracks-(ihi-ilo))
4530 self.scrollbar.setRange(0, vmax)
4531 self.scrollbar.setValue(ilo)
4532 self.scrollbar.setHidden(vmax == 0)
4533 self.scrollbar.blockSignals(False)
4535 def scrollbar_changed(self, value):
4536 self.block_scrollbar_changes = True
4537 ilo = value
4538 ihi = ilo + self.scrollbar.pageStep()
4539 self.viewer.set_tracks_range((ilo, ihi))
4540 self.block_scrollbar_changes = False
4541 self.update_contents()
4543 def controls(self):
4544 frame = qw.QFrame(self)
4545 layout = qw.QGridLayout()
4546 frame.setLayout(layout)
4548 minfreq = 0.001
4549 maxfreq = 1000.0
4550 self.lowpass_control = ValControl(high_is_none=True)
4551 self.lowpass_control.setup(
4552 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4553 self.highpass_control = ValControl(low_is_none=True)
4554 self.highpass_control.setup(
4555 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4556 self.gain_control = ValControl()
4557 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4558 self.rot_control = LinValControl()
4559 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4560 self.colorbar_control = ColorbarControl(self)
4562 self.lowpass_control.valchange.connect(
4563 self.viewer.lowpass_change)
4564 self.highpass_control.valchange.connect(
4565 self.viewer.highpass_change)
4566 self.gain_control.valchange.connect(
4567 self.viewer.gain_change)
4568 self.rot_control.valchange.connect(
4569 self.viewer.rot_change)
4570 self.colorbar_control.cmap_changed.connect(
4571 self.viewer.waterfall_cmap_change
4572 )
4573 self.colorbar_control.clip_changed.connect(
4574 self.viewer.waterfall_clip_change
4575 )
4576 self.colorbar_control.show_absolute_toggled.connect(
4577 self.viewer.waterfall_show_absolute_change
4578 )
4579 self.colorbar_control.show_integrate_toggled.connect(
4580 self.viewer.waterfall_set_integrate
4581 )
4583 for icontrol, control in enumerate((
4584 self.highpass_control,
4585 self.lowpass_control,
4586 self.gain_control,
4587 self.rot_control,
4588 self.colorbar_control)):
4590 for iwidget, widget in enumerate(control.widgets()):
4591 layout.addWidget(widget, icontrol, iwidget)
4593 spacer = qw.QSpacerItem(
4594 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4595 layout.addItem(spacer, 4, 0, 1, 3)
4597 self.adjust_controls()
4598 self.viewer.viewmode_change(ViewMode.Wiggle)
4599 return frame
4601 def marker_editor(self):
4602 editor = pyrocko.gui.snuffler.marker_editor.MarkerEditor(
4603 self, sortable=self.marker_editor_sortable)
4605 editor.set_viewer(self.get_view())
4606 editor.get_marker_model().dataChanged.connect(
4607 self.update_contents)
4608 return editor
4610 def adjust_controls(self):
4611 dtmin, dtmax = self.viewer.content_deltat_range()
4612 maxfreq = 0.5/dtmin
4613 minfreq = (0.5/dtmax)*0.0001
4614 self.lowpass_control.set_range(minfreq, maxfreq)
4615 self.highpass_control.set_range(minfreq, maxfreq)
4617 def setup_snufflings(self):
4618 self.viewer.setup_snufflings()
4620 def get_view(self):
4621 return self.viewer
4623 def update_contents(self):
4624 self.viewer.update()
4626 def get_pile(self):
4627 return self.viewer.get_pile()
4629 def show_colorbar_ctrl(self, show):
4630 for w in self.colorbar_control.widgets():
4631 w.setVisible(show)
4633 def show_gain_ctrl(self, show):
4634 for w in self.gain_control.widgets():
4635 w.setVisible(show)