1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import, print_function
7import os
8import time
9import calendar
10import datetime
11import re
12import math
13import logging
14import operator
15import copy
16import enum
17from itertools import groupby
19import numpy as num
20import pyrocko.model
21import pyrocko.pile
22import pyrocko.trace
23import pyrocko.response
24import pyrocko.util
25import pyrocko.plot
26import pyrocko.plot.colors
27import pyrocko.gui.snuffling
28import pyrocko.gui.snufflings
29import pyrocko.gui.marker_editor
31from pyrocko.util import hpfloat, gmtime_x, mystrftime
33from .marker import associate_phases_to_events, MarkerOneNSLCRequired
35from .util import (ValControl, LinValControl, Marker, EventMarker,
36 PhaseMarker, make_QPolygonF, draw_label, Label,
37 Progressbars, ColorbarControl)
39from .qt_compat import qc, qg, qw, qsvg
41from .pile_viewer_waterfall import TraceWaterfall
43import scipy.stats as sstats
44import platform
46MIN_LABEL_SIZE_PT = 6
49qc.QString = str
51qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
52 qw.QFileDialog.DontUseSheet
54is_macos = platform.uname()[0] == 'Darwin'
56logger = logging.getLogger('pyrocko.gui.pile_viewer')
59def detrend(x, y):
60 slope, offset, _, _, _ = sstats.linregress(x, y)
61 y_detrended = y - slope * x - offset
62 return y_detrended, slope, offset
65def retrend(x, y_detrended, slope, offset):
66 return x * slope + y_detrended + offset
69class Global(object):
70 appOnDemand = None
73class NSLC(object):
74 def __init__(self, n, s, l=None, c=None): # noqa
75 self.network = n
76 self.station = s
77 self.location = l
78 self.channel = c
81class m_float(float):
83 def __str__(self):
84 if abs(self) >= 10000.:
85 return '%g km' % round(self/1000., 0)
86 elif abs(self) >= 1000.:
87 return '%g km' % round(self/1000., 1)
88 else:
89 return '%.5g m' % self
91 def __lt__(self, other):
92 if other is None:
93 return True
94 return float(self) < float(other)
96 def __gt__(self, other):
97 if other is None:
98 return False
99 return float(self) > float(other)
102def m_float_or_none(x):
103 if x is None:
104 return None
105 else:
106 return m_float(x)
109def make_chunks(items):
110 '''
111 Split a list of integers into sublists of consecutive elements.
112 '''
113 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
114 enumerate(items), (lambda x: x[1]-x[0]))]
117class deg_float(float):
119 def __str__(self):
120 return '%4.0f' % self
122 def __lt__(self, other):
123 if other is None:
124 return True
125 return float(self) < float(other)
127 def __gt__(self, other):
128 if other is None:
129 return False
130 return float(self) > float(other)
133def deg_float_or_none(x):
134 if x is None:
135 return None
136 else:
137 return deg_float(x)
140class sector_int(int):
142 def __str__(self):
143 return '[%i]' % self
145 def __lt__(self, other):
146 if other is None:
147 return True
148 return int(self) < int(other)
150 def __gt__(self, other):
151 if other is None:
152 return False
153 return int(self) > int(other)
156def num_to_html(num):
157 snum = '%g' % num
158 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
159 if m:
160 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
162 return snum
165gap_lap_tolerance = 5.
168class ViewMode(enum.Enum):
169 Wiggle = 1
170 Waterfall = 2
173class Timer(object):
174 def __init__(self):
175 self._start = None
176 self._stop = None
178 def start(self):
179 self._start = os.times()
181 def stop(self):
182 self._stop = os.times()
184 def get(self):
185 a = self._start
186 b = self._stop
187 if a is not None and b is not None:
188 return tuple([b[i] - a[i] for i in range(5)])
189 else:
190 return tuple([0.] * 5)
192 def __sub__(self, other):
193 a = self.get()
194 b = other.get()
195 return tuple([a[i] - b[i] for i in range(5)])
198class ObjectStyle(object):
199 def __init__(self, frame_pen, fill_brush):
200 self.frame_pen = frame_pen
201 self.fill_brush = fill_brush
204box_styles = []
205box_alpha = 100
206for color in 'orange skyblue butter chameleon chocolate plum ' \
207 'scarletred'.split():
209 box_styles.append(ObjectStyle(
210 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
211 qg.QBrush(qg.QColor(
212 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
213 ))
215box_styles_coverage = {}
217box_styles_coverage['waveform'] = [
218 ObjectStyle(
219 qg.QPen(
220 qg.QColor(*pyrocko.plot.colors.g_nat_colors['nat_green1']),
221 1, qc.Qt.DashLine),
222 qg.QBrush(qg.QColor(
223 *(pyrocko.plot.colors.g_nat_colors['nat_green1'] + (50,)))),
224 ),
225 ObjectStyle(
226 qg.QPen(qg.QColor(*pyrocko.plot.colors.g_nat_colors['nat_brown'])),
227 qg.QBrush(qg.QColor(
228 *(pyrocko.plot.colors.g_nat_colors['nat_brown'] + (50,)))),
229 ),
230 ObjectStyle(
231 qg.QPen(qg.QColor(*pyrocko.plot.colors.g_nat_colors['nat_yellow'])),
232 qg.QBrush(qg.QColor(
233 *(pyrocko.plot.colors.g_nat_colors['nat_yellow'] + (50,)))),
234 )]
236box_styles_coverage['waveform_promise'] = [
237 ObjectStyle(
238 qg.QPen(
239 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
240 1, qc.Qt.DashLine),
241 qg.QBrush(qg.QColor(
242 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
243 ),
244 ObjectStyle(
245 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
246 qg.QBrush(qg.QColor(
247 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
248 ),
249 ObjectStyle(
250 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
251 qg.QBrush(qg.QColor(
252 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
253 )]
255sday = 60*60*24. # \
256smonth = 60*60*24*30. # | only used as approx. intervals...
257syear = 60*60*24*365. # /
259acceptable_tincs = num.array([
260 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
261 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
264working_system_time_range = \
265 pyrocko.util.working_system_time_range()
267initial_time_range = []
269try:
270 initial_time_range.append(
271 calendar.timegm((1950, 1, 1, 0, 0, 0)))
272except Exception:
273 initial_time_range.append(working_system_time_range[0])
275try:
276 initial_time_range.append(
277 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
278except Exception:
279 initial_time_range.append(working_system_time_range[1])
282def is_working_time(t):
283 return working_system_time_range[0] <= t and \
284 t <= working_system_time_range[1]
287def fancy_time_ax_format(inc):
288 l0_fmt_brief = ''
289 l2_fmt = ''
290 l2_trig = 0
291 if inc < 0.000001:
292 l0_fmt = '.%n'
293 l0_center = False
294 l1_fmt = '%H:%M:%S'
295 l1_trig = 6
296 l2_fmt = '%b %d, %Y'
297 l2_trig = 3
298 elif inc < 0.001:
299 l0_fmt = '.%u'
300 l0_center = False
301 l1_fmt = '%H:%M:%S'
302 l1_trig = 6
303 l2_fmt = '%b %d, %Y'
304 l2_trig = 3
305 elif inc < 1:
306 l0_fmt = '.%r'
307 l0_center = False
308 l1_fmt = '%H:%M:%S'
309 l1_trig = 6
310 l2_fmt = '%b %d, %Y'
311 l2_trig = 3
312 elif inc < 60:
313 l0_fmt = '%H:%M:%S'
314 l0_center = False
315 l1_fmt = '%b %d, %Y'
316 l1_trig = 3
317 elif inc < 3600:
318 l0_fmt = '%H:%M'
319 l0_center = False
320 l1_fmt = '%b %d, %Y'
321 l1_trig = 3
322 elif inc < sday:
323 l0_fmt = '%H:%M'
324 l0_center = False
325 l1_fmt = '%b %d, %Y'
326 l1_trig = 3
327 elif inc < smonth:
328 l0_fmt = '%a %d'
329 l0_fmt_brief = '%d'
330 l0_center = True
331 l1_fmt = '%b, %Y'
332 l1_trig = 2
333 elif inc < syear:
334 l0_fmt = '%b'
335 l0_center = True
336 l1_fmt = '%Y'
337 l1_trig = 1
338 else:
339 l0_fmt = '%Y'
340 l0_center = False
341 l1_fmt = ''
342 l1_trig = 0
344 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
347def day_start(timestamp):
348 tt = time.gmtime(int(timestamp))
349 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
350 return calendar.timegm(tts)
353def month_start(timestamp):
354 tt = time.gmtime(int(timestamp))
355 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
356 return calendar.timegm(tts)
359def year_start(timestamp):
360 tt = time.gmtime(int(timestamp))
361 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
362 return calendar.timegm(tts)
365def time_nice_value(inc0):
366 if inc0 < acceptable_tincs[0]:
367 return pyrocko.plot.nice_value(inc0)
368 elif inc0 > acceptable_tincs[-1]:
369 return pyrocko.plot.nice_value(inc0/syear)*syear
370 else:
371 i = num.argmin(num.abs(acceptable_tincs-inc0))
372 return acceptable_tincs[i]
375class TimeScaler(pyrocko.plot.AutoScaler):
376 def __init__(self):
377 pyrocko.plot.AutoScaler.__init__(self)
378 self.mode = 'min-max'
380 def make_scale(self, data_range):
381 assert self.mode in ('min-max', 'off'), \
382 'mode must be "min-max" or "off" for TimeScaler'
384 data_min = min(data_range)
385 data_max = max(data_range)
386 is_reverse = (data_range[0] > data_range[1])
388 mi, ma = data_min, data_max
389 nmi = mi
390 if self.mode != 'off':
391 nmi = mi - self.space*(ma-mi)
393 nma = ma
394 if self.mode != 'off':
395 nma = ma + self.space*(ma-mi)
397 mi, ma = nmi, nma
399 if mi == ma and self.mode != 'off':
400 mi -= 1.0
401 ma += 1.0
403 mi = max(working_system_time_range[0], mi)
404 ma = min(working_system_time_range[1], ma)
406 # make nice tick increment
407 if self.inc is not None:
408 inc = self.inc
409 else:
410 if self.approx_ticks > 0.:
411 inc = time_nice_value((ma-mi)/self.approx_ticks)
412 else:
413 inc = time_nice_value((ma-mi)*10.)
415 if inc == 0.0:
416 inc = 1.0
418 if is_reverse:
419 return ma, mi, -inc
420 else:
421 return mi, ma, inc
423 def make_ticks(self, data_range):
424 mi, ma, inc = self.make_scale(data_range)
426 is_reverse = False
427 if inc < 0:
428 mi, ma, inc = ma, mi, -inc
429 is_reverse = True
431 ticks = []
433 if inc < sday:
434 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
435 if inc < 0.001:
436 mi_day = hpfloat(mi_day)
438 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
439 if inc < 0.001:
440 base = hpfloat(base)
442 base_day = mi_day
443 i = 0
444 while True:
445 tick = base+i*inc
446 if tick > ma:
447 break
449 tick_day = day_start(tick)
450 if tick_day > base_day:
451 base_day = tick_day
452 base = base_day
453 i = 0
454 else:
455 ticks.append(tick)
456 i += 1
458 elif inc < smonth:
459 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
460 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
461 delta = datetime.timedelta(days=int(round(inc/sday)))
462 if mi_day == mi:
463 dt_base += delta
464 i = 0
465 while True:
466 current = dt_base + i*delta
467 tick = calendar.timegm(current.timetuple())
468 if tick > ma:
469 break
470 ticks.append(tick)
471 i += 1
473 elif inc < syear:
474 mi_month = month_start(max(
475 mi, working_system_time_range[0]+smonth*1.5))
477 y, m = time.gmtime(mi_month)[:2]
478 while True:
479 tick = calendar.timegm((y, m, 1, 0, 0, 0))
480 m += 1
481 if m > 12:
482 y, m = y+1, 1
484 if tick > ma:
485 break
487 if tick >= mi:
488 ticks.append(tick)
490 else:
491 mi_year = year_start(max(
492 mi, working_system_time_range[0]+syear*1.5))
494 incy = int(round(inc/syear))
495 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
497 while True:
498 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
499 y += incy
500 if tick > ma:
501 break
502 if tick >= mi:
503 ticks.append(tick)
505 if is_reverse:
506 ticks.reverse()
508 return ticks, inc
511def need_l1_tick(tt, ms, l1_trig):
512 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
515def tick_to_labels(tick, inc):
516 tt, ms = gmtime_x(tick)
517 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
518 fancy_time_ax_format(inc)
520 l0 = mystrftime(l0_fmt, tt, ms)
521 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
522 l1, l2 = None, None
523 if need_l1_tick(tt, ms, l1_trig):
524 l1 = mystrftime(l1_fmt, tt, ms)
525 if need_l1_tick(tt, ms, l2_trig):
526 l2 = mystrftime(l2_fmt, tt, ms)
528 return l0, l0_brief, l0_center, l1, l2
531def l1_l2_tick(tick, inc):
532 tt, ms = gmtime_x(tick)
533 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
534 fancy_time_ax_format(inc)
536 l1 = mystrftime(l1_fmt, tt, ms)
537 l2 = mystrftime(l2_fmt, tt, ms)
538 return l1, l2
541class TimeAx(TimeScaler):
542 def __init__(self, *args):
543 TimeScaler.__init__(self, *args)
545 def drawit(self, p, xprojection, yprojection):
546 pen = qg.QPen(
547 qg.QColor(*pyrocko.plot.colors.g_nat_colors['nat_gray']), 1)
548 p.setPen(pen)
549 font = qg.QFont()
550 font.setBold(True)
551 p.setFont(font)
552 fm = p.fontMetrics()
553 ticklen = 10
554 pad = 10
555 tmin, tmax = xprojection.get_in_range()
556 ticks, inc = self.make_ticks((tmin, tmax))
557 l1_hits = 0
558 l2_hits = 0
560 vmin, vmax = yprojection(0), yprojection(ticklen)
561 uumin, uumax = xprojection.get_out_range()
562 first_tick_with_label = None
563 for tick in ticks:
564 umin = xprojection(tick)
566 umin_approx_next = xprojection(tick+inc)
567 umax = xprojection(tick)
569 pinc_approx = umin_approx_next - umin
571 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
572 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
574 if tick == 0.0 and tmax - tmin < 3600*24:
575 # hide year at epoch (we assume that synthetic data is shown)
576 if l2:
577 l2 = None
578 elif l1:
579 l1 = None
581 if l0_center:
582 ushift = (umin_approx_next-umin)/2.
583 else:
584 ushift = 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 if uumin+pad < umin-rect0.width()/2.+ushift and \
593 umin+rect0.width()/2.+ushift < uumax-pad:
595 if first_tick_with_label is None:
596 first_tick_with_label = tick
597 p.drawText(qc.QPointF(
598 umin-rect0.width()/2.+ushift,
599 vmin+rect0.height()+ticklen), label0)
601 if l1:
602 label1 = l1
603 rect1 = fm.boundingRect(label1)
604 if uumin+pad < umin-rect1.width()/2. and \
605 umin+rect1.width()/2. < uumax-pad:
607 p.drawText(qc.QPointF(
608 umin-rect1.width()/2.,
609 vmin+rect0.height()+rect1.height()+ticklen),
610 label1)
612 l1_hits += 1
614 if l2:
615 label2 = l2
616 rect2 = fm.boundingRect(label2)
617 if uumin+pad < umin-rect2.width()/2. and \
618 umin+rect2.width()/2. < uumax-pad:
620 p.drawText(qc.QPointF(
621 umin-rect2.width()/2.,
622 vmin+rect0.height()+rect1.height()+rect2.height() +
623 ticklen), label2)
625 l2_hits += 1
627 if first_tick_with_label is None:
628 first_tick_with_label = tmin
630 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
632 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
633 tmax - tmin < 3600*24:
635 # hide year at epoch (we assume that synthetic data is shown)
636 if l2:
637 l2 = None
638 elif l1:
639 l1 = None
641 if l1_hits == 0 and l1:
642 label1 = l1
643 rect1 = fm.boundingRect(label1)
644 p.drawText(qc.QPointF(
645 uumin+pad,
646 vmin+rect0.height()+rect1.height()+ticklen),
647 label1)
649 l1_hits += 1
651 if l2_hits == 0 and l2:
652 label2 = l2
653 rect2 = fm.boundingRect(label2)
654 p.drawText(qc.QPointF(
655 uumin+pad,
656 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
657 label2)
659 v = yprojection(0)
660 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
663class Projection(object):
664 def __init__(self):
665 self.xr = 0., 1.
666 self.ur = 0., 1.
668 def set_in_range(self, xmin, xmax):
669 if xmax == xmin:
670 xmax = xmin + 1.
672 self.xr = xmin, xmax
674 def get_in_range(self):
675 return self.xr
677 def set_out_range(self, umin, umax):
678 if umax == umin:
679 umax = umin + 1.
681 self.ur = umin, umax
683 def get_out_range(self):
684 return self.ur
686 def __call__(self, x):
687 umin, umax = self.ur
688 xmin, xmax = self.xr
689 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
691 def clipped(self, x, umax_pad):
692 umin, umax = self.ur
693 xmin, xmax = self.xr
694 return min(
695 umax-umax_pad,
696 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
698 def rev(self, u):
699 umin, umax = self.ur
700 xmin, xmax = self.xr
701 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
703 def copy(self):
704 return copy.copy(self)
707def add_radiobuttongroup(menu, menudef, target, default=None):
708 group = qw.QActionGroup(menu)
709 group.setExclusive(True)
710 menuitems = []
712 for name, value, *shortcut in menudef:
713 action = menu.addAction(name)
714 action.setCheckable(True)
715 action.setActionGroup(group)
716 if shortcut:
717 action.setShortcut(shortcut[0])
719 menuitems.append((action, value))
720 if default is not None and (
721 name.lower().replace(' ', '_') == default or
722 value == default):
723 action.setChecked(True)
725 group.triggered.connect(target)
727 if default is None:
728 menuitems[0][0].setChecked(True)
730 return menuitems
733def sort_actions(menu):
734 actions = [act for act in menu.actions() if not act.menu()]
735 for action in actions:
736 menu.removeAction(action)
737 actions.sort(key=lambda x: str(x.text()))
739 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
740 if help_action:
741 actions.insert(0, actions.pop(actions.index(help_action[0])))
742 for action in actions:
743 menu.addAction(action)
746fkey_map = dict(zip(
747 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
748 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
749 range(10)))
752class PileViewerMainException(Exception):
753 pass
756class PileViewerMenuBar(qw.QMenuBar):
757 ...
760class PileViewerMenu(qw.QMenu):
761 ...
764def MakePileViewerMainClass(base):
766 class PileViewerMain(base):
768 want_input = qc.pyqtSignal()
769 about_to_close = qc.pyqtSignal()
770 pile_has_changed_signal = qc.pyqtSignal()
771 tracks_range_changed = qc.pyqtSignal(int, int, int)
773 begin_markers_add = qc.pyqtSignal(int, int)
774 end_markers_add = qc.pyqtSignal()
775 begin_markers_remove = qc.pyqtSignal(int, int)
776 end_markers_remove = qc.pyqtSignal()
778 marker_selection_changed = qc.pyqtSignal(list)
779 active_event_marker_changed = qc.pyqtSignal()
781 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
782 menu=None):
783 base.__init__(self, *args)
785 self.pile = pile
786 self.ax_height = 80
787 self.panel_parent = panel_parent
789 self.click_tolerance = 5
791 self.ntracks_shown_max = ntracks_shown_max
792 self.initial_ntracks_shown_max = ntracks_shown_max
793 self.ntracks = 0
794 self.show_all = True
795 self.shown_tracks_range = None
796 self.track_start = None
797 self.track_trange = None
799 self.lowpass = None
800 self.highpass = None
801 self.gain = 1.0
802 self.rotate = 0.0
803 self.picking_down = None
804 self.picking = None
805 self.floating_marker = None
806 self.markers = pyrocko.pile.Sorted([], 'tmin')
807 self.markers_deltat_max = 0.
808 self.n_selected_markers = 0
809 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
810 self.visible_marker_kinds = self.all_marker_kinds
811 self.active_event_marker = None
812 self.ignore_releases = 0
813 self.message = None
814 self.reloaded = False
815 self.pile_has_changed = False
816 self.config = pyrocko.config.config('snuffler')
818 self.tax = TimeAx()
819 self.setBackgroundRole(qg.QPalette.Base)
820 self.setAutoFillBackground(True)
821 poli = qw.QSizePolicy(
822 qw.QSizePolicy.Expanding,
823 qw.QSizePolicy.Expanding)
825 self.setSizePolicy(poli)
826 self.setMinimumSize(300, 200)
827 self.setFocusPolicy(qc.Qt.ClickFocus)
829 self.menu = menu or PileViewerMenu(self)
831 file_menu = self.menu.addMenu('&File')
832 view_menu = self.menu.addMenu('&View')
833 options_menu = self.menu.addMenu('&Options')
834 scale_menu = self.menu.addMenu('&Scaling')
835 sort_menu = self.menu.addMenu('Sor&ting')
836 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
838 help_menu = self.menu.addMenu('&Help')
840 self.snufflings_menu = self.toggle_panel_menu.addMenu(
841 'Run Snuffling')
842 self.toggle_panel_menu.addSeparator()
843 self.snuffling_help = help_menu.addMenu('Snuffling Help')
844 help_menu.addSeparator()
846 file_menu.addAction(
847 qg.QIcon.fromTheme('document-open'),
848 'Open waveform files...',
849 self.open_waveforms,
850 qg.QKeySequence.Open)
852 file_menu.addAction(
853 qg.QIcon.fromTheme('document-open'),
854 'Open waveform directory...',
855 self.open_waveform_directory)
857 file_menu.addAction(
858 'Open station files...',
859 self.open_stations)
861 file_menu.addAction(
862 'Open StationXML files...',
863 self.open_stations_xml)
865 file_menu.addAction(
866 'Open event file...',
867 self.read_events)
869 file_menu.addSeparator()
870 file_menu.addAction(
871 'Open marker file...',
872 self.read_markers)
874 file_menu.addAction(
875 qg.QIcon.fromTheme('document-save'),
876 'Save markers...',
877 self.write_markers,
878 qg.QKeySequence.Save)
880 file_menu.addAction(
881 qg.QIcon.fromTheme('document-save-as'),
882 'Save selected markers...',
883 self.write_selected_markers,
884 qg.QKeySequence.SaveAs)
886 file_menu.addSeparator()
887 file_menu.addAction(
888 qg.QIcon.fromTheme('document-print'),
889 'Print',
890 self.printit,
891 qg.QKeySequence.Print)
893 file_menu.addAction(
894 qg.QIcon.fromTheme('insert-image'),
895 'Save as SVG or PNG',
896 self.savesvg,
897 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
899 file_menu.addSeparator()
900 close = file_menu.addAction(
901 qg.QIcon.fromTheme('window-close'),
902 'Close',
903 self.myclose)
904 close.setShortcuts(
905 (qg.QKeySequence(qc.Qt.Key_Q),
906 qg.QKeySequence(qc.Qt.Key_X)))
908 # Scale Menu
909 menudef = [
910 ('Individual Scale',
911 lambda tr: tr.nslc_id,
912 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
913 ('Common Scale',
914 lambda tr: None,
915 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
916 ('Common Scale per Station',
917 lambda tr: (tr.network, tr.station),
918 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
919 ('Common Scale per Station Location',
920 lambda tr: (tr.network, tr.station, tr.location)),
921 ('Common Scale per Component',
922 lambda tr: (tr.channel)),
923 ]
925 self.menuitems_scaling = add_radiobuttongroup(
926 scale_menu, menudef, self.scalingmode_change,
927 default=self.config.trace_scale)
928 scale_menu.addSeparator()
930 self.scaling_key = self.menuitems_scaling[0][1]
931 self.scaling_hooks = {}
932 self.scalingmode_change()
934 menudef = [
935 ('Scaling based on Minimum and Maximum',
936 ('minmax', 'minmax')),
937 ('Scaling based on Minimum and Maximum (Robust)',
938 ('minmax', 'robust')),
939 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
940 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
941 ]
943 self.menuitems_scaling_base = add_radiobuttongroup(
944 scale_menu, menudef, self.scaling_base_change)
946 self.scaling_base = self.menuitems_scaling_base[0][1]
947 scale_menu.addSeparator()
949 self.menuitem_fixscalerange = scale_menu.addAction(
950 'Fix Scale Ranges')
951 self.menuitem_fixscalerange.setCheckable(True)
953 # Sort Menu
954 def sector_dist(sta):
955 if sta.dist_m is None:
956 return None, None
957 else:
958 return (
959 sector_int(round((sta.azimuth+15.)/30.)),
960 m_float(sta.dist_m))
962 menudef = [
963 ('Sort by Names',
964 lambda tr: (),
965 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
966 ('Sort by Distance',
967 lambda tr: self.station_attrib(
968 tr,
969 lambda sta: (m_float_or_none(sta.dist_m),),
970 lambda tr: (None,)),
971 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
972 ('Sort by Azimuth',
973 lambda tr: self.station_attrib(
974 tr,
975 lambda sta: (deg_float_or_none(sta.azimuth),),
976 lambda tr: (None,))),
977 ('Sort by Distance in 12 Azimuthal Blocks',
978 lambda tr: self.station_attrib(
979 tr,
980 sector_dist,
981 lambda tr: (None, None))),
982 ('Sort by Backazimuth',
983 lambda tr: self.station_attrib(
984 tr,
985 lambda sta: (deg_float_or_none(sta.backazimuth),),
986 lambda tr: (None,))),
987 ]
988 self.menuitems_ssorting = add_radiobuttongroup(
989 sort_menu, menudef, self.s_sortingmode_change)
990 sort_menu.addSeparator()
992 self._ssort = lambda tr: ()
994 self.menu.addSeparator()
996 menudef = [
997 ('Subsort by Network, Station, Location, Channel',
998 ((0, 1, 2, 3), # gathering
999 lambda tr: tr.location)), # coloring
1000 ('Subsort by Network, Station, Channel, Location',
1001 ((0, 1, 3, 2),
1002 lambda tr: tr.channel)),
1003 ('Subsort by Station, Network, Channel, Location',
1004 ((1, 0, 3, 2),
1005 lambda tr: tr.channel)),
1006 ('Subsort by Location, Network, Station, Channel',
1007 ((2, 0, 1, 3),
1008 lambda tr: tr.channel)),
1009 ('Subsort by Channel, Network, Station, Location',
1010 ((3, 0, 1, 2),
1011 lambda tr: (tr.network, tr.station, tr.location))),
1012 ('Subsort by Network, Station, Channel (Grouped by Location)',
1013 ((0, 1, 3),
1014 lambda tr: tr.location)),
1015 ('Subsort by Station, Network, Channel (Grouped by Location)',
1016 ((1, 0, 3),
1017 lambda tr: tr.location)),
1018 ]
1020 self.menuitems_sorting = add_radiobuttongroup(
1021 sort_menu, menudef, self.sortingmode_change)
1023 menudef = [(x.key, x.value) for x in
1024 self.config.visible_length_setting]
1026 # View menu
1027 self.menuitems_visible_length = add_radiobuttongroup(
1028 view_menu, menudef,
1029 self.visible_length_change)
1030 view_menu.addSeparator()
1032 view_modes = [
1033 ('Wiggle Plot', ViewMode.Wiggle),
1034 ('Waterfall', ViewMode.Waterfall)
1035 ]
1037 self.menuitems_viewmode = add_radiobuttongroup(
1038 view_menu, view_modes,
1039 self.viewmode_change, default=ViewMode.Wiggle)
1040 view_menu.addSeparator()
1042 self.menuitem_cliptraces = view_menu.addAction(
1043 'Clip Traces')
1044 self.menuitem_cliptraces.setCheckable(True)
1045 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1047 self.menuitem_showboxes = view_menu.addAction(
1048 'Show Boxes')
1049 self.menuitem_showboxes.setCheckable(True)
1050 self.menuitem_showboxes.setChecked(
1051 self.config.show_boxes)
1053 self.menuitem_colortraces = view_menu.addAction(
1054 'Color Traces')
1055 self.menuitem_colortraces.setCheckable(True)
1056 self.menuitem_antialias = view_menu.addAction(
1057 'Antialiasing')
1058 self.menuitem_antialias.setCheckable(True)
1060 view_menu.addSeparator()
1061 self.menuitem_showscalerange = view_menu.addAction(
1062 'Show Scale Ranges')
1063 self.menuitem_showscalerange.setCheckable(True)
1064 self.menuitem_showscalerange.setChecked(
1065 self.config.show_scale_ranges)
1067 self.menuitem_showscaleaxis = view_menu.addAction(
1068 'Show Scale Axes')
1069 self.menuitem_showscaleaxis.setCheckable(True)
1070 self.menuitem_showscaleaxis.setChecked(
1071 self.config.show_scale_axes)
1073 self.menuitem_showzeroline = view_menu.addAction(
1074 'Show Zero Lines')
1075 self.menuitem_showzeroline.setCheckable(True)
1077 view_menu.addSeparator()
1078 view_menu.addAction(
1079 qg.QIcon.fromTheme('view-fullscreen'),
1080 'Fullscreen',
1081 self.toggle_fullscreen,
1082 qg.QKeySequence(qc.Qt.Key_F11))
1084 # Options Menu
1085 self.menuitem_demean = options_menu.addAction('Demean')
1086 self.menuitem_demean.setCheckable(True)
1087 self.menuitem_demean.setChecked(self.config.demean)
1088 self.menuitem_demean.setShortcut(
1089 qg.QKeySequence(qc.Qt.Key_Underscore))
1091 self.menuitem_distances_3d = options_menu.addAction(
1092 '3D distances',
1093 self.distances_3d_changed)
1094 self.menuitem_distances_3d.setCheckable(True)
1096 self.menuitem_allowdownsampling = options_menu.addAction(
1097 'Allow Downsampling')
1098 self.menuitem_allowdownsampling.setCheckable(True)
1099 self.menuitem_allowdownsampling.setChecked(True)
1101 self.menuitem_degap = options_menu.addAction(
1102 'Allow Degapping')
1103 self.menuitem_degap.setCheckable(True)
1104 self.menuitem_degap.setChecked(True)
1106 options_menu.addSeparator()
1108 self.menuitem_fft_filtering = options_menu.addAction(
1109 'FFT Filtering')
1110 self.menuitem_fft_filtering.setCheckable(True)
1112 self.menuitem_lphp = options_menu.addAction(
1113 'Bandpass is Low- + Highpass')
1114 self.menuitem_lphp.setCheckable(True)
1115 self.menuitem_lphp.setChecked(True)
1117 options_menu.addSeparator()
1118 self.menuitem_watch = options_menu.addAction(
1119 'Watch Files')
1120 self.menuitem_watch.setCheckable(True)
1122 self.menuitem_liberal_fetch = options_menu.addAction(
1123 'Liberal Fetch Optimization')
1124 self.menuitem_liberal_fetch.setCheckable(True)
1126 self.visible_length = menudef[0][1]
1128 self.snufflings_menu.addAction(
1129 'Reload Snufflings',
1130 self.setup_snufflings)
1132 # Disable ShadowPileTest
1133 if False:
1134 test_action = self.menu.addAction(
1135 'Test',
1136 self.toggletest)
1137 test_action.setCheckable(True)
1139 help_menu.addAction(
1140 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1141 'Snuffler Controls',
1142 self.help,
1143 qg.QKeySequence(qc.Qt.Key_Question))
1145 help_menu.addAction(
1146 'About',
1147 self.about)
1149 self.time_projection = Projection()
1150 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1151 self.time_projection.set_out_range(0., self.width())
1153 self.gather = None
1155 self.trace_filter = None
1156 self.quick_filter = None
1157 self.quick_filter_patterns = None, None
1158 self.blacklist = []
1160 self.track_to_screen = Projection()
1161 self.track_to_nslc_ids = {}
1163 self.cached_vec = None
1164 self.cached_processed_traces = None
1166 self.timer = qc.QTimer(self)
1167 self.timer.timeout.connect(self.periodical)
1168 self.timer.setInterval(1000)
1169 self.timer.start()
1170 self.pile.add_listener(self)
1171 self.trace_styles = {}
1172 if self.get_squirrel() is None:
1173 self.determine_box_styles()
1175 self.setMouseTracking(True)
1177 user_home_dir = os.path.expanduser('~')
1178 self.snuffling_modules = {}
1179 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1180 self.default_snufflings = None
1181 self.snufflings = []
1183 self.stations = {}
1185 self.timer_draw = Timer()
1186 self.timer_cutout = Timer()
1187 self.time_spent_painting = 0.0
1188 self.time_last_painted = time.time()
1190 self.interactive_range_change_time = 0.0
1191 self.interactive_range_change_delay_time = 10.0
1192 self.follow_timer = None
1194 self.sortingmode_change_time = 0.0
1195 self.sortingmode_change_delay_time = None
1197 self.old_data_ranges = {}
1199 self.error_messages = {}
1200 self.return_tag = None
1201 self.wheel_pos = 60
1203 self.setAcceptDrops(True)
1204 self._paths_to_load = []
1206 self.tf_cache = {}
1208 self.waterfall = TraceWaterfall()
1209 self.waterfall_cmap = 'viridis'
1210 self.waterfall_clip_min = 0.
1211 self.waterfall_clip_max = 1.
1212 self.waterfall_show_absolute = False
1213 self.waterfall_integrate = False
1214 self.view_mode = ViewMode.Wiggle
1216 self.automatic_updates = True
1218 self.closing = False
1219 self.in_paint_event = False
1221 def fail(self, reason):
1222 box = qw.QMessageBox(self)
1223 box.setText(reason)
1224 box.exec_()
1226 def set_trace_filter(self, filter_func):
1227 self.trace_filter = filter_func
1228 self.sortingmode_change()
1230 def update_trace_filter(self):
1231 if self.blacklist:
1233 def blacklist_func(tr):
1234 return not pyrocko.util.match_nslc(
1235 self.blacklist, tr.nslc_id)
1237 else:
1238 blacklist_func = None
1240 if self.quick_filter is None and blacklist_func is None:
1241 self.set_trace_filter(None)
1242 elif self.quick_filter is None:
1243 self.set_trace_filter(blacklist_func)
1244 elif blacklist_func is None:
1245 self.set_trace_filter(self.quick_filter)
1246 else:
1247 self.set_trace_filter(
1248 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1250 def set_quick_filter(self, filter_func):
1251 self.quick_filter = filter_func
1252 self.update_trace_filter()
1254 def set_quick_filter_patterns(self, patterns, inputline=None):
1255 if patterns is not None:
1256 self.set_quick_filter(
1257 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1258 else:
1259 self.set_quick_filter(None)
1261 self.quick_filter_patterns = patterns, inputline
1263 def get_quick_filter_patterns(self):
1264 return self.quick_filter_patterns
1266 def add_blacklist_pattern(self, pattern):
1267 if pattern == 'empty':
1268 keys = set(self.pile.nslc_ids)
1269 trs = self.pile.all(
1270 tmin=self.tmin,
1271 tmax=self.tmax,
1272 load_data=False,
1273 degap=False)
1275 for tr in trs:
1276 if tr.nslc_id in keys:
1277 keys.remove(tr.nslc_id)
1279 for key in keys:
1280 xpattern = '.'.join(key)
1281 if xpattern not in self.blacklist:
1282 self.blacklist.append(xpattern)
1284 else:
1285 if pattern in self.blacklist:
1286 self.blacklist.remove(pattern)
1288 self.blacklist.append(pattern)
1290 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1291 self.update_trace_filter()
1293 def remove_blacklist_pattern(self, pattern):
1294 if pattern in self.blacklist:
1295 self.blacklist.remove(pattern)
1296 else:
1297 raise PileViewerMainException(
1298 'Pattern not found in blacklist.')
1300 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1301 self.update_trace_filter()
1303 def clear_blacklist(self):
1304 self.blacklist = []
1305 self.update_trace_filter()
1307 def ssort(self, tr):
1308 return self._ssort(tr)
1310 def station_key(self, x):
1311 return x.network, x.station
1313 def station_keys(self, x):
1314 return [
1315 (x.network, x.station, x.location),
1316 (x.network, x.station)]
1318 def station_attrib(self, tr, getter, default_getter):
1319 for sk in self.station_keys(tr):
1320 if sk in self.stations:
1321 station = self.stations[sk]
1322 return getter(station)
1324 return default_getter(tr)
1326 def get_station(self, sk):
1327 return self.stations[sk]
1329 def has_station(self, station):
1330 for sk in self.station_keys(station):
1331 if sk in self.stations:
1332 return True
1334 return False
1336 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1337 return self.station_attrib(
1338 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1340 def set_stations(self, stations):
1341 self.stations = {}
1342 self.add_stations(stations)
1344 def add_stations(self, stations):
1345 for station in stations:
1346 for sk in self.station_keys(station):
1347 self.stations[sk] = station
1349 ev = self.get_active_event()
1350 if ev:
1351 self.set_origin(ev)
1353 def add_event(self, event):
1354 marker = EventMarker(event)
1355 self.add_marker(marker)
1357 def add_events(self, events):
1358 markers = [EventMarker(e) for e in events]
1359 self.add_markers(markers)
1361 def set_event_marker_as_origin(self, ignore=None):
1362 selected = self.selected_markers()
1363 if not selected:
1364 self.fail('An event marker must be selected.')
1365 return
1367 m = selected[0]
1368 if not isinstance(m, EventMarker):
1369 self.fail('Selected marker is not an event.')
1370 return
1372 self.set_active_event_marker(m)
1374 def deactivate_event_marker(self):
1375 if self.active_event_marker:
1376 self.active_event_marker.active = False
1378 self.active_event_marker_changed.emit()
1379 self.active_event_marker = None
1381 def set_active_event_marker(self, event_marker):
1382 if self.active_event_marker:
1383 self.active_event_marker.active = False
1385 self.active_event_marker = event_marker
1386 event_marker.active = True
1387 event = event_marker.get_event()
1388 self.set_origin(event)
1389 self.active_event_marker_changed.emit()
1391 def set_active_event(self, event):
1392 for marker in self.markers:
1393 if isinstance(marker, EventMarker):
1394 if marker.get_event() is event:
1395 self.set_active_event_marker(marker)
1397 def get_active_event_marker(self):
1398 return self.active_event_marker
1400 def get_active_event(self):
1401 m = self.get_active_event_marker()
1402 if m is not None:
1403 return m.get_event()
1404 else:
1405 return None
1407 def get_active_markers(self):
1408 emarker = self.get_active_event_marker()
1409 if emarker is None:
1410 return None, []
1412 else:
1413 ev = emarker.get_event()
1414 pmarkers = [
1415 m for m in self.markers
1416 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1418 return emarker, pmarkers
1420 def set_origin(self, location):
1421 for station in self.stations.values():
1422 station.set_event_relative_data(
1423 location,
1424 distance_3d=self.menuitem_distances_3d.isChecked())
1426 self.sortingmode_change()
1428 def distances_3d_changed(self):
1429 ignore = self.menuitem_distances_3d.isChecked()
1430 self.set_event_marker_as_origin(ignore)
1432 def iter_snuffling_modules(self):
1433 pjoin = os.path.join
1434 for path in self.snuffling_paths:
1436 if not os.path.isdir(path):
1437 os.mkdir(path)
1439 for entry in os.listdir(path):
1440 directory = path
1441 fn = entry
1442 d = pjoin(path, entry)
1443 if os.path.isdir(d):
1444 directory = d
1445 if os.path.isfile(
1446 os.path.join(directory, 'snuffling.py')):
1447 fn = 'snuffling.py'
1449 if not fn.endswith('.py'):
1450 continue
1452 name = fn[:-3]
1454 if (directory, name) not in self.snuffling_modules:
1455 self.snuffling_modules[directory, name] = \
1456 pyrocko.gui.snuffling.SnufflingModule(
1457 directory, name, self)
1459 yield self.snuffling_modules[directory, name]
1461 def setup_snufflings(self):
1462 # user snufflings
1463 for mod in self.iter_snuffling_modules():
1464 try:
1465 mod.load_if_needed()
1466 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1467 logger.warning('Snuffling module "%s" is broken' % e)
1469 # load the default snufflings on first run
1470 if self.default_snufflings is None:
1471 self.default_snufflings = pyrocko.gui\
1472 .snufflings.__snufflings__()
1473 for snuffling in self.default_snufflings:
1474 self.add_snuffling(snuffling)
1476 def set_panel_parent(self, panel_parent):
1477 self.panel_parent = panel_parent
1479 def get_panel_parent(self):
1480 return self.panel_parent
1482 def add_snuffling(self, snuffling, reloaded=False):
1483 logger.debug('Adding snuffling %s' % snuffling.get_name())
1484 snuffling.init_gui(
1485 self, self.get_panel_parent(), self, reloaded=reloaded)
1486 self.snufflings.append(snuffling)
1487 self.update()
1489 def remove_snuffling(self, snuffling):
1490 snuffling.delete_gui()
1491 self.update()
1492 self.snufflings.remove(snuffling)
1493 snuffling.pre_destroy()
1495 def add_snuffling_menuitem(self, item):
1496 self.snufflings_menu.addAction(item)
1497 item.setParent(self.snufflings_menu)
1498 sort_actions(self.snufflings_menu)
1500 def remove_snuffling_menuitem(self, item):
1501 self.snufflings_menu.removeAction(item)
1503 def add_snuffling_help_menuitem(self, item):
1504 self.snuffling_help.addAction(item)
1505 item.setParent(self.snuffling_help)
1506 sort_actions(self.snuffling_help)
1508 def remove_snuffling_help_menuitem(self, item):
1509 self.snuffling_help.removeAction(item)
1511 def add_panel_toggler(self, item):
1512 self.toggle_panel_menu.addAction(item)
1513 item.setParent(self.toggle_panel_menu)
1514 sort_actions(self.toggle_panel_menu)
1516 def remove_panel_toggler(self, item):
1517 self.toggle_panel_menu.removeAction(item)
1519 def load(self, paths, regex=None, format='detect',
1520 cache_dir=None, force_cache=False):
1522 if cache_dir is None:
1523 cache_dir = pyrocko.config.config().cache_dir
1524 if isinstance(paths, str):
1525 paths = [paths]
1527 fns = pyrocko.util.select_files(
1528 paths, selector=None, include=regex, show_progress=False)
1530 if not fns:
1531 return
1533 cache = pyrocko.pile.get_cache(cache_dir)
1535 t = [time.time()]
1537 def update_bar(label, value):
1538 pbs = self.parent().get_progressbars()
1539 if label.lower() == 'looking at files':
1540 label = 'Looking at %i files' % len(fns)
1541 else:
1542 label = 'Scanning %i files' % len(fns)
1544 return pbs.set_status(label, value)
1546 def update_progress(label, i, n):
1547 abort = False
1549 qw.qApp.processEvents()
1550 if n != 0:
1551 perc = i*100/n
1552 else:
1553 perc = 100
1554 abort |= update_bar(label, perc)
1555 abort |= self.window().is_closing()
1557 tnow = time.time()
1558 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1559 self.update()
1560 t[0] = tnow
1562 return abort
1564 self.automatic_updates = False
1566 self.pile.load_files(
1567 sorted(fns),
1568 filename_attributes=regex,
1569 cache=cache,
1570 fileformat=format,
1571 show_progress=False,
1572 update_progress=update_progress)
1574 self.automatic_updates = True
1575 self.update()
1577 def load_queued(self):
1578 if not self._paths_to_load:
1579 return
1580 paths = self._paths_to_load
1581 self._paths_to_load = []
1582 self.load(paths)
1584 def load_soon(self, paths):
1585 self._paths_to_load.extend(paths)
1586 qc.QTimer.singleShot(200, self.load_queued)
1588 def open_waveforms(self):
1589 caption = 'Select one or more files to open'
1591 fns, _ = qw.QFileDialog.getOpenFileNames(
1592 self, caption, options=qfiledialog_options)
1594 if fns:
1595 self.load(list(str(fn) for fn in fns))
1597 def open_waveform_directory(self):
1598 caption = 'Select directory to scan for waveform files'
1600 dn = qw.QFileDialog.getExistingDirectory(
1601 self, caption, options=qfiledialog_options)
1603 if dn:
1604 self.load([str(dn)])
1606 def open_stations(self, fns=None):
1607 caption = 'Select one or more Pyrocko station files to open'
1609 if not fns:
1610 fns, _ = qw.QFileDialog.getOpenFileNames(
1611 self, caption, options=qfiledialog_options)
1613 try:
1614 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1615 for stat in stations:
1616 self.add_stations(stat)
1618 except Exception as e:
1619 self.fail('Failed to read station file: %s' % str(e))
1621 def open_stations_xml(self, fns=None):
1622 from pyrocko.io import stationxml
1624 caption = 'Select one or more StationXML files'
1625 if not fns:
1626 fns, _ = qw.QFileDialog.getOpenFileNames(
1627 self, caption, options=qfiledialog_options,
1628 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1629 ';;All files (*)')
1631 try:
1632 stations = [
1633 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1634 for x in fns]
1636 for stat in stations:
1637 self.add_stations(stat)
1639 except Exception as e:
1640 self.fail('Failed to read StationXML file: %s' % str(e))
1642 def add_traces(self, traces):
1643 if traces:
1644 mtf = pyrocko.pile.MemTracesFile(None, traces)
1645 self.pile.add_file(mtf)
1646 ticket = (self.pile, mtf)
1647 return ticket
1648 else:
1649 return (None, None)
1651 def release_data(self, tickets):
1652 for ticket in tickets:
1653 pile, mtf = ticket
1654 if pile is not None:
1655 pile.remove_file(mtf)
1657 def periodical(self):
1658 if self.menuitem_watch.isChecked():
1659 if self.pile.reload_modified():
1660 self.update()
1662 def get_pile(self):
1663 return self.pile
1665 def pile_changed(self, what):
1666 self.pile_has_changed = True
1667 self.pile_has_changed_signal.emit()
1668 if self.automatic_updates:
1669 self.update()
1671 def set_gathering(self, gather=None, color=None):
1673 if gather is None:
1674 def gather_func(tr):
1675 return tr.nslc_id
1677 gather = (0, 1, 2, 3)
1679 else:
1680 def gather_func(tr):
1681 return (
1682 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1684 if color is None:
1685 def color(tr):
1686 return tr.location
1688 self.gather = gather_func
1689 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1691 self.color_gather = color
1692 self.color_keys = self.pile.gather_keys(color)
1693 previous_ntracks = self.ntracks
1694 self.set_ntracks(len(keys))
1696 if self.shown_tracks_range is None or \
1697 previous_ntracks == 0 or \
1698 self.show_all:
1700 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1701 key_at_top = None
1702 n = high-low
1704 else:
1705 low, high = self.shown_tracks_range
1706 key_at_top = self.track_keys[low]
1707 n = high-low
1709 self.track_keys = sorted(keys)
1711 track_patterns = []
1712 for k in self.track_keys:
1713 pat = ['*', '*', '*', '*']
1714 for i, j in enumerate(gather):
1715 pat[j] = k[-len(gather)+i]
1717 track_patterns.append(pat)
1719 self.track_patterns = track_patterns
1721 if key_at_top is not None:
1722 try:
1723 ind = self.track_keys.index(key_at_top)
1724 low = ind
1725 high = low+n
1726 except Exception:
1727 pass
1729 self.set_tracks_range((low, high))
1731 self.key_to_row = dict(
1732 [(key, i) for (i, key) in enumerate(self.track_keys)])
1734 def inrange(x, r):
1735 return r[0] <= x and x < r[1]
1737 def trace_selector(trace):
1738 gt = self.gather(trace)
1739 return (
1740 gt in self.key_to_row and
1741 inrange(self.key_to_row[gt], self.shown_tracks_range))
1743 self.trace_selector = lambda x: \
1744 (self.trace_filter is None or self.trace_filter(x)) \
1745 and trace_selector(x)
1747 if self.tmin == working_system_time_range[0] and \
1748 self.tmax == working_system_time_range[1] or \
1749 self.show_all:
1751 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1752 if tmin is not None and tmax is not None:
1753 tlen = (tmax - tmin)
1754 tpad = tlen * 5./self.width()
1755 self.set_time_range(tmin-tpad, tmax+tpad)
1757 def set_time_range(self, tmin, tmax):
1758 if tmin is None:
1759 tmin = initial_time_range[0]
1761 if tmax is None:
1762 tmax = initial_time_range[1]
1764 if tmin > tmax:
1765 tmin, tmax = tmax, tmin
1767 if tmin == tmax:
1768 tmin -= 1.
1769 tmax += 1.
1771 tmin = max(working_system_time_range[0], tmin)
1772 tmax = min(working_system_time_range[1], tmax)
1774 min_deltat = self.content_deltat_range()[0]
1775 if (tmax - tmin < min_deltat):
1776 m = (tmin + tmax) / 2.
1777 tmin = m - min_deltat/2.
1778 tmax = m + min_deltat/2.
1780 self.time_projection.set_in_range(tmin, tmax)
1781 self.tmin, self.tmax = tmin, tmax
1783 def get_time_range(self):
1784 return self.tmin, self.tmax
1786 def ypart(self, y):
1787 if y < self.ax_height:
1788 return -1
1789 elif y > self.height()-self.ax_height:
1790 return 1
1791 else:
1792 return 0
1794 def time_fractional_digits(self):
1795 min_deltat = self.content_deltat_range()[0]
1796 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1798 def write_markers(self, fn=None):
1799 caption = "Choose a file name to write markers"
1800 if not fn:
1801 fn, _ = qw.QFileDialog.getSaveFileName(
1802 self, caption, options=qfiledialog_options)
1803 if fn:
1804 try:
1805 Marker.save_markers(
1806 self.markers, fn,
1807 fdigits=self.time_fractional_digits())
1809 except Exception as e:
1810 self.fail('Failed to write marker file: %s' % str(e))
1812 def write_selected_markers(self, fn=None):
1813 caption = "Choose a file name to write selected markers"
1814 if not fn:
1815 fn, _ = qw.QFileDialog.getSaveFileName(
1816 self, caption, options=qfiledialog_options)
1817 if fn:
1818 try:
1819 Marker.save_markers(
1820 self.iter_selected_markers(),
1821 fn,
1822 fdigits=self.time_fractional_digits())
1824 except Exception as e:
1825 self.fail('Failed to write marker file: %s' % str(e))
1827 def read_events(self, fn=None):
1828 '''
1829 Open QFileDialog to open, read and add
1830 :py:class:`pyrocko.model.Event` instances and their marker
1831 representation to the pile viewer.
1832 '''
1833 caption = "Selet one or more files to open"
1834 if not fn:
1835 fn, _ = qw.QFileDialog.getOpenFileName(
1836 self, caption, options=qfiledialog_options)
1837 if fn:
1838 try:
1839 self.add_events(pyrocko.model.load_events(fn))
1840 self.associate_phases_to_events()
1842 except Exception as e:
1843 self.fail('Failed to read event file: %s' % str(e))
1845 def read_markers(self, fn=None):
1846 '''
1847 Open QFileDialog to open, read and add markers to the pile viewer.
1848 '''
1849 caption = "Selet one or more marker files to open"
1850 if not fn:
1851 fn, _ = qw.QFileDialog.getOpenFileName(
1852 self, caption, options=qfiledialog_options)
1853 if fn:
1854 try:
1855 self.add_markers(Marker.load_markers(fn))
1856 self.associate_phases_to_events()
1858 except Exception as e:
1859 self.fail('Failed to read marker file: %s' % str(e))
1861 def associate_phases_to_events(self):
1862 associate_phases_to_events(self.markers)
1864 def add_marker(self, marker):
1865 # need index to inform QAbstactTableModel about upcoming change,
1866 # but have to restore current state in order to not cause problems
1867 self.markers.insert(marker)
1868 i = self.markers.remove(marker)
1870 self.begin_markers_add.emit(i, i)
1871 self.markers.insert(marker)
1872 self.end_markers_add.emit()
1873 self.markers_deltat_max = max(
1874 self.markers_deltat_max, marker.tmax - marker.tmin)
1876 def add_markers(self, markers):
1877 if not self.markers:
1878 self.begin_markers_add.emit(0, len(markers) - 1)
1879 self.markers.insert_many(markers)
1880 self.end_markers_add.emit()
1881 self.update_markers_deltat_max()
1882 else:
1883 for marker in markers:
1884 self.add_marker(marker)
1886 def update_markers_deltat_max(self):
1887 if self.markers:
1888 self.markers_deltat_max = max(
1889 marker.tmax - marker.tmin for marker in self.markers)
1891 def remove_marker(self, marker):
1892 '''
1893 Remove a ``marker`` from the :py:class:`PileViewer`.
1895 :param marker: :py:class:`Marker` (or subclass) instance
1896 '''
1898 if marker is self.active_event_marker:
1899 self.deactivate_event_marker()
1901 try:
1902 i = self.markers.index(marker)
1903 self.begin_markers_remove.emit(i, i)
1904 self.markers.remove_at(i)
1905 self.end_markers_remove.emit()
1906 except ValueError:
1907 pass
1909 def remove_markers(self, markers):
1910 '''
1911 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1913 :param markers: list of :py:class:`Marker` (or subclass)
1914 instances
1915 '''
1917 if markers is self.markers:
1918 markers = list(markers)
1920 for marker in markers:
1921 self.remove_marker(marker)
1923 self.update_markers_deltat_max()
1925 def remove_selected_markers(self):
1926 def delete_segment(istart, iend):
1927 self.begin_markers_remove.emit(istart, iend-1)
1928 for _ in range(iend - istart):
1929 self.markers.remove_at(istart)
1931 self.end_markers_remove.emit()
1933 istart = None
1934 ipos = 0
1935 markers = self.markers
1936 nmarkers = len(self.markers)
1937 while ipos < nmarkers:
1938 marker = markers[ipos]
1939 if marker.is_selected():
1940 if marker is self.active_event_marker:
1941 self.deactivate_event_marker()
1943 if istart is None:
1944 istart = ipos
1945 else:
1946 if istart is not None:
1947 delete_segment(istart, ipos)
1948 nmarkers -= ipos - istart
1949 ipos = istart - 1
1950 istart = None
1952 ipos += 1
1954 if istart is not None:
1955 delete_segment(istart, ipos)
1957 self.update_markers_deltat_max()
1959 def selected_markers(self):
1960 return [marker for marker in self.markers if marker.is_selected()]
1962 def iter_selected_markers(self):
1963 for marker in self.markers:
1964 if marker.is_selected():
1965 yield marker
1967 def get_markers(self):
1968 return self.markers
1970 def mousePressEvent(self, mouse_ev):
1971 self.show_all = False
1972 point = self.mapFromGlobal(mouse_ev.globalPos())
1974 if mouse_ev.button() == qc.Qt.LeftButton:
1975 marker = self.marker_under_cursor(point.x(), point.y())
1976 if self.picking:
1977 if self.picking_down is None:
1978 self.picking_down = (
1979 self.time_projection.rev(mouse_ev.x()),
1980 mouse_ev.y())
1982 elif marker is not None:
1983 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1984 self.deselect_all()
1985 marker.selected = True
1986 self.emit_selected_markers()
1987 self.update()
1988 else:
1989 self.track_start = mouse_ev.x(), mouse_ev.y()
1990 self.track_trange = self.tmin, self.tmax
1992 if mouse_ev.button() == qc.Qt.RightButton \
1993 and isinstance(self.menu, qw.QMenu):
1994 self.menu.exec_(qg.QCursor.pos())
1995 self.update_status()
1997 def mouseReleaseEvent(self, mouse_ev):
1998 if self.ignore_releases:
1999 self.ignore_releases -= 1
2000 return
2002 if self.picking:
2003 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2004 self.emit_selected_markers()
2006 if self.track_start:
2007 self.update()
2009 self.track_start = None
2010 self.track_trange = None
2011 self.update_status()
2013 def mouseDoubleClickEvent(self, mouse_ev):
2014 self.show_all = False
2015 self.start_picking(None)
2016 self.ignore_releases = 1
2018 def mouseMoveEvent(self, mouse_ev):
2019 point = self.mapFromGlobal(mouse_ev.globalPos())
2021 if self.picking:
2022 self.update_picking(point.x(), point.y())
2024 elif self.track_start is not None:
2025 x0, y0 = self.track_start
2026 dx = (point.x() - x0)/float(self.width())
2027 dy = (point.y() - y0)/float(self.height())
2028 if self.ypart(y0) == 1:
2029 dy = 0
2031 tmin0, tmax0 = self.track_trange
2033 scale = math.exp(-dy*5.)
2034 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2035 frac = x0/float(self.width())
2036 dt = dx*(tmax0-tmin0)*scale
2038 self.interrupt_following()
2039 self.set_time_range(
2040 tmin0 - dt - dtr*frac,
2041 tmax0 - dt + dtr*(1.-frac))
2043 self.update()
2044 else:
2045 self.hoovering(point.x(), point.y())
2047 self.update_status()
2049 def nslc_ids_under_cursor(self, x, y):
2050 ftrack = self.track_to_screen.rev(y)
2051 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2052 return nslc_ids
2054 def marker_under_cursor(self, x, y):
2055 mouset = self.time_projection.rev(x)
2056 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2057 relevant_nslc_ids = None
2058 for marker in self.markers:
2059 if marker.kind not in self.visible_marker_kinds:
2060 continue
2062 if (abs(mouset-marker.tmin) < deltat or
2063 abs(mouset-marker.tmax) < deltat):
2065 if relevant_nslc_ids is None:
2066 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2068 marker_nslc_ids = marker.get_nslc_ids()
2069 if not marker_nslc_ids:
2070 return marker
2072 for nslc_id in marker_nslc_ids:
2073 if nslc_id in relevant_nslc_ids:
2074 return marker
2076 def hoovering(self, x, y):
2077 mouset = self.time_projection.rev(x)
2078 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2079 needupdate = False
2080 haveone = False
2081 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2082 for marker in self.markers:
2083 if marker.kind not in self.visible_marker_kinds:
2084 continue
2086 state = abs(mouset-marker.tmin) < deltat or \
2087 abs(mouset-marker.tmax) < deltat and not haveone
2089 if state:
2090 xstate = False
2092 marker_nslc_ids = marker.get_nslc_ids()
2093 if not marker_nslc_ids:
2094 xstate = True
2096 for nslc in relevant_nslc_ids:
2097 if marker.match_nslc(nslc):
2098 xstate = True
2100 state = xstate
2102 if state:
2103 haveone = True
2104 oldstate = marker.is_alerted()
2105 if oldstate != state:
2106 needupdate = True
2107 marker.set_alerted(state)
2108 if state:
2109 self.message = marker.hoover_message()
2111 if not haveone:
2112 self.message = None
2114 if needupdate:
2115 self.update()
2117 def event(self, event):
2118 if event.type() == qc.QEvent.KeyPress:
2119 self.keyPressEvent(event)
2120 return True
2121 else:
2122 return base.event(self, event)
2124 def keyPressEvent(self, key_event):
2125 self.show_all = False
2126 dt = self.tmax - self.tmin
2127 tmid = (self.tmin + self.tmax) / 2.
2129 key = key_event.key()
2130 try:
2131 keytext = str(key_event.text())
2132 except UnicodeEncodeError:
2133 return
2135 if key == qc.Qt.Key_Space:
2136 self.interrupt_following()
2137 self.set_time_range(self.tmin+dt, self.tmax+dt)
2139 elif key == qc.Qt.Key_Up:
2140 for m in self.selected_markers():
2141 if isinstance(m, PhaseMarker):
2142 if key_event.modifiers() & qc.Qt.ShiftModifier:
2143 p = 0
2144 else:
2145 p = 1 if m.get_polarity() != 1 else None
2146 m.set_polarity(p)
2148 elif key == qc.Qt.Key_Down:
2149 for m in self.selected_markers():
2150 if isinstance(m, PhaseMarker):
2151 if key_event.modifiers() & qc.Qt.ShiftModifier:
2152 p = 0
2153 else:
2154 p = -1 if m.get_polarity() != -1 else None
2155 m.set_polarity(p)
2157 elif key == qc.Qt.Key_B:
2158 dt = self.tmax - self.tmin
2159 self.interrupt_following()
2160 self.set_time_range(self.tmin-dt, self.tmax-dt)
2162 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2163 self.interrupt_following()
2165 tgo = None
2167 class TraceDummy(object):
2168 def __init__(self, marker):
2169 self._marker = marker
2171 @property
2172 def nslc_id(self):
2173 return self._marker.one_nslc()
2175 def marker_to_itrack(marker):
2176 try:
2177 return self.key_to_row.get(
2178 self.gather(TraceDummy(marker)), -1)
2180 except MarkerOneNSLCRequired:
2181 return -1
2183 emarker, pmarkers = self.get_active_markers()
2184 pmarkers = [
2185 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2186 pmarkers.sort(key=lambda m: (
2187 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2189 if key == qc.Qt.Key_Backtab:
2190 pmarkers.reverse()
2192 smarkers = self.selected_markers()
2193 iselected = []
2194 for sm in smarkers:
2195 try:
2196 iselected.append(pmarkers.index(sm))
2197 except ValueError:
2198 pass
2200 if iselected:
2201 icurrent = max(iselected) + 1
2202 else:
2203 icurrent = 0
2205 if icurrent < len(pmarkers):
2206 self.deselect_all()
2207 cmarker = pmarkers[icurrent]
2208 cmarker.selected = True
2209 tgo = cmarker.tmin
2210 if not self.tmin < tgo < self.tmax:
2211 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2213 itrack = marker_to_itrack(cmarker)
2214 if itrack != -1:
2215 if itrack < self.shown_tracks_range[0]:
2216 self.scroll_tracks(
2217 - (self.shown_tracks_range[0] - itrack))
2218 elif self.shown_tracks_range[1] <= itrack:
2219 self.scroll_tracks(
2220 itrack - self.shown_tracks_range[1]+1)
2222 if itrack not in self.track_to_nslc_ids:
2223 self.go_to_selection()
2225 elif keytext in ('p', 'n', 'P', 'N'):
2226 smarkers = self.selected_markers()
2227 tgo = None
2228 dir = str(keytext)
2229 if smarkers:
2230 tmid = smarkers[0].tmin
2231 for smarker in smarkers:
2232 if dir == 'n':
2233 tmid = max(smarker.tmin, tmid)
2234 else:
2235 tmid = min(smarker.tmin, tmid)
2237 tgo = tmid
2239 if dir.lower() == 'n':
2240 for marker in sorted(
2241 self.markers,
2242 key=operator.attrgetter('tmin')):
2244 t = marker.tmin
2245 if t > tmid and \
2246 marker.kind in self.visible_marker_kinds and \
2247 (dir == 'n' or
2248 isinstance(marker, EventMarker)):
2250 self.deselect_all()
2251 marker.selected = True
2252 tgo = t
2253 break
2254 else:
2255 for marker in sorted(
2256 self.markers,
2257 key=operator.attrgetter('tmin'),
2258 reverse=True):
2260 t = marker.tmin
2261 if t < tmid and \
2262 marker.kind in self.visible_marker_kinds and \
2263 (dir == 'p' or
2264 isinstance(marker, EventMarker)):
2265 self.deselect_all()
2266 marker.selected = True
2267 tgo = t
2268 break
2270 if tgo is not None:
2271 self.interrupt_following()
2272 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2274 elif keytext == 'r':
2275 if self.pile.reload_modified():
2276 self.reloaded = True
2278 elif keytext == 'R':
2279 self.setup_snufflings()
2281 elif key == qc.Qt.Key_Backspace:
2282 self.remove_selected_markers()
2284 elif keytext == 'a':
2285 for marker in self.markers:
2286 if ((self.tmin <= marker.tmin <= self.tmax or
2287 self.tmin <= marker.tmax <= self.tmax) and
2288 marker.kind in self.visible_marker_kinds):
2289 marker.selected = True
2290 else:
2291 marker.selected = False
2293 elif keytext == 'A':
2294 for marker in self.markers:
2295 if marker.kind in self.visible_marker_kinds:
2296 marker.selected = True
2298 elif keytext == 'd':
2299 self.deselect_all()
2301 elif keytext == 'E':
2302 self.deactivate_event_marker()
2304 elif keytext == 'e':
2305 markers = self.selected_markers()
2306 event_markers_in_spe = [
2307 marker for marker in markers
2308 if not isinstance(marker, PhaseMarker)]
2310 phase_markers = [
2311 marker for marker in markers
2312 if isinstance(marker, PhaseMarker)]
2314 if len(event_markers_in_spe) == 1:
2315 event_marker = event_markers_in_spe[0]
2316 if not isinstance(event_marker, EventMarker):
2317 nslcs = list(event_marker.nslc_ids)
2318 lat, lon = 0.0, 0.0
2319 old = self.get_active_event()
2320 if len(nslcs) == 1:
2321 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2322 elif old is not None:
2323 lat, lon = old.lat, old.lon
2325 event_marker.convert_to_event_marker(lat, lon)
2327 self.set_active_event_marker(event_marker)
2328 event = event_marker.get_event()
2329 for marker in phase_markers:
2330 marker.set_event(event)
2332 else:
2333 for marker in event_markers_in_spe:
2334 marker.convert_to_event_marker()
2336 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2337 for marker in self.selected_markers():
2338 marker.set_kind(int(keytext))
2339 self.emit_selected_markers()
2341 elif key in fkey_map:
2342 self.handle_fkeys(key)
2344 elif key == qc.Qt.Key_Escape:
2345 if self.picking:
2346 self.stop_picking(0, 0, abort=True)
2348 elif key == qc.Qt.Key_PageDown:
2349 self.scroll_tracks(
2350 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2352 elif key == qc.Qt.Key_PageUp:
2353 self.scroll_tracks(
2354 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2356 elif key == qc.Qt.Key_Plus:
2357 self.zoom_tracks(0., 1.)
2359 elif key == qc.Qt.Key_Minus:
2360 self.zoom_tracks(0., -1.)
2362 elif key == qc.Qt.Key_Equal:
2363 ntracks_shown = self.shown_tracks_range[1] - \
2364 self.shown_tracks_range[0]
2365 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2366 self.zoom_tracks(0., dtracks)
2368 elif key == qc.Qt.Key_Colon:
2369 self.want_input.emit()
2371 elif keytext == 'f':
2372 self.toggle_fullscreen()
2374 elif keytext == 'g':
2375 self.go_to_selection()
2377 elif keytext == 'G':
2378 self.go_to_selection(tight=True)
2380 elif keytext == 'm':
2381 self.toggle_marker_editor()
2383 elif keytext == 'c':
2384 self.toggle_main_controls()
2386 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2387 dir = 1
2388 amount = 1
2389 if key_event.key() == qc.Qt.Key_Left:
2390 dir = -1
2391 if key_event.modifiers() & qc.Qt.ShiftModifier:
2392 amount = 10
2393 self.nudge_selected_markers(dir*amount)
2394 else:
2395 super().keyPressEvent(key_event)
2397 if keytext != '' and keytext in 'degaApPnN':
2398 self.emit_selected_markers()
2400 self.update()
2401 self.update_status()
2403 def handle_fkeys(self, key):
2404 self.set_phase_kind(
2405 self.selected_markers(),
2406 fkey_map[key] + 1)
2407 self.emit_selected_markers()
2409 def emit_selected_markers(self):
2410 ibounds = []
2411 last_selected = False
2412 for imarker, marker in enumerate(self.markers):
2413 this_selected = marker.is_selected()
2414 if this_selected != last_selected:
2415 ibounds.append(imarker)
2417 last_selected = this_selected
2419 if last_selected:
2420 ibounds.append(len(self.markers))
2422 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2423 self.n_selected_markers = sum(
2424 chunk[1] - chunk[0] for chunk in chunks)
2425 self.marker_selection_changed.emit(chunks)
2427 def toggle_marker_editor(self):
2428 self.panel_parent.toggle_marker_editor()
2430 def toggle_main_controls(self):
2431 self.panel_parent.toggle_main_controls()
2433 def nudge_selected_markers(self, npixels):
2434 a, b = self.time_projection.ur
2435 c, d = self.time_projection.xr
2436 for marker in self.selected_markers():
2437 if not isinstance(marker, EventMarker):
2438 marker.tmin += npixels * (d-c)/b
2439 marker.tmax += npixels * (d-c)/b
2441 def toggle_fullscreen(self):
2442 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2443 self.window().windowState() & qc.Qt.WindowMaximized:
2444 self.window().showNormal()
2445 else:
2446 if is_macos:
2447 self.window().showMaximized()
2448 else:
2449 self.window().showFullScreen()
2451 def about(self):
2452 fn = pyrocko.util.data_file('snuffler.png')
2453 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2454 txt = f.read()
2455 label = qw.QLabel(txt % {'logo': fn})
2456 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2457 self.show_doc('About', [label], target='tab')
2459 def help(self):
2460 class MyScrollArea(qw.QScrollArea):
2462 def sizeHint(self):
2463 s = qc.QSize()
2464 s.setWidth(self.widget().sizeHint().width())
2465 s.setHeight(self.widget().sizeHint().height())
2466 return s
2468 with open(pyrocko.util.data_file(
2469 'snuffler_help.html')) as f:
2470 hcheat = qw.QLabel(f.read())
2472 with open(pyrocko.util.data_file(
2473 'snuffler_help_epilog.html')) as f:
2474 hepilog = qw.QLabel(f.read())
2476 for h in [hcheat, hepilog]:
2477 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2478 h.setWordWrap(True)
2480 self.show_doc('Help', [hcheat, hepilog], target='panel')
2482 def show_doc(self, name, labels, target='panel'):
2483 scroller = qw.QScrollArea()
2484 frame = qw.QFrame(scroller)
2485 frame.setLineWidth(0)
2486 layout = qw.QVBoxLayout()
2487 layout.setContentsMargins(0, 0, 0, 0)
2488 layout.setSpacing(0)
2489 frame.setLayout(layout)
2490 scroller.setWidget(frame)
2491 scroller.setWidgetResizable(True)
2492 frame.setBackgroundRole(qg.QPalette.Base)
2493 for h in labels:
2494 h.setParent(frame)
2495 h.setMargin(3)
2496 h.setTextInteractionFlags(
2497 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2498 h.setBackgroundRole(qg.QPalette.Base)
2499 layout.addWidget(h)
2500 h.linkActivated.connect(
2501 self.open_link)
2503 if self.panel_parent is not None:
2504 if target == 'panel':
2505 self.panel_parent.add_panel(
2506 name, scroller, True, volatile=False)
2507 else:
2508 self.panel_parent.add_tab(name, scroller)
2510 def open_link(self, link):
2511 qg.QDesktopServices.openUrl(qc.QUrl(link))
2513 def wheelEvent(self, wheel_event):
2514 self.wheel_pos += wheel_event.angleDelta().y()
2516 n = self.wheel_pos // 120
2517 self.wheel_pos = self.wheel_pos % 120
2518 if n == 0:
2519 return
2521 amount = max(
2522 1.,
2523 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2524 wdelta = amount * n
2526 trmin, trmax = self.track_to_screen.get_in_range()
2527 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2528 / (trmax-trmin)
2530 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2531 self.zoom_tracks(anchor, wdelta)
2532 else:
2533 self.scroll_tracks(-wdelta)
2535 def dragEnterEvent(self, event):
2536 if event.mimeData().hasUrls():
2537 if any(url.toLocalFile() for url in event.mimeData().urls()):
2538 event.setDropAction(qc.Qt.LinkAction)
2539 event.accept()
2541 def dropEvent(self, event):
2542 if event.mimeData().hasUrls():
2543 paths = list(
2544 str(url.toLocalFile()) for url in event.mimeData().urls())
2545 event.acceptProposedAction()
2546 self.load(paths)
2548 def get_phase_name(self, kind):
2549 return self.config.get_phase_name(kind)
2551 def set_phase_kind(self, markers, kind):
2552 phasename = self.get_phase_name(kind)
2554 for marker in markers:
2555 if isinstance(marker, PhaseMarker):
2556 if kind == 10:
2557 marker.convert_to_marker()
2558 else:
2559 marker.set_phasename(phasename)
2560 marker.set_event(self.get_active_event())
2562 elif isinstance(marker, EventMarker):
2563 pass
2565 else:
2566 if kind != 10:
2567 event = self.get_active_event()
2568 marker.convert_to_phase_marker(
2569 event, phasename, None, False)
2571 def set_ntracks(self, ntracks):
2572 if self.ntracks != ntracks:
2573 self.ntracks = ntracks
2574 if self.shown_tracks_range is not None:
2575 l, h = self.shown_tracks_range
2576 else:
2577 l, h = 0, self.ntracks
2579 self.tracks_range_changed.emit(self.ntracks, l, h)
2581 def set_tracks_range(self, range, start=None):
2583 low, high = range
2584 low = min(self.ntracks-1, low)
2585 high = min(self.ntracks, high)
2586 low = max(0, low)
2587 high = max(1, high)
2589 if start is None:
2590 start = float(low)
2592 if self.shown_tracks_range != (low, high):
2593 self.shown_tracks_range = low, high
2594 self.shown_tracks_start = start
2596 self.tracks_range_changed.emit(self.ntracks, low, high)
2598 def scroll_tracks(self, shift):
2599 shown = self.shown_tracks_range
2600 shiftmin = -shown[0]
2601 shiftmax = self.ntracks-shown[1]
2602 shift = max(shiftmin, shift)
2603 shift = min(shiftmax, shift)
2604 shown = shown[0] + shift, shown[1] + shift
2606 self.set_tracks_range((int(shown[0]), int(shown[1])))
2608 self.update()
2610 def zoom_tracks(self, anchor, delta):
2611 ntracks_shown = self.shown_tracks_range[1] \
2612 - self.shown_tracks_range[0]
2614 if (ntracks_shown == 1 and delta <= 0) or \
2615 (ntracks_shown == self.ntracks and delta >= 0):
2616 return
2618 ntracks_shown += int(round(delta))
2619 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2621 u = self.shown_tracks_start
2622 nu = max(0., u-anchor*delta)
2623 nv = nu + ntracks_shown
2624 if nv > self.ntracks:
2625 nu -= nv - self.ntracks
2626 nv -= nv - self.ntracks
2628 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2630 self.ntracks_shown_max = self.shown_tracks_range[1] \
2631 - self.shown_tracks_range[0]
2633 self.update()
2635 def content_time_range(self):
2636 pile = self.get_pile()
2637 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2638 if tmin is None:
2639 tmin = initial_time_range[0]
2640 if tmax is None:
2641 tmax = initial_time_range[1]
2643 return tmin, tmax
2645 def content_deltat_range(self):
2646 pile = self.get_pile()
2648 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2650 if deltatmin is None:
2651 deltatmin = 0.001
2653 if deltatmax is None:
2654 deltatmax = 1000.0
2656 return deltatmin, deltatmax
2658 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2659 if tmax < tmin:
2660 tmin, tmax = tmax, tmin
2662 deltatmin = self.content_deltat_range()[0]
2663 dt = deltatmin * self.visible_length * 0.95
2665 if dt == 0.0:
2666 dt = 1.0
2668 if tight:
2669 if tmax != tmin:
2670 dtm = tmax - tmin
2671 tmin -= dtm*0.1
2672 tmax += dtm*0.1
2673 return tmin, tmax
2674 else:
2675 tcenter = (tmin + tmax) / 2.
2676 tmin = tcenter - 0.5*dt
2677 tmax = tcenter + 0.5*dt
2678 return tmin, tmax
2680 if tmax-tmin < dt:
2681 vmin, vmax = self.get_time_range()
2682 dt = min(vmax - vmin, dt)
2684 tcenter = (tmin+tmax)/2.
2685 etmin, etmax = tmin, tmax
2686 tmin = min(etmin, tcenter - 0.5*dt)
2687 tmax = max(etmax, tcenter + 0.5*dt)
2688 dtm = tmax-tmin
2689 if etmin == tmin:
2690 tmin -= dtm*0.1
2691 if etmax == tmax:
2692 tmax += dtm*0.1
2694 else:
2695 dtm = tmax-tmin
2696 tmin -= dtm*0.1
2697 tmax += dtm*0.1
2699 return tmin, tmax
2701 def go_to_selection(self, tight=False):
2702 markers = self.selected_markers()
2703 if markers:
2704 tmax, tmin = self.content_time_range()
2705 for marker in markers:
2706 tmin = min(tmin, marker.tmin)
2707 tmax = max(tmax, marker.tmax)
2709 else:
2710 if tight:
2711 vmin, vmax = self.get_time_range()
2712 tmin = tmax = (vmin + vmax) / 2.
2713 else:
2714 tmin, tmax = self.content_time_range()
2716 tmin, tmax = self.make_good_looking_time_range(
2717 tmin, tmax, tight=tight)
2719 self.interrupt_following()
2720 self.set_time_range(tmin, tmax)
2721 self.update()
2723 def go_to_time(self, t, tlen=None):
2724 tmax = t
2725 if tlen is not None:
2726 tmax = t+tlen
2727 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2728 self.interrupt_following()
2729 self.set_time_range(tmin, tmax)
2730 self.update()
2732 def go_to_event_by_name(self, name):
2733 for marker in self.markers:
2734 if isinstance(marker, EventMarker):
2735 event = marker.get_event()
2736 if event.name and event.name.lower() == name.lower():
2737 tmin, tmax = self.make_good_looking_time_range(
2738 event.time, event.time)
2740 self.interrupt_following()
2741 self.set_time_range(tmin, tmax)
2743 def printit(self):
2744 from .qt_compat import qprint
2745 printer = qprint.QPrinter()
2746 printer.setOrientation(qprint.QPrinter.Landscape)
2748 dialog = qprint.QPrintDialog(printer, self)
2749 dialog.setWindowTitle('Print')
2751 if dialog.exec_() != qw.QDialog.Accepted:
2752 return
2754 painter = qg.QPainter()
2755 painter.begin(printer)
2756 page = printer.pageRect()
2757 self.drawit(
2758 painter, printmode=False, w=page.width(), h=page.height())
2760 painter.end()
2762 def savesvg(self, fn=None):
2764 if not fn:
2765 fn, _ = qw.QFileDialog.getSaveFileName(
2766 self,
2767 'Save as SVG|PNG',
2768 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2769 'SVG|PNG (*.svg *.png)',
2770 options=qfiledialog_options)
2772 if fn == '':
2773 return
2775 fn = str(fn)
2777 if fn.lower().endswith('.svg'):
2778 try:
2779 w, h = 842, 595
2780 margin = 0.025
2781 m = max(w, h)*margin
2783 generator = qsvg.QSvgGenerator()
2784 generator.setFileName(fn)
2785 generator.setSize(qc.QSize(w, h))
2786 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2788 painter = qg.QPainter()
2789 painter.begin(generator)
2790 self.drawit(painter, printmode=False, w=w, h=h)
2791 painter.end()
2793 except Exception as e:
2794 self.fail('Failed to write SVG file: %s' % str(e))
2796 elif fn.lower().endswith('.png'):
2797 pixmap = self.grab()
2799 try:
2800 pixmap.save(fn)
2802 except Exception as e:
2803 self.fail('Failed to write PNG file: %s' % str(e))
2805 else:
2806 self.fail(
2807 'Unsupported file type: filename must end with ".svg" or '
2808 '".png".')
2810 def paintEvent(self, paint_ev):
2811 '''
2812 Called by QT whenever widget needs to be painted.
2813 '''
2814 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2815 # was called twice (by different threads?), causing segfaults.
2816 if self.in_paint_event:
2817 logger.warning('Blocking reentrant call to paintEvent().')
2818 return
2820 self.in_paint_event = True
2822 painter = qg.QPainter(self)
2824 if self.menuitem_antialias.isChecked():
2825 painter.setRenderHint(qg.QPainter.Antialiasing)
2827 self.drawit(painter)
2829 logger.debug(
2830 'Time spent drawing: '
2831 ' user:%.3f sys:%.3f children_user:%.3f'
2832 ' childred_sys:%.3f elapsed:%.3f' %
2833 (self.timer_draw - self.timer_cutout))
2835 logger.debug(
2836 'Time spent processing:'
2837 ' user:%.3f sys:%.3f children_user:%.3f'
2838 ' childred_sys:%.3f elapsed:%.3f' %
2839 self.timer_cutout.get())
2841 self.time_spent_painting = self.timer_draw.get()[-1]
2842 self.time_last_painted = time.time()
2843 self.in_paint_event = False
2845 def determine_box_styles(self):
2847 traces = list(self.pile.iter_traces())
2848 traces.sort(key=operator.attrgetter('full_id'))
2849 istyle = 0
2850 trace_styles = {}
2851 for itr, tr in enumerate(traces):
2852 if itr > 0:
2853 other = traces[itr-1]
2854 if not (
2855 other.nslc_id == tr.nslc_id
2856 and other.deltat == tr.deltat
2857 and abs(other.tmax - tr.tmin)
2858 < gap_lap_tolerance*tr.deltat):
2860 istyle += 1
2862 trace_styles[tr.full_id, tr.deltat] = istyle
2864 self.trace_styles = trace_styles
2866 def draw_trace_boxes(self, p, time_projection, track_projections):
2868 for v_projection in track_projections.values():
2869 v_projection.set_in_range(0., 1.)
2871 def selector(x):
2872 return x.overlaps(*time_projection.get_in_range())
2874 if self.trace_filter is not None:
2875 def tselector(x):
2876 return selector(x) and self.trace_filter(x)
2878 else:
2879 tselector = selector
2881 traces = list(self.pile.iter_traces(
2882 group_selector=selector, trace_selector=tselector))
2884 traces.sort(key=operator.attrgetter('full_id'))
2886 def drawbox(itrack, istyle, traces):
2887 v_projection = track_projections[itrack]
2888 dvmin = v_projection(0.)
2889 dvmax = v_projection(1.)
2890 dtmin = time_projection.clipped(traces[0].tmin, 0)
2891 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2893 style = box_styles[istyle % len(box_styles)]
2894 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2895 p.fillRect(rect, style.fill_brush)
2896 p.setPen(style.frame_pen)
2897 p.drawRect(rect)
2899 traces_by_style = {}
2900 for itr, tr in enumerate(traces):
2901 gt = self.gather(tr)
2902 if gt not in self.key_to_row:
2903 continue
2905 itrack = self.key_to_row[gt]
2906 if itrack not in track_projections:
2907 continue
2909 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2911 if len(traces) < 500:
2912 drawbox(itrack, istyle, [tr])
2913 else:
2914 if (itrack, istyle) not in traces_by_style:
2915 traces_by_style[itrack, istyle] = []
2916 traces_by_style[itrack, istyle].append(tr)
2918 for (itrack, istyle), traces in traces_by_style.items():
2919 drawbox(itrack, istyle, traces)
2921 def draw_visible_markers(
2922 self, p, vcenter_projection, primary_pen):
2924 try:
2925 markers = self.markers.with_key_in_limited(
2926 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2928 except pyrocko.pile.TooMany:
2929 tmin = self.markers[0].tmin
2930 tmax = self.markers[-1].tmax
2931 umin_view, umax_view = self.time_projection.get_out_range()
2932 umin = max(umin_view, self.time_projection(tmin))
2933 umax = min(umax_view, self.time_projection(tmax))
2934 v0, _ = vcenter_projection.get_out_range()
2935 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2937 p.save()
2939 pen = qg.QPen(primary_pen)
2940 pen.setWidth(2)
2941 pen.setStyle(qc.Qt.DotLine)
2942 # pat = [5., 3.]
2943 # pen.setDashPattern(pat)
2944 p.setPen(pen)
2946 if self.n_selected_markers == len(self.markers):
2947 s_selected = ' (all selected)'
2948 elif self.n_selected_markers > 0:
2949 s_selected = ' (%i selected)' % self.n_selected_markers
2950 else:
2951 s_selected = ''
2953 draw_label(
2954 p, umin+10., v0-10.,
2955 '%i Markers' % len(self.markers) + s_selected,
2956 label_bg, 'LB')
2958 line = qc.QLineF(umin, v0, umax, v0)
2959 p.drawLine(line)
2960 p.restore()
2962 return
2964 for marker in markers:
2965 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2966 and marker.kind in self.visible_marker_kinds:
2968 marker.draw(
2969 p, self.time_projection, vcenter_projection,
2970 with_label=True)
2972 def get_squirrel(self):
2973 try:
2974 return self.pile._squirrel
2975 except AttributeError:
2976 return None
2978 def draw_coverage(self, p, time_projection, track_projections):
2979 sq = self.get_squirrel()
2980 if sq is None:
2981 return
2983 def drawbox(itrack, tmin, tmax, style):
2984 v_projection = track_projections[itrack]
2985 dvmin = v_projection(0.)
2986 dvmax = v_projection(1.)
2987 dtmin = time_projection.clipped(tmin, 0)
2988 dtmax = time_projection.clipped(tmax, 1)
2990 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2991 p.fillRect(rect, style.fill_brush)
2992 p.setPen(style.frame_pen)
2993 p.drawRect(rect)
2995 pattern_list = []
2996 pattern_to_itrack = {}
2997 for key in self.track_keys:
2998 itrack = self.key_to_row[key]
2999 if itrack not in track_projections:
3000 continue
3002 pattern = self.track_patterns[itrack]
3003 pattern_to_itrack[tuple(pattern)] = itrack
3004 pattern_list.append(tuple(pattern))
3006 vmin, vmax = self.get_time_range()
3008 for kind in ['waveform', 'waveform_promise']:
3009 for coverage in sq.get_coverage(
3010 kind, vmin, vmax, pattern_list, limit=500):
3011 itrack = pattern_to_itrack[coverage.pattern.nslc]
3013 if coverage.changes is None:
3014 drawbox(
3015 itrack, coverage.tmin, coverage.tmax,
3016 box_styles_coverage[kind][0])
3017 else:
3018 t = None
3019 pcount = 0
3020 for tb, count in coverage.changes:
3021 if t is not None and tb > t:
3022 if pcount > 0:
3023 drawbox(
3024 itrack, t, tb,
3025 box_styles_coverage[kind][
3026 min(len(box_styles_coverage)-1,
3027 pcount)])
3029 t = tb
3030 pcount = count
3032 def drawit(self, p, printmode=False, w=None, h=None):
3033 '''
3034 This performs the actual drawing.
3035 '''
3037 self.timer_draw.start()
3038 show_boxes = self.menuitem_showboxes.isChecked()
3039 sq = self.get_squirrel()
3041 if self.gather is None:
3042 self.set_gathering()
3044 if self.pile_has_changed:
3046 if not self.sortingmode_change_delayed():
3047 self.sortingmode_change()
3049 if show_boxes and sq is None:
3050 self.determine_box_styles()
3052 self.pile_has_changed = False
3054 if h is None:
3055 h = float(self.height())
3056 if w is None:
3057 w = float(self.width())
3059 if printmode:
3060 primary_color = (0, 0, 0)
3061 else:
3062 primary_color = pyrocko.plot.colors.g_nat_colors['nat_gray']
3064 primary_pen = qg.QPen(qg.QColor(*primary_color))
3066 ax_h = float(self.ax_height)
3068 vbottom_ax_projection = Projection()
3069 vtop_ax_projection = Projection()
3070 vcenter_projection = Projection()
3072 self.time_projection.set_out_range(0., w)
3073 vbottom_ax_projection.set_out_range(h-ax_h, h)
3074 vtop_ax_projection.set_out_range(0., ax_h)
3075 vcenter_projection.set_out_range(ax_h, h-ax_h)
3076 vcenter_projection.set_in_range(0., 1.)
3077 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3079 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3080 track_projections = {}
3081 for i in range(*self.shown_tracks_range):
3082 proj = Projection()
3083 proj.set_out_range(
3084 self.track_to_screen(i+0.05),
3085 self.track_to_screen(i+1.-0.05))
3087 track_projections[i] = proj
3089 if self.tmin > self.tmax:
3090 return
3092 self.time_projection.set_in_range(self.tmin, self.tmax)
3093 vbottom_ax_projection.set_in_range(0, ax_h)
3095 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3097 yscaler = pyrocko.plot.AutoScaler()
3099 p.setPen(primary_pen)
3101 font = qg.QFont()
3102 font.setBold(True)
3104 axannotfont = qg.QFont()
3105 axannotfont.setBold(True)
3106 axannotfont.setPointSize(8)
3108 processed_traces = self.prepare_cutout2(
3109 self.tmin, self.tmax,
3110 trace_selector=self.trace_selector,
3111 degap=self.menuitem_degap.isChecked(),
3112 demean=self.menuitem_demean.isChecked())
3114 if not printmode and show_boxes:
3115 if (self.view_mode is ViewMode.Wiggle) \
3116 or (self.view_mode is ViewMode.Waterfall
3117 and not processed_traces):
3119 if sq is None:
3120 self.draw_trace_boxes(
3121 p, self.time_projection, track_projections)
3123 else:
3124 self.draw_coverage(
3125 p, self.time_projection, track_projections)
3127 p.setFont(font)
3128 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3130 color_lookup = dict(
3131 [(k, i) for (i, k) in enumerate(self.color_keys)])
3133 self.track_to_nslc_ids = {}
3134 nticks = 0
3135 annot_labels = []
3137 if self.view_mode is ViewMode.Waterfall and processed_traces:
3138 waterfall = self.waterfall
3139 waterfall.set_time_range(self.tmin, self.tmax)
3140 waterfall.set_traces(processed_traces)
3141 waterfall.set_cmap(self.waterfall_cmap)
3142 waterfall.set_integrate(self.waterfall_integrate)
3143 waterfall.set_clip(
3144 self.waterfall_clip_min, self.waterfall_clip_max)
3145 waterfall.show_absolute_values(
3146 self.waterfall_show_absolute)
3148 rect = qc.QRectF(
3149 0, self.ax_height,
3150 self.width(), self.height() - self.ax_height*2
3151 )
3152 waterfall.draw_waterfall(p, rect=rect)
3154 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3155 show_scales = self.menuitem_showscalerange.isChecked() \
3156 or self.menuitem_showscaleaxis.isChecked()
3158 fm = qg.QFontMetrics(axannotfont, p.device())
3159 trackheight = self.track_to_screen(1.-0.05) \
3160 - self.track_to_screen(0.05)
3162 nlinesavail = trackheight/float(fm.lineSpacing())
3164 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3165 if self.menuitem_showscaleaxis.isChecked() \
3166 else 15
3168 yscaler = pyrocko.plot.AutoScaler(
3169 no_exp_interval=(-3, 2), approx_ticks=nticks,
3170 snap=show_scales
3171 and not self.menuitem_showscaleaxis.isChecked())
3173 data_ranges = pyrocko.trace.minmax(
3174 processed_traces,
3175 key=self.scaling_key,
3176 mode=self.scaling_base[0],
3177 outer_mode=self.scaling_base[1])
3179 if not self.menuitem_fixscalerange.isChecked():
3180 self.old_data_ranges = data_ranges
3181 else:
3182 data_ranges.update(self.old_data_ranges)
3184 self.apply_scaling_hooks(data_ranges)
3186 trace_to_itrack = {}
3187 track_scaling_keys = {}
3188 track_scaling_colors = {}
3189 for trace in processed_traces:
3190 gt = self.gather(trace)
3191 if gt not in self.key_to_row:
3192 continue
3194 itrack = self.key_to_row[gt]
3195 if itrack not in track_projections:
3196 continue
3198 trace_to_itrack[trace] = itrack
3200 if itrack not in self.track_to_nslc_ids:
3201 self.track_to_nslc_ids[itrack] = set()
3203 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3205 if itrack not in track_scaling_keys:
3206 track_scaling_keys[itrack] = set()
3208 scaling_key = self.scaling_key(trace)
3209 track_scaling_keys[itrack].add(scaling_key)
3211 color = pyrocko.plot.color(
3212 color_lookup[self.color_gather(trace)])
3214 k = itrack, scaling_key
3215 if k not in track_scaling_colors \
3216 and self.menuitem_colortraces.isChecked():
3217 track_scaling_colors[k] = color
3218 else:
3219 track_scaling_colors[k] = primary_color
3221 # y axes, zero lines
3222 trace_projections = {}
3223 for itrack in list(track_projections.keys()):
3224 if itrack not in track_scaling_keys:
3225 continue
3226 uoff = 0
3227 for scaling_key in track_scaling_keys[itrack]:
3228 data_range = data_ranges[scaling_key]
3229 dymin, dymax = data_range
3230 ymin, ymax, yinc = yscaler.make_scale(
3231 (dymin/self.gain, dymax/self.gain))
3232 iexp = yscaler.make_exp(yinc)
3233 factor = 10**iexp
3234 trace_projection = track_projections[itrack].copy()
3235 trace_projection.set_in_range(ymax, ymin)
3236 trace_projections[itrack, scaling_key] = \
3237 trace_projection
3238 umin, umax = self.time_projection.get_out_range()
3239 vmin, vmax = trace_projection.get_out_range()
3240 umax_zeroline = umax
3241 uoffnext = uoff
3243 if show_scales:
3244 pen = qg.QPen(primary_pen)
3245 k = itrack, scaling_key
3246 if k in track_scaling_colors:
3247 c = qg.QColor(*track_scaling_colors[
3248 itrack, scaling_key])
3250 pen.setColor(c)
3252 p.setPen(pen)
3253 if nlinesavail > 3:
3254 if self.menuitem_showscaleaxis.isChecked():
3255 ymin_annot = math.ceil(ymin/yinc)*yinc
3256 ny_annot = int(
3257 math.floor(ymax/yinc)
3258 - math.ceil(ymin/yinc)) + 1
3260 for iy_annot in range(ny_annot):
3261 y = ymin_annot + iy_annot*yinc
3262 v = trace_projection(y)
3263 line = qc.QLineF(
3264 umax-10-uoff, v, umax-uoff, v)
3266 p.drawLine(line)
3267 if iy_annot == ny_annot - 1 \
3268 and iexp != 0:
3269 sexp = ' × ' \
3270 '10<sup>%i</sup>' % iexp
3271 else:
3272 sexp = ''
3274 snum = num_to_html(y/factor)
3275 lab = Label(
3276 p,
3277 umax-20-uoff,
3278 v, '%s%s' % (snum, sexp),
3279 label_bg=None,
3280 anchor='MR',
3281 font=axannotfont,
3282 color=c)
3284 uoffnext = max(
3285 lab.rect.width()+30., uoffnext)
3287 annot_labels.append(lab)
3288 if y == 0.:
3289 umax_zeroline = \
3290 umax - 20 \
3291 - lab.rect.width() - 10 \
3292 - uoff
3293 else:
3294 if not show_boxes:
3295 qpoints = make_QPolygonF(
3296 [umax-20-uoff,
3297 umax-10-uoff,
3298 umax-10-uoff,
3299 umax-20-uoff],
3300 [vmax, vmax, vmin, vmin])
3301 p.drawPolyline(qpoints)
3303 snum = num_to_html(ymin)
3304 labmin = Label(
3305 p, umax-15-uoff, vmax, snum,
3306 label_bg=None,
3307 anchor='BR',
3308 font=axannotfont,
3309 color=c)
3311 annot_labels.append(labmin)
3312 snum = num_to_html(ymax)
3313 labmax = Label(
3314 p, umax-15-uoff, vmin, snum,
3315 label_bg=None,
3316 anchor='TR',
3317 font=axannotfont,
3318 color=c)
3320 annot_labels.append(labmax)
3322 for lab in (labmin, labmax):
3323 uoffnext = max(
3324 lab.rect.width()+10., uoffnext)
3326 if self.menuitem_showzeroline.isChecked():
3327 v = trace_projection(0.)
3328 if vmin <= v <= vmax:
3329 line = qc.QLineF(umin, v, umax_zeroline, v)
3330 p.drawLine(line)
3332 uoff = uoffnext
3334 p.setFont(font)
3335 p.setPen(primary_pen)
3336 for trace in processed_traces:
3337 if self.view_mode is not ViewMode.Wiggle:
3338 break
3340 if trace not in trace_to_itrack:
3341 continue
3343 itrack = trace_to_itrack[trace]
3344 scaling_key = self.scaling_key(trace)
3345 trace_projection = trace_projections[
3346 itrack, scaling_key]
3348 vdata = trace_projection(trace.get_ydata())
3350 udata_min = float(self.time_projection(trace.tmin))
3351 udata_max = float(self.time_projection(
3352 trace.tmin+trace.deltat*(vdata.size-1)))
3353 udata = num.linspace(udata_min, udata_max, vdata.size)
3355 qpoints = make_QPolygonF(udata, vdata)
3357 umin, umax = self.time_projection.get_out_range()
3358 vmin, vmax = trace_projection.get_out_range()
3360 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3362 if self.menuitem_cliptraces.isChecked():
3363 p.setClipRect(trackrect)
3365 if self.menuitem_colortraces.isChecked():
3366 color = pyrocko.plot.color(
3367 color_lookup[self.color_gather(trace)])
3368 pen = qg.QPen(qg.QColor(*color), 1)
3369 p.setPen(pen)
3371 p.drawPolyline(qpoints)
3373 if self.floating_marker:
3374 self.floating_marker.draw_trace(
3375 self, p, trace,
3376 self.time_projection, trace_projection, 1.0)
3378 for marker in self.markers.with_key_in(
3379 self.tmin - self.markers_deltat_max,
3380 self.tmax):
3382 if marker.tmin < self.tmax \
3383 and self.tmin < marker.tmax \
3384 and marker.kind \
3385 in self.visible_marker_kinds:
3386 marker.draw_trace(
3387 self, p, trace, self.time_projection,
3388 trace_projection, 1.0)
3390 p.setPen(primary_pen)
3392 if self.menuitem_cliptraces.isChecked():
3393 p.setClipRect(0, 0, int(w), int(h))
3395 if self.floating_marker:
3396 self.floating_marker.draw(
3397 p, self.time_projection, vcenter_projection)
3399 self.draw_visible_markers(
3400 p, vcenter_projection, primary_pen)
3402 p.setPen(primary_pen)
3403 while font.pointSize() > 2:
3404 fm = qg.QFontMetrics(font, p.device())
3405 trackheight = self.track_to_screen(1.-0.05) \
3406 - self.track_to_screen(0.05)
3407 nlinesavail = trackheight/float(fm.lineSpacing())
3408 if nlinesavail > 1:
3409 break
3411 font.setPointSize(font.pointSize()-1)
3413 p.setFont(font)
3414 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3416 for key in self.track_keys:
3417 itrack = self.key_to_row[key]
3418 if itrack in track_projections:
3419 plabel = ' '.join(
3420 [str(x) for x in key if x is not None])
3421 lx = 10
3422 ly = self.track_to_screen(itrack+0.5)
3424 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3425 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3426 continue
3428 contains_cursor = \
3429 self.track_to_screen(itrack) \
3430 < mouse_pos.y() \
3431 < self.track_to_screen(itrack+1)
3433 if not contains_cursor:
3434 continue
3436 font_large = p.font()
3437 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3438 p.setFont(font_large)
3439 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3440 p.setFont(font)
3442 for lab in annot_labels:
3443 lab.draw()
3445 self.timer_draw.stop()
3447 def see_data_params(self):
3449 min_deltat = self.content_deltat_range()[0]
3451 # determine padding and downampling requirements
3452 if self.lowpass is not None:
3453 deltat_target = 1./self.lowpass * 0.25
3454 ndecimate = min(
3455 50,
3456 max(1, int(round(deltat_target / min_deltat))))
3457 tpad = 1./self.lowpass * 2.
3458 else:
3459 ndecimate = 1
3460 tpad = min_deltat*5.
3462 if self.highpass is not None:
3463 tpad = max(1./self.highpass * 2., tpad)
3465 nsee_points_per_trace = 5000*10
3466 tsee = ndecimate*nsee_points_per_trace*min_deltat
3468 return ndecimate, tpad, tsee
3470 def clean_update(self):
3471 self.cached_processed_traces = None
3472 self.update()
3474 def get_adequate_tpad(self):
3475 tpad = 0.
3476 for f in [self.highpass, self.lowpass]:
3477 if f is not None:
3478 tpad = max(tpad, 1.0/f)
3480 for snuffling in self.snufflings:
3481 if snuffling._post_process_hook_enabled \
3482 or snuffling._pre_process_hook_enabled:
3484 tpad = max(tpad, snuffling.get_tpad())
3486 return tpad
3488 def prepare_cutout2(
3489 self, tmin, tmax, trace_selector=None, degap=True,
3490 demean=True, nmax=6000):
3492 if self.pile.is_empty():
3493 return []
3495 nmax = self.visible_length
3497 self.timer_cutout.start()
3499 tsee = tmax-tmin
3500 min_deltat_wo_decimate = tsee/nmax
3501 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3503 min_deltat_allow = min_deltat_wo_decimate
3504 if self.lowpass is not None:
3505 target_deltat_lp = 0.25/self.lowpass
3506 if target_deltat_lp > min_deltat_wo_decimate:
3507 min_deltat_allow = min_deltat_w_decimate
3509 min_deltat_allow = math.exp(
3510 int(math.floor(math.log(min_deltat_allow))))
3512 tmin_ = tmin
3513 tmax_ = tmax
3515 # fetch more than needed?
3516 if self.menuitem_liberal_fetch.isChecked():
3517 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3518 tmin = math.floor(tmin/tlen) * tlen
3519 tmax = math.ceil(tmax/tlen) * tlen
3521 fft_filtering = self.menuitem_fft_filtering.isChecked()
3522 lphp = self.menuitem_lphp.isChecked()
3523 ads = self.menuitem_allowdownsampling.isChecked()
3525 tpad = self.get_adequate_tpad()
3526 tpad = max(tpad, tsee)
3528 # state vector to decide if cached traces can be used
3529 vec = (
3530 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3531 self.highpass, fft_filtering, lphp,
3532 min_deltat_allow, self.rotate, self.shown_tracks_range,
3533 ads, self.pile.get_update_count())
3535 if (self.cached_vec
3536 and self.cached_vec[0] <= vec[0]
3537 and vec[1] <= self.cached_vec[1]
3538 and vec[2:] == self.cached_vec[2:]
3539 and not (self.reloaded or self.menuitem_watch.isChecked())
3540 and self.cached_processed_traces is not None):
3542 logger.debug('Using cached traces')
3543 processed_traces = self.cached_processed_traces
3545 else:
3546 processed_traces = []
3547 if self.pile.deltatmax >= min_deltat_allow:
3549 if isinstance(self.pile, pyrocko.pile.Pile):
3550 def group_selector(gr):
3551 return gr.deltatmax >= min_deltat_allow
3553 kwargs = dict(group_selector=group_selector)
3554 else:
3555 kwargs = {}
3557 if trace_selector is not None:
3558 def trace_selectorx(tr):
3559 return tr.deltat >= min_deltat_allow \
3560 and trace_selector(tr)
3561 else:
3562 def trace_selectorx(tr):
3563 return tr.deltat >= min_deltat_allow
3565 for traces in self.pile.chopper(
3566 tmin=tmin, tmax=tmax, tpad=tpad,
3567 want_incomplete=True,
3568 degap=degap,
3569 maxgap=gap_lap_tolerance,
3570 maxlap=gap_lap_tolerance,
3571 keep_current_files_open=True,
3572 trace_selector=trace_selectorx,
3573 accessor_id=id(self),
3574 snap=(math.floor, math.ceil),
3575 include_last=True, **kwargs):
3577 if demean:
3578 for tr in traces:
3579 if (tr.meta and tr.meta.get('tabu', False)):
3580 continue
3581 y = tr.get_ydata()
3582 tr.set_ydata(y - num.mean(y))
3584 traces = self.pre_process_hooks(traces)
3586 for trace in traces:
3588 if not (trace.meta
3589 and trace.meta.get('tabu', False)):
3591 if fft_filtering:
3592 but = pyrocko.response.ButterworthResponse
3593 multres = pyrocko.response.MultiplyResponse
3594 if self.lowpass is not None \
3595 or self.highpass is not None:
3597 it = num.arange(
3598 trace.data_len(), dtype=float)
3599 detr_data, m, b = detrend(
3600 it, trace.get_ydata())
3602 trace.set_ydata(detr_data)
3604 freqs, fdata = trace.spectrum(
3605 pad_to_pow2=True, tfade=None)
3607 nfreqs = fdata.size
3609 key = (trace.deltat, nfreqs)
3611 if key not in self.tf_cache:
3612 resps = []
3613 if self.lowpass is not None:
3614 resps.append(but(
3615 order=4,
3616 corner=self.lowpass,
3617 type='low'))
3619 if self.highpass is not None:
3620 resps.append(but(
3621 order=4,
3622 corner=self.highpass,
3623 type='high'))
3625 resp = multres(resps)
3626 self.tf_cache[key] = \
3627 resp.evaluate(freqs)
3629 filtered_data = num.fft.irfft(
3630 fdata*self.tf_cache[key]
3631 )[:trace.data_len()]
3633 retrended_data = retrend(
3634 it, filtered_data, m, b)
3636 trace.set_ydata(retrended_data)
3638 else:
3640 if ads and self.lowpass is not None:
3641 while trace.deltat \
3642 < min_deltat_wo_decimate:
3644 trace.downsample(2, demean=False)
3646 fmax = 0.5/trace.deltat
3647 if not lphp and (
3648 self.lowpass is not None
3649 and self.highpass is not None
3650 and self.lowpass < fmax
3651 and self.highpass < fmax
3652 and self.highpass < self.lowpass):
3654 trace.bandpass(
3655 2, self.highpass, self.lowpass)
3656 else:
3657 if self.lowpass is not None:
3658 if self.lowpass < 0.5/trace.deltat:
3659 trace.lowpass(
3660 4, self.lowpass,
3661 demean=False)
3663 if self.highpass is not None:
3664 if self.lowpass is None \
3665 or self.highpass \
3666 < self.lowpass:
3668 if self.highpass < \
3669 0.5/trace.deltat:
3670 trace.highpass(
3671 4, self.highpass,
3672 demean=False)
3674 processed_traces.append(trace)
3676 if self.rotate != 0.0:
3677 phi = self.rotate/180.*math.pi
3678 cphi = math.cos(phi)
3679 sphi = math.sin(phi)
3680 for a in processed_traces:
3681 for b in processed_traces:
3682 if (a.network == b.network
3683 and a.station == b.station
3684 and a.location == b.location
3685 and ((a.channel.lower().endswith('n')
3686 and b.channel.lower().endswith('e'))
3687 or (a.channel.endswith('1')
3688 and b.channel.endswith('2')))
3689 and abs(a.deltat-b.deltat) < a.deltat*0.001
3690 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3691 len(a.get_ydata()) == len(b.get_ydata())):
3693 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3694 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3695 a.set_ydata(aydata)
3696 b.set_ydata(bydata)
3698 processed_traces = self.post_process_hooks(processed_traces)
3700 self.cached_processed_traces = processed_traces
3701 self.cached_vec = vec
3703 chopped_traces = []
3704 for trace in processed_traces:
3705 chop_tmin = tmin_ - trace.deltat*4
3706 chop_tmax = tmax_ + trace.deltat*4
3708 try:
3709 ctrace = trace.chop(
3710 chop_tmin, chop_tmax,
3711 inplace=False)
3713 except pyrocko.trace.NoData:
3714 continue
3716 if ctrace.data_len() < 2:
3717 continue
3719 chopped_traces.append(ctrace)
3721 self.timer_cutout.stop()
3722 return chopped_traces
3724 def pre_process_hooks(self, traces):
3725 for snuffling in self.snufflings:
3726 if snuffling._pre_process_hook_enabled:
3727 traces = snuffling.pre_process_hook(traces)
3729 return traces
3731 def post_process_hooks(self, traces):
3732 for snuffling in self.snufflings:
3733 if snuffling._post_process_hook_enabled:
3734 traces = snuffling.post_process_hook(traces)
3736 return traces
3738 def visible_length_change(self, ignore=None):
3739 for menuitem, vlen in self.menuitems_visible_length:
3740 if menuitem.isChecked():
3741 self.visible_length = vlen
3743 def scaling_base_change(self, ignore=None):
3744 for menuitem, scaling_base in self.menuitems_scaling_base:
3745 if menuitem.isChecked():
3746 self.scaling_base = scaling_base
3748 def scalingmode_change(self, ignore=None):
3749 for menuitem, scaling_key in self.menuitems_scaling:
3750 if menuitem.isChecked():
3751 self.scaling_key = scaling_key
3752 self.update()
3754 def apply_scaling_hooks(self, data_ranges):
3755 for k in sorted(self.scaling_hooks.keys()):
3756 hook = self.scaling_hooks[k]
3757 hook(data_ranges)
3759 def viewmode_change(self, ignore=True):
3760 for item, mode in self.menuitems_viewmode:
3761 if item.isChecked():
3762 self.view_mode = mode
3763 break
3764 else:
3765 raise AttributeError('unknown view mode')
3767 items_waterfall_disabled = (
3768 self.menuitem_showscaleaxis,
3769 self.menuitem_showscalerange,
3770 self.menuitem_showzeroline,
3771 self.menuitem_colortraces,
3772 self.menuitem_cliptraces,
3773 *(itm[0] for itm in self.menuitems_visible_length)
3774 )
3776 if self.view_mode is ViewMode.Waterfall:
3777 self.parent().show_colorbar_ctrl(True)
3778 self.parent().show_gain_ctrl(False)
3780 for item in items_waterfall_disabled:
3781 item.setDisabled(True)
3783 self.visible_length = 180.
3784 else:
3785 self.parent().show_colorbar_ctrl(False)
3786 self.parent().show_gain_ctrl(True)
3788 for item in items_waterfall_disabled:
3789 item.setDisabled(False)
3791 self.visible_length_change()
3792 self.update()
3794 def set_scaling_hook(self, k, hook):
3795 self.scaling_hooks[k] = hook
3797 def remove_scaling_hook(self, k):
3798 del self.scaling_hooks[k]
3800 def remove_scaling_hooks(self):
3801 self.scaling_hooks = {}
3803 def s_sortingmode_change(self, ignore=None):
3804 for menuitem, valfunc in self.menuitems_ssorting:
3805 if menuitem.isChecked():
3806 self._ssort = valfunc
3808 self.sortingmode_change()
3810 def sortingmode_change(self, ignore=None):
3811 for menuitem, (gather, color) in self.menuitems_sorting:
3812 if menuitem.isChecked():
3813 self.set_gathering(gather, color)
3815 self.sortingmode_change_time = time.time()
3817 def lowpass_change(self, value, ignore=None):
3818 self.lowpass = value
3819 self.passband_check()
3820 self.tf_cache = {}
3821 self.update()
3823 def highpass_change(self, value, ignore=None):
3824 self.highpass = value
3825 self.passband_check()
3826 self.tf_cache = {}
3827 self.update()
3829 def passband_check(self):
3830 if self.highpass and self.lowpass \
3831 and self.highpass >= self.lowpass:
3833 self.message = 'Corner frequency of highpass larger than ' \
3834 'corner frequency of lowpass! I will now ' \
3835 'deactivate the highpass.'
3837 self.update_status()
3838 else:
3839 oldmess = self.message
3840 self.message = None
3841 if oldmess is not None:
3842 self.update_status()
3844 def gain_change(self, value, ignore):
3845 self.gain = value
3846 self.update()
3848 def rot_change(self, value, ignore):
3849 self.rotate = value
3850 self.update()
3852 def waterfall_cmap_change(self, cmap):
3853 self.waterfall_cmap = cmap
3854 self.update()
3856 def waterfall_clip_change(self, clip_min, clip_max):
3857 self.waterfall_clip_min = clip_min
3858 self.waterfall_clip_max = clip_max
3859 self.update()
3861 def waterfall_show_absolute_change(self, toggle):
3862 self.waterfall_show_absolute = toggle
3863 self.update()
3865 def waterfall_set_integrate(self, toggle):
3866 self.waterfall_integrate = toggle
3867 self.update()
3869 def set_selected_markers(self, markers):
3870 '''
3871 Set a list of markers selected
3873 :param markers: list of markers
3874 '''
3875 self.deselect_all()
3876 for m in markers:
3877 m.selected = True
3879 self.update()
3881 def deselect_all(self):
3882 for marker in self.markers:
3883 marker.selected = False
3885 def animate_picking(self):
3886 point = self.mapFromGlobal(qg.QCursor.pos())
3887 self.update_picking(point.x(), point.y(), doshift=True)
3889 def get_nslc_ids_for_track(self, ftrack):
3890 itrack = int(ftrack)
3891 return self.track_to_nslc_ids.get(itrack, [])
3893 def stop_picking(self, x, y, abort=False):
3894 if self.picking:
3895 self.update_picking(x, y, doshift=False)
3896 self.picking = None
3897 self.picking_down = None
3898 self.picking_timer.stop()
3899 self.picking_timer = None
3900 if not abort:
3901 self.add_marker(self.floating_marker)
3902 self.floating_marker.selected = True
3903 self.emit_selected_markers()
3905 self.floating_marker = None
3907 def start_picking(self, ignore):
3909 if not self.picking:
3910 self.deselect_all()
3911 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3912 point = self.mapFromGlobal(qg.QCursor.pos())
3914 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3915 self.picking.setGeometry(
3916 gpoint.x(), gpoint.y(), 1, self.height())
3917 t = self.time_projection.rev(point.x())
3919 ftrack = self.track_to_screen.rev(point.y())
3920 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3921 self.floating_marker = Marker(nslc_ids, t, t)
3922 self.floating_marker.selected = True
3924 self.picking_timer = qc.QTimer()
3925 self.picking_timer.timeout.connect(
3926 self.animate_picking)
3928 self.picking_timer.setInterval(50)
3929 self.picking_timer.start()
3931 def update_picking(self, x, y, doshift=False):
3932 if self.picking:
3933 mouset = self.time_projection.rev(x)
3934 dt = 0.0
3935 if mouset < self.tmin or mouset > self.tmax:
3936 if mouset < self.tmin:
3937 dt = -(self.tmin - mouset)
3938 else:
3939 dt = mouset - self.tmax
3940 ddt = self.tmax-self.tmin
3941 dt = max(dt, -ddt/10.)
3942 dt = min(dt, ddt/10.)
3944 x0 = x
3945 if self.picking_down is not None:
3946 x0 = self.time_projection(self.picking_down[0])
3948 w = abs(x-x0)
3949 x0 = min(x0, x)
3951 tmin, tmax = (
3952 self.time_projection.rev(x0),
3953 self.time_projection.rev(x0+w))
3955 tmin, tmax = (
3956 max(working_system_time_range[0], tmin),
3957 min(working_system_time_range[1], tmax))
3959 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3961 self.picking.setGeometry(
3962 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3964 ftrack = self.track_to_screen.rev(y)
3965 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3966 self.floating_marker.set(nslc_ids, tmin, tmax)
3968 if dt != 0.0 and doshift:
3969 self.interrupt_following()
3970 self.set_time_range(self.tmin+dt, self.tmax+dt)
3972 self.update()
3974 def update_status(self):
3976 if self.message is None:
3977 point = self.mapFromGlobal(qg.QCursor.pos())
3979 mouse_t = self.time_projection.rev(point.x())
3980 if not is_working_time(mouse_t):
3981 return
3983 if self.floating_marker:
3984 tmi, tma = (
3985 self.floating_marker.tmin,
3986 self.floating_marker.tmax)
3988 tt, ms = gmtime_x(tmi)
3990 if tmi == tma:
3991 message = mystrftime(
3992 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
3993 tt=tt, milliseconds=ms)
3994 else:
3995 srange = '%g s' % (tma-tmi)
3996 message = mystrftime(
3997 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
3998 tt=tt, milliseconds=ms)
3999 else:
4000 tt, ms = gmtime_x(mouse_t)
4002 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4003 else:
4004 message = self.message
4006 sb = self.window().statusBar()
4007 sb.clearMessage()
4008 sb.showMessage(message)
4010 def set_sortingmode_change_delay_time(self, dt):
4011 self.sortingmode_change_delay_time = dt
4013 def sortingmode_change_delayed(self):
4014 now = time.time()
4015 return (
4016 self.sortingmode_change_delay_time is not None
4017 and now - self.sortingmode_change_time
4018 < self.sortingmode_change_delay_time)
4020 def set_visible_marker_kinds(self, kinds):
4021 self.deselect_all()
4022 self.visible_marker_kinds = tuple(kinds)
4023 self.emit_selected_markers()
4025 def following(self):
4026 return self.follow_timer is not None \
4027 and not self.following_interrupted()
4029 def interrupt_following(self):
4030 self.interactive_range_change_time = time.time()
4032 def following_interrupted(self, now=None):
4033 if now is None:
4034 now = time.time()
4035 return now - self.interactive_range_change_time \
4036 < self.interactive_range_change_delay_time
4038 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4039 if tmax_start is None:
4040 tmax_start = time.time()
4041 self.show_all = False
4042 self.follow_time = tlen
4043 self.follow_timer = qc.QTimer(self)
4044 self.follow_timer.timeout.connect(
4045 self.follow_update)
4046 self.follow_timer.setInterval(interval)
4047 self.follow_timer.start()
4048 self.follow_started = time.time()
4049 self.follow_lapse = lapse
4050 self.follow_tshift = self.follow_started - tmax_start
4051 self.interactive_range_change_time = 0.0
4053 def unfollow(self):
4054 if self.follow_timer is not None:
4055 self.follow_timer.stop()
4056 self.follow_timer = None
4057 self.interactive_range_change_time = 0.0
4059 def follow_update(self):
4060 rnow = time.time()
4061 if self.follow_lapse is None:
4062 now = rnow
4063 else:
4064 now = self.follow_started + (rnow - self.follow_started) \
4065 * self.follow_lapse
4067 if self.following_interrupted(rnow):
4068 return
4069 self.set_time_range(
4070 now-self.follow_time-self.follow_tshift,
4071 now-self.follow_tshift)
4073 self.update()
4075 def myclose(self, return_tag=''):
4076 self.return_tag = return_tag
4077 self.window().close()
4079 def cleanup(self):
4080 self.about_to_close.emit()
4081 self.timer.stop()
4082 if self.follow_timer is not None:
4083 self.follow_timer.stop()
4085 for snuffling in list(self.snufflings):
4086 self.remove_snuffling(snuffling)
4088 def set_error_message(self, key, value):
4089 if value is None:
4090 if key in self.error_messages:
4091 del self.error_messages[key]
4092 else:
4093 self.error_messages[key] = value
4095 def inputline_changed(self, text):
4096 pass
4098 def inputline_finished(self, text):
4099 line = str(text)
4101 toks = line.split()
4102 clearit, hideit, error = False, True, None
4103 if len(toks) >= 1:
4104 command = toks[0].lower()
4106 try:
4107 quick_filter_commands = {
4108 'n': '%s.*.*.*',
4109 's': '*.%s.*.*',
4110 'l': '*.*.%s.*',
4111 'c': '*.*.*.%s'}
4113 if command in quick_filter_commands:
4114 if len(toks) >= 2:
4115 patterns = [
4116 quick_filter_commands[toks[0]] % pat
4117 for pat in toks[1:]]
4118 self.set_quick_filter_patterns(patterns, line)
4119 else:
4120 self.set_quick_filter_patterns(None)
4122 self.update()
4124 elif command in ('hide', 'unhide'):
4125 if len(toks) >= 2:
4126 patterns = []
4127 if len(toks) == 2:
4128 patterns = [toks[1]]
4129 elif len(toks) >= 3:
4130 x = {
4131 'n': '%s.*.*.*',
4132 's': '*.%s.*.*',
4133 'l': '*.*.%s.*',
4134 'c': '*.*.*.%s'}
4136 if toks[1] in x:
4137 patterns.extend(
4138 x[toks[1]] % tok for tok in toks[2:])
4140 for pattern in patterns:
4141 if command == 'hide':
4142 self.add_blacklist_pattern(pattern)
4143 else:
4144 self.remove_blacklist_pattern(pattern)
4146 elif command == 'unhide' and len(toks) == 1:
4147 self.clear_blacklist()
4149 clearit = True
4151 self.update()
4153 elif command == 'markers':
4154 if len(toks) == 2:
4155 if toks[1] == 'all':
4156 kinds = self.all_marker_kinds
4157 else:
4158 kinds = []
4159 for x in toks[1]:
4160 try:
4161 kinds.append(int(x))
4162 except Exception:
4163 pass
4165 self.set_visible_marker_kinds(kinds)
4167 elif len(toks) == 1:
4168 self.set_visible_marker_kinds(())
4170 self.update()
4172 elif command == 'scaling':
4173 if len(toks) == 2:
4174 hideit = False
4175 error = 'wrong number of arguments'
4177 if len(toks) >= 3:
4178 vmin, vmax = [
4179 pyrocko.model.float_or_none(x)
4180 for x in toks[-2:]]
4182 def upd(d, k, vmin, vmax):
4183 if k in d:
4184 if vmin is not None:
4185 d[k] = vmin, d[k][1]
4186 if vmax is not None:
4187 d[k] = d[k][0], vmax
4189 if len(toks) == 1:
4190 self.remove_scaling_hooks()
4192 elif len(toks) == 3:
4193 def hook(data_ranges):
4194 for k in data_ranges:
4195 upd(data_ranges, k, vmin, vmax)
4197 self.set_scaling_hook('_', hook)
4199 elif len(toks) == 4:
4200 pattern = toks[1]
4202 def hook(data_ranges):
4203 for k in pyrocko.util.match_nslcs(
4204 pattern, list(data_ranges.keys())):
4206 upd(data_ranges, k, vmin, vmax)
4208 self.set_scaling_hook(pattern, hook)
4210 elif command == 'goto':
4211 toks2 = line.split(None, 1)
4212 if len(toks2) == 2:
4213 arg = toks2[1]
4214 m = re.match(
4215 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4216 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4217 if m:
4218 tlen = None
4219 if not m.group(1):
4220 tlen = 12*32*24*60*60
4221 elif not m.group(2):
4222 tlen = 32*24*60*60
4223 elif not m.group(3):
4224 tlen = 24*60*60
4225 elif not m.group(4):
4226 tlen = 60*60
4227 elif not m.group(5):
4228 tlen = 60
4230 supl = '1970-01-01 00:00:00'
4231 if len(supl) > len(arg):
4232 arg = arg + supl[-(len(supl)-len(arg)):]
4233 t = pyrocko.util.str_to_time(arg)
4234 self.go_to_time(t, tlen=tlen)
4236 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4237 supl = '00:00:00'
4238 if len(supl) > len(arg):
4239 arg = arg + supl[-(len(supl)-len(arg)):]
4240 tmin, tmax = self.get_time_range()
4241 sdate = pyrocko.util.time_to_str(
4242 tmin/2.+tmax/2., format='%Y-%m-%d')
4243 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4244 self.go_to_time(t)
4246 elif arg == 'today':
4247 self.go_to_time(
4248 day_start(
4249 time.time()), tlen=24*60*60)
4251 elif arg == 'yesterday':
4252 self.go_to_time(
4253 day_start(
4254 time.time()-24*60*60), tlen=24*60*60)
4256 else:
4257 self.go_to_event_by_name(arg)
4259 else:
4260 raise PileViewerMainException(
4261 'No such command: %s' % command)
4263 except PileViewerMainException as e:
4264 error = str(e)
4265 hideit = False
4267 return clearit, hideit, error
4269 return PileViewerMain
4272PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4273GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4276class LineEditWithAbort(qw.QLineEdit):
4278 aborted = qc.pyqtSignal()
4279 history_down = qc.pyqtSignal()
4280 history_up = qc.pyqtSignal()
4282 def keyPressEvent(self, key_event):
4283 if key_event.key() == qc.Qt.Key_Escape:
4284 self.aborted.emit()
4285 elif key_event.key() == qc.Qt.Key_Down:
4286 self.history_down.emit()
4287 elif key_event.key() == qc.Qt.Key_Up:
4288 self.history_up.emit()
4289 else:
4290 return qw.QLineEdit.keyPressEvent(self, key_event)
4293class PileViewer(qw.QFrame):
4294 '''
4295 PileViewerMain + Controls + Inputline
4296 '''
4298 def __init__(
4299 self, pile,
4300 ntracks_shown_max=20,
4301 marker_editor_sortable=True,
4302 use_opengl=None,
4303 panel_parent=None,
4304 *args):
4306 qw.QFrame.__init__(self, *args)
4308 layout = qw.QGridLayout()
4309 layout.setContentsMargins(0, 0, 0, 0)
4310 layout.setSpacing(0)
4312 self.menu = PileViewerMenuBar(self)
4313 # self.menu.setNativeMenuBar(False) # put menubar into window on mac
4315 if use_opengl is None:
4316 use_opengl = is_macos
4318 if use_opengl:
4319 self.viewer = GLPileViewerMain(
4320 pile,
4321 ntracks_shown_max=ntracks_shown_max,
4322 panel_parent=panel_parent,
4323 menu=self.menu)
4324 else:
4325 self.viewer = PileViewerMain(
4326 pile,
4327 ntracks_shown_max=ntracks_shown_max,
4328 panel_parent=panel_parent,
4329 menu=self.menu)
4331 self.marker_editor_sortable = marker_editor_sortable
4333 # self.setFrameShape(qw.QFrame.StyledPanel)
4334 # self.setFrameShadow(qw.QFrame.Sunken)
4336 self.input_area = qw.QFrame(self)
4337 ia_layout = qw.QGridLayout()
4338 ia_layout.setContentsMargins(11, 11, 11, 11)
4339 self.input_area.setLayout(ia_layout)
4341 self.inputline = LineEditWithAbort(self.input_area)
4342 self.inputline.returnPressed.connect(
4343 self.inputline_returnpressed)
4344 self.inputline.editingFinished.connect(
4345 self.inputline_finished)
4346 self.inputline.aborted.connect(
4347 self.inputline_aborted)
4349 self.inputline.history_down.connect(
4350 lambda: self.step_through_history(1))
4351 self.inputline.history_up.connect(
4352 lambda: self.step_through_history(-1))
4354 self.inputline.textEdited.connect(
4355 self.inputline_changed)
4357 self.inputline.setPlaceholderText(
4358 u'Quick commands: e.g. \'c HH?\' to select channels. '
4359 u'Use ↑ or ↓ to navigate.')
4360 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4361 self.input_area.hide()
4362 self.history = None
4364 self.inputline_error_str = None
4366 self.inputline_error = qw.QLabel()
4367 self.inputline_error.hide()
4369 ia_layout.addWidget(self.inputline, 0, 0)
4370 ia_layout.addWidget(self.inputline_error, 1, 0)
4371 layout.addWidget(self.input_area, 0, 0, 1, 2)
4372 layout.addWidget(self.viewer, 1, 0)
4374 pb = Progressbars(self)
4375 layout.addWidget(pb, 2, 0, 1, 2)
4376 self.progressbars = pb
4378 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4379 self.scrollbar = scrollbar
4380 layout.addWidget(scrollbar, 1, 1)
4381 self.scrollbar.valueChanged.connect(
4382 self.scrollbar_changed)
4384 self.block_scrollbar_changes = False
4386 self.viewer.want_input.connect(
4387 self.inputline_show)
4388 self.viewer.tracks_range_changed.connect(
4389 self.tracks_range_changed)
4390 self.viewer.pile_has_changed_signal.connect(
4391 self.adjust_controls)
4392 self.viewer.about_to_close.connect(
4393 self.save_inputline_history)
4395 self.setLayout(layout)
4397 def cleanup(self):
4398 self.viewer.cleanup()
4400 def get_progressbars(self):
4401 return self.progressbars
4403 def inputline_show(self):
4404 if not self.history:
4405 self.load_inputline_history()
4407 self.input_area.show()
4408 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4409 self.inputline.selectAll()
4411 def inputline_set_error(self, string):
4412 self.inputline_error_str = string
4413 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4414 self.inputline.selectAll()
4415 self.inputline_error.setText(string)
4416 self.input_area.show()
4417 self.inputline_error.show()
4419 def inputline_clear_error(self):
4420 if self.inputline_error_str:
4421 self.inputline.setPalette(qw.QApplication.palette())
4422 self.inputline_error_str = None
4423 self.inputline_error.clear()
4424 self.inputline_error.hide()
4426 def inputline_changed(self, line):
4427 self.viewer.inputline_changed(str(line))
4428 self.inputline_clear_error()
4430 def inputline_returnpressed(self):
4431 line = str(self.inputline.text())
4432 clearit, hideit, error = self.viewer.inputline_finished(line)
4434 if error:
4435 self.inputline_set_error(error)
4437 line = line.strip()
4439 if line != '' and not error:
4440 if not (len(self.history) >= 1 and line == self.history[-1]):
4441 self.history.append(line)
4443 if clearit:
4445 self.inputline.blockSignals(True)
4446 qpat, qinp = self.viewer.get_quick_filter_patterns()
4447 if qpat is None:
4448 self.inputline.clear()
4449 else:
4450 self.inputline.setText(qinp)
4451 self.inputline.blockSignals(False)
4453 if hideit and not error:
4454 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4455 self.input_area.hide()
4457 self.hist_ind = len(self.history)
4459 def inputline_aborted(self):
4460 '''
4461 Hide the input line.
4462 '''
4463 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4464 self.hist_ind = len(self.history)
4465 self.input_area.hide()
4467 def save_inputline_history(self):
4468 '''
4469 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4470 '''
4471 if not self.history:
4472 return
4474 conf = pyrocko.config
4475 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4476 with open(fn_hist, 'w') as f:
4477 i = min(100, len(self.history))
4478 for c in self.history[-i:]:
4479 f.write('%s\n' % c)
4481 def load_inputline_history(self):
4482 '''
4483 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4484 '''
4485 conf = pyrocko.config
4486 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4487 if not os.path.exists(fn_hist):
4488 with open(fn_hist, 'w+') as f:
4489 f.write('\n')
4491 with open(fn_hist, 'r') as f:
4492 self.history = [line.strip() for line in f.readlines()]
4494 self.hist_ind = len(self.history)
4496 def step_through_history(self, ud=1):
4497 '''
4498 Step through input line history and set the input line text.
4499 '''
4500 n = len(self.history)
4501 self.hist_ind += ud
4502 self.hist_ind %= (n + 1)
4503 if len(self.history) != 0 and self.hist_ind != n:
4504 self.inputline.setText(self.history[self.hist_ind])
4505 else:
4506 self.inputline.setText('')
4508 def inputline_finished(self):
4509 pass
4511 def tracks_range_changed(self, ntracks, ilo, ihi):
4512 if self.block_scrollbar_changes:
4513 return
4515 self.scrollbar.blockSignals(True)
4516 self.scrollbar.setPageStep(ihi-ilo)
4517 vmax = max(0, ntracks-(ihi-ilo))
4518 self.scrollbar.setRange(0, vmax)
4519 self.scrollbar.setValue(ilo)
4520 self.scrollbar.setHidden(vmax == 0)
4521 self.scrollbar.blockSignals(False)
4523 def scrollbar_changed(self, value):
4524 self.block_scrollbar_changes = True
4525 ilo = value
4526 ihi = ilo + self.scrollbar.pageStep()
4527 self.viewer.set_tracks_range((ilo, ihi))
4528 self.block_scrollbar_changes = False
4529 self.update_contents()
4531 def controls(self):
4532 frame = qw.QFrame(self)
4533 layout = qw.QGridLayout()
4534 frame.setLayout(layout)
4536 minfreq = 0.001
4537 maxfreq = 1000.0
4538 self.lowpass_control = ValControl(high_is_none=True)
4539 self.lowpass_control.setup(
4540 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4541 self.highpass_control = ValControl(low_is_none=True)
4542 self.highpass_control.setup(
4543 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4544 self.gain_control = ValControl()
4545 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4546 self.rot_control = LinValControl()
4547 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4548 self.colorbar_control = ColorbarControl(self)
4550 self.lowpass_control.valchange.connect(
4551 self.viewer.lowpass_change)
4552 self.highpass_control.valchange.connect(
4553 self.viewer.highpass_change)
4554 self.gain_control.valchange.connect(
4555 self.viewer.gain_change)
4556 self.rot_control.valchange.connect(
4557 self.viewer.rot_change)
4558 self.colorbar_control.cmap_changed.connect(
4559 self.viewer.waterfall_cmap_change
4560 )
4561 self.colorbar_control.clip_changed.connect(
4562 self.viewer.waterfall_clip_change
4563 )
4564 self.colorbar_control.show_absolute_toggled.connect(
4565 self.viewer.waterfall_show_absolute_change
4566 )
4567 self.colorbar_control.show_integrate_toggled.connect(
4568 self.viewer.waterfall_set_integrate
4569 )
4571 for icontrol, control in enumerate((
4572 self.highpass_control,
4573 self.lowpass_control,
4574 self.gain_control,
4575 self.rot_control,
4576 self.colorbar_control)):
4578 for iwidget, widget in enumerate(control.widgets()):
4579 layout.addWidget(widget, icontrol, iwidget)
4581 spacer = qw.QSpacerItem(
4582 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4583 layout.addItem(spacer, 4, 0, 1, 3)
4585 self.adjust_controls()
4586 self.viewer.viewmode_change(ViewMode.Wiggle)
4587 return frame
4589 def marker_editor(self):
4590 editor = pyrocko.gui.marker_editor.MarkerEditor(
4591 self, sortable=self.marker_editor_sortable)
4593 editor.set_viewer(self.get_view())
4594 editor.get_marker_model().dataChanged.connect(
4595 self.update_contents)
4596 return editor
4598 def adjust_controls(self):
4599 dtmin, dtmax = self.viewer.content_deltat_range()
4600 maxfreq = 0.5/dtmin
4601 minfreq = (0.5/dtmax)*0.001
4602 self.lowpass_control.set_range(minfreq, maxfreq)
4603 self.highpass_control.set_range(minfreq, maxfreq)
4605 def setup_snufflings(self):
4606 self.viewer.setup_snufflings()
4608 def get_view(self):
4609 return self.viewer
4611 def update_contents(self):
4612 self.viewer.update()
4614 def get_pile(self):
4615 return self.viewer.get_pile()
4617 def show_colorbar_ctrl(self, show):
4618 for w in self.colorbar_control.widgets():
4619 w.setVisible(show)
4621 def show_gain_ctrl(self, show):
4622 for w in self.gain_control.widgets():
4623 w.setVisible(show)