1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import, print_function
7import sys
8import os
9import time
10import calendar
11import datetime
12import re
13import math
14import logging
15import operator
16import copy
17import enum
18from itertools import groupby
20import numpy as num
21import pyrocko.model
22import pyrocko.pile
23import pyrocko.trace
24import pyrocko.response
25import pyrocko.util
26import pyrocko.plot
27import pyrocko.gui.snuffling
28import pyrocko.gui.snufflings
29import pyrocko.gui.marker_editor
31from pyrocko.util import hpfloat, gmtime_x, mystrftime
33from .marker import associate_phases_to_events, MarkerOneNSLCRequired
35from .util import (ValControl, LinValControl, Marker, EventMarker,
36 PhaseMarker, make_QPolygonF, draw_label, Label,
37 Progressbars, ColorbarControl)
39from .qt_compat import qc, qg, qw, qgl, qsvg, use_pyqt5
41from .pile_viewer_waterfall import TraceWaterfall
43import scipy.stats as sstats
44import platform
46MIN_LABEL_SIZE_PT = 6
48try:
49 newstr = unicode
50except NameError:
51 newstr = str
54def fnpatch(x):
55 if use_pyqt5:
56 return x
57 else:
58 return x, None
61if sys.version_info[0] >= 3:
62 qc.QString = str
64qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
65 qw.QFileDialog.DontUseSheet
67if platform.mac_ver() != ('', ('', '', ''), ''):
68 macosx = True
69else:
70 macosx = False
72logger = logging.getLogger('pyrocko.gui.pile_viewer')
75def detrend(x, y):
76 slope, offset, _, _, _ = sstats.linregress(x, y)
77 y_detrended = y - slope * x - offset
78 return y_detrended, slope, offset
81def retrend(x, y_detrended, slope, offset):
82 return x * slope + y_detrended + offset
85class Global(object):
86 appOnDemand = None
89class NSLC(object):
90 def __init__(self, n, s, l=None, c=None): # noqa
91 self.network = n
92 self.station = s
93 self.location = l
94 self.channel = c
97class m_float(float):
99 def __str__(self):
100 if abs(self) >= 10000.:
101 return '%g km' % round(self/1000., 0)
102 elif abs(self) >= 1000.:
103 return '%g km' % round(self/1000., 1)
104 else:
105 return '%.5g m' % self
107 def __lt__(self, other):
108 if other is None:
109 return True
110 return float(self) < float(other)
112 def __gt__(self, other):
113 if other is None:
114 return False
115 return float(self) > float(other)
118def m_float_or_none(x):
119 if x is None:
120 return None
121 else:
122 return m_float(x)
125def make_chunks(items):
126 '''
127 Split a list of integers into sublists of consecutive elements.
128 '''
129 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
130 enumerate(items), (lambda x: x[1]-x[0]))]
133class deg_float(float):
135 def __str__(self):
136 return '%4.0f' % self
139def deg_float_or_none(x):
140 if x is None:
141 return None
142 else:
143 return deg_float(x)
146class sector_int(int):
148 def __str__(self):
149 return '[%i]' % self
152def num_to_html(num):
153 snum = '%g' % num
154 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
155 if m:
156 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
158 return snum
161gap_lap_tolerance = 5.
164class ViewMode(enum.Enum):
165 Wiggle = 1
166 Waterfall = 2
169class Timer(object):
170 def __init__(self):
171 self._start = None
172 self._stop = None
174 def start(self):
175 self._start = os.times()
177 def stop(self):
178 self._stop = os.times()
180 def get(self):
181 a = self._start
182 b = self._stop
183 if a is not None and b is not None:
184 return tuple([b[i] - a[i] for i in range(5)])
185 else:
186 return tuple([0.] * 5)
188 def __sub__(self, other):
189 a = self.get()
190 b = other.get()
191 return tuple([a[i] - b[i] for i in range(5)])
194class ObjectStyle(object):
195 def __init__(self, frame_pen, fill_brush):
196 self.frame_pen = frame_pen
197 self.fill_brush = fill_brush
200box_styles = []
201box_alpha = 100
202for color in 'orange skyblue butter chameleon chocolate plum ' \
203 'scarletred'.split():
205 box_styles.append(ObjectStyle(
206 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
207 qg.QBrush(qg.QColor(
208 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
209 ))
211box_styles_coverage = {}
213box_styles_coverage['waveform'] = [
214 ObjectStyle(
215 qg.QPen(
216 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
217 1, qc.Qt.DashLine),
218 qg.QBrush(qg.QColor(
219 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
220 ),
221 ObjectStyle(
222 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
223 qg.QBrush(qg.QColor(
224 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
225 ),
226 ObjectStyle(
227 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
228 qg.QBrush(qg.QColor(
229 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
230 )]
232box_styles_coverage['waveform_promise'] = [
233 ObjectStyle(
234 qg.QPen(
235 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
236 1, qc.Qt.DashLine),
237 qg.QBrush(qg.QColor(
238 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
239 ),
240 ObjectStyle(
241 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
242 qg.QBrush(qg.QColor(
243 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
244 ),
245 ObjectStyle(
246 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
247 qg.QBrush(qg.QColor(
248 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
249 )]
251sday = 60*60*24. # \
252smonth = 60*60*24*30. # | only used as approx. intervals...
253syear = 60*60*24*365. # /
255acceptable_tincs = num.array([
256 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
257 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
260working_system_time_range = \
261 pyrocko.util.working_system_time_range()
263initial_time_range = []
265try:
266 initial_time_range.append(
267 calendar.timegm((1950, 1, 1, 0, 0, 0)))
268except Exception:
269 initial_time_range.append(working_system_time_range[0])
271try:
272 initial_time_range.append(
273 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
274except Exception:
275 initial_time_range.append(working_system_time_range[1])
278def is_working_time(t):
279 return working_system_time_range[0] <= t and \
280 t <= working_system_time_range[1]
283def fancy_time_ax_format(inc):
284 l0_fmt_brief = ''
285 l2_fmt = ''
286 l2_trig = 0
287 if inc < 0.000001:
288 l0_fmt = '.%n'
289 l0_center = False
290 l1_fmt = '%H:%M:%S'
291 l1_trig = 6
292 l2_fmt = '%b %d, %Y'
293 l2_trig = 3
294 elif inc < 0.001:
295 l0_fmt = '.%u'
296 l0_center = False
297 l1_fmt = '%H:%M:%S'
298 l1_trig = 6
299 l2_fmt = '%b %d, %Y'
300 l2_trig = 3
301 elif inc < 1:
302 l0_fmt = '.%r'
303 l0_center = False
304 l1_fmt = '%H:%M:%S'
305 l1_trig = 6
306 l2_fmt = '%b %d, %Y'
307 l2_trig = 3
308 elif inc < 60:
309 l0_fmt = '%H:%M:%S'
310 l0_center = False
311 l1_fmt = '%b %d, %Y'
312 l1_trig = 3
313 elif inc < 3600:
314 l0_fmt = '%H:%M'
315 l0_center = False
316 l1_fmt = '%b %d, %Y'
317 l1_trig = 3
318 elif inc < sday:
319 l0_fmt = '%H:%M'
320 l0_center = False
321 l1_fmt = '%b %d, %Y'
322 l1_trig = 3
323 elif inc < smonth:
324 l0_fmt = '%a %d'
325 l0_fmt_brief = '%d'
326 l0_center = True
327 l1_fmt = '%b, %Y'
328 l1_trig = 2
329 elif inc < syear:
330 l0_fmt = '%b'
331 l0_center = True
332 l1_fmt = '%Y'
333 l1_trig = 1
334 else:
335 l0_fmt = '%Y'
336 l0_center = False
337 l1_fmt = ''
338 l1_trig = 0
340 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
343def day_start(timestamp):
344 tt = time.gmtime(int(timestamp))
345 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
346 return calendar.timegm(tts)
349def month_start(timestamp):
350 tt = time.gmtime(int(timestamp))
351 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
352 return calendar.timegm(tts)
355def year_start(timestamp):
356 tt = time.gmtime(int(timestamp))
357 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
358 return calendar.timegm(tts)
361def time_nice_value(inc0):
362 if inc0 < acceptable_tincs[0]:
363 return pyrocko.plot.nice_value(inc0)
364 elif inc0 > acceptable_tincs[-1]:
365 return pyrocko.plot.nice_value(inc0/syear)*syear
366 else:
367 i = num.argmin(num.abs(acceptable_tincs-inc0))
368 return acceptable_tincs[i]
371class TimeScaler(pyrocko.plot.AutoScaler):
372 def __init__(self):
373 pyrocko.plot.AutoScaler.__init__(self)
374 self.mode = 'min-max'
376 def make_scale(self, data_range):
377 assert self.mode in ('min-max', 'off'), \
378 'mode must be "min-max" or "off" for TimeScaler'
380 data_min = min(data_range)
381 data_max = max(data_range)
382 is_reverse = (data_range[0] > data_range[1])
384 mi, ma = data_min, data_max
385 nmi = mi
386 if self.mode != 'off':
387 nmi = mi - self.space*(ma-mi)
389 nma = ma
390 if self.mode != 'off':
391 nma = ma + self.space*(ma-mi)
393 mi, ma = nmi, nma
395 if mi == ma and self.mode != 'off':
396 mi -= 1.0
397 ma += 1.0
399 mi = max(working_system_time_range[0], mi)
400 ma = min(working_system_time_range[1], ma)
402 # make nice tick increment
403 if self.inc is not None:
404 inc = self.inc
405 else:
406 if self.approx_ticks > 0.:
407 inc = time_nice_value((ma-mi)/self.approx_ticks)
408 else:
409 inc = time_nice_value((ma-mi)*10.)
411 if inc == 0.0:
412 inc = 1.0
414 if is_reverse:
415 return ma, mi, -inc
416 else:
417 return mi, ma, inc
419 def make_ticks(self, data_range):
420 mi, ma, inc = self.make_scale(data_range)
422 is_reverse = False
423 if inc < 0:
424 mi, ma, inc = ma, mi, -inc
425 is_reverse = True
427 ticks = []
429 if inc < sday:
430 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
431 if inc < 0.001:
432 mi_day = hpfloat(mi_day)
434 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
435 if inc < 0.001:
436 base = hpfloat(base)
438 base_day = mi_day
439 i = 0
440 while True:
441 tick = base+i*inc
442 if tick > ma:
443 break
445 tick_day = day_start(tick)
446 if tick_day > base_day:
447 base_day = tick_day
448 base = base_day
449 i = 0
450 else:
451 ticks.append(tick)
452 i += 1
454 elif inc < smonth:
455 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
456 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
457 delta = datetime.timedelta(days=int(round(inc/sday)))
458 if mi_day == mi:
459 dt_base += delta
460 i = 0
461 while True:
462 current = dt_base + i*delta
463 tick = calendar.timegm(current.timetuple())
464 if tick > ma:
465 break
466 ticks.append(tick)
467 i += 1
469 elif inc < syear:
470 mi_month = month_start(max(
471 mi, working_system_time_range[0]+smonth*1.5))
473 y, m = time.gmtime(mi_month)[:2]
474 while True:
475 tick = calendar.timegm((y, m, 1, 0, 0, 0))
476 m += 1
477 if m > 12:
478 y, m = y+1, 1
480 if tick > ma:
481 break
483 if tick >= mi:
484 ticks.append(tick)
486 else:
487 mi_year = year_start(max(
488 mi, working_system_time_range[0]+syear*1.5))
490 incy = int(round(inc/syear))
491 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
493 while True:
494 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
495 y += incy
496 if tick > ma:
497 break
498 if tick >= mi:
499 ticks.append(tick)
501 if is_reverse:
502 ticks.reverse()
504 return ticks, inc
507def need_l1_tick(tt, ms, l1_trig):
508 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
511def tick_to_labels(tick, inc):
512 tt, ms = gmtime_x(tick)
513 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
514 fancy_time_ax_format(inc)
516 l0 = mystrftime(l0_fmt, tt, ms)
517 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
518 l1, l2 = None, None
519 if need_l1_tick(tt, ms, l1_trig):
520 l1 = mystrftime(l1_fmt, tt, ms)
521 if need_l1_tick(tt, ms, l2_trig):
522 l2 = mystrftime(l2_fmt, tt, ms)
524 return l0, l0_brief, l0_center, l1, l2
527def l1_l2_tick(tick, inc):
528 tt, ms = gmtime_x(tick)
529 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
530 fancy_time_ax_format(inc)
532 l1 = mystrftime(l1_fmt, tt, ms)
533 l2 = mystrftime(l2_fmt, tt, ms)
534 return l1, l2
537class TimeAx(TimeScaler):
538 def __init__(self, *args):
539 TimeScaler.__init__(self, *args)
541 def drawit(self, p, xprojection, yprojection):
542 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
543 p.setPen(pen)
544 font = qg.QFont()
545 font.setBold(True)
546 p.setFont(font)
547 fm = p.fontMetrics()
548 ticklen = 10
549 pad = 10
550 tmin, tmax = xprojection.get_in_range()
551 ticks, inc = self.make_ticks((tmin, tmax))
552 l1_hits = 0
553 l2_hits = 0
555 vmin, vmax = yprojection(0), yprojection(ticklen)
556 uumin, uumax = xprojection.get_out_range()
557 first_tick_with_label = None
558 for tick in ticks:
559 umin = xprojection(tick)
561 umin_approx_next = xprojection(tick+inc)
562 umax = xprojection(tick)
564 pinc_approx = umin_approx_next - umin
566 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
567 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
569 if tick == 0.0 and tmax - tmin < 3600*24:
570 # hide year at epoch (we assume that synthetic data is shown)
571 if l2:
572 l2 = None
573 elif l1:
574 l1 = None
576 if l0_center:
577 ushift = (umin_approx_next-umin)/2.
578 else:
579 ushift = 0.
581 for l0x in (l0, l0_brief, ''):
582 label0 = l0x
583 rect0 = fm.boundingRect(label0)
584 if rect0.width() <= pinc_approx*0.9:
585 break
587 if uumin+pad < umin-rect0.width()/2.+ushift and \
588 umin+rect0.width()/2.+ushift < uumax-pad:
590 if first_tick_with_label is None:
591 first_tick_with_label = tick
592 p.drawText(qc.QPointF(
593 umin-rect0.width()/2.+ushift,
594 vmin+rect0.height()+ticklen), label0)
596 if l1:
597 label1 = l1
598 rect1 = fm.boundingRect(label1)
599 if uumin+pad < umin-rect1.width()/2. and \
600 umin+rect1.width()/2. < uumax-pad:
602 p.drawText(qc.QPointF(
603 umin-rect1.width()/2.,
604 vmin+rect0.height()+rect1.height()+ticklen),
605 label1)
607 l1_hits += 1
609 if l2:
610 label2 = l2
611 rect2 = fm.boundingRect(label2)
612 if uumin+pad < umin-rect2.width()/2. and \
613 umin+rect2.width()/2. < uumax-pad:
615 p.drawText(qc.QPointF(
616 umin-rect2.width()/2.,
617 vmin+rect0.height()+rect1.height()+rect2.height() +
618 ticklen), label2)
620 l2_hits += 1
622 if first_tick_with_label is None:
623 first_tick_with_label = tmin
625 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
627 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
628 tmax - tmin < 3600*24:
630 # hide year at epoch (we assume that synthetic data is shown)
631 if l2:
632 l2 = None
633 elif l1:
634 l1 = None
636 if l1_hits == 0 and l1:
637 label1 = l1
638 rect1 = fm.boundingRect(label1)
639 p.drawText(qc.QPointF(
640 uumin+pad,
641 vmin+rect0.height()+rect1.height()+ticklen),
642 label1)
644 l1_hits += 1
646 if l2_hits == 0 and l2:
647 label2 = l2
648 rect2 = fm.boundingRect(label2)
649 p.drawText(qc.QPointF(
650 uumin+pad,
651 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
652 label2)
654 v = yprojection(0)
655 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
658class Projection(object):
659 def __init__(self):
660 self.xr = 0., 1.
661 self.ur = 0., 1.
663 def set_in_range(self, xmin, xmax):
664 if xmax == xmin:
665 xmax = xmin + 1.
667 self.xr = xmin, xmax
669 def get_in_range(self):
670 return self.xr
672 def set_out_range(self, umin, umax):
673 if umax == umin:
674 umax = umin + 1.
676 self.ur = umin, umax
678 def get_out_range(self):
679 return self.ur
681 def __call__(self, x):
682 umin, umax = self.ur
683 xmin, xmax = self.xr
684 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
686 def clipped(self, x):
687 umin, umax = self.ur
688 xmin, xmax = self.xr
689 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
691 def rev(self, u):
692 umin, umax = self.ur
693 xmin, xmax = self.xr
694 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
696 def copy(self):
697 return copy.copy(self)
700def add_radiobuttongroup(menu, menudef, target, default=None):
701 group = qw.QActionGroup(menu)
702 group.setExclusive(True)
703 menuitems = []
705 for name, value, *shortcut in menudef:
706 action = menu.addAction(name)
707 action.setCheckable(True)
708 action.setActionGroup(group)
709 if shortcut:
710 action.setShortcut(shortcut[0])
712 menuitems.append((action, value))
713 if default is not None and (
714 name.lower().replace(' ', '_') == default or
715 value == default):
716 action.setChecked(True)
718 group.triggered.connect(target)
720 if default is None:
721 menuitems[0][0].setChecked(True)
723 return menuitems
726def sort_actions(menu):
727 actions = [act for act in menu.actions() if not act.menu()]
728 for action in actions:
729 menu.removeAction(action)
730 actions.sort(key=lambda x: newstr(x.text()))
732 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
733 if help_action:
734 actions.insert(0, actions.pop(actions.index(help_action[0])))
735 for action in actions:
736 menu.addAction(action)
739fkey_map = dict(zip(
740 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
741 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
742 range(10)))
745class PileViewerMainException(Exception):
746 pass
749class PileViewerMenuBar(qw.QMenuBar):
750 ...
753class PileViewerMenu(qw.QMenu):
754 ...
757def MakePileViewerMainClass(base):
759 class PileViewerMain(base):
761 want_input = qc.pyqtSignal()
762 about_to_close = qc.pyqtSignal()
763 pile_has_changed_signal = qc.pyqtSignal()
764 tracks_range_changed = qc.pyqtSignal(int, int, int)
766 begin_markers_add = qc.pyqtSignal(int, int)
767 end_markers_add = qc.pyqtSignal()
768 begin_markers_remove = qc.pyqtSignal(int, int)
769 end_markers_remove = qc.pyqtSignal()
771 marker_selection_changed = qc.pyqtSignal(list)
772 active_event_marker_changed = qc.pyqtSignal()
774 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
775 menu=None):
776 if base == qgl.QGLWidget:
777 from OpenGL import GL # noqa
779 base.__init__(
780 self, qgl.QGLFormat(qgl.QGL.SampleBuffers), *args)
781 else:
782 base.__init__(self, *args)
784 self.pile = pile
785 self.ax_height = 80
786 self.panel_parent = panel_parent
788 self.click_tolerance = 5
790 self.ntracks_shown_max = ntracks_shown_max
791 self.initial_ntracks_shown_max = ntracks_shown_max
792 self.ntracks = 0
793 self.show_all = True
794 self.shown_tracks_range = None
795 self.track_start = None
796 self.track_trange = None
798 self.lowpass = None
799 self.highpass = None
800 self.gain = 1.0
801 self.rotate = 0.0
802 self.picking_down = None
803 self.picking = None
804 self.floating_marker = None
805 self.markers = pyrocko.pile.Sorted([], 'tmin')
806 self.markers_deltat_max = 0.
807 self.n_selected_markers = 0
808 self.all_marker_kinds = (0, 1, 2, 3, 4, 5)
809 self.visible_marker_kinds = self.all_marker_kinds
810 self.active_event_marker = None
811 self.ignore_releases = 0
812 self.message = None
813 self.reloaded = False
814 self.pile_has_changed = False
815 self.config = pyrocko.config.config('snuffler')
817 self.tax = TimeAx()
818 self.setBackgroundRole(qg.QPalette.Base)
819 self.setAutoFillBackground(True)
820 poli = qw.QSizePolicy(
821 qw.QSizePolicy.Expanding,
822 qw.QSizePolicy.Expanding)
824 self.setSizePolicy(poli)
825 self.setMinimumSize(300, 200)
826 self.setFocusPolicy(qc.Qt.ClickFocus)
828 self.menu = menu or PileViewerMenu(self)
830 file_menu = self.menu.addMenu('&File')
831 view_menu = self.menu.addMenu('&View')
832 options_menu = self.menu.addMenu('&Options')
833 scale_menu = self.menu.addMenu('&Scaling')
834 sort_menu = self.menu.addMenu('Sor&ting')
835 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
837 help_menu = self.menu.addMenu('&Help')
839 self.snufflings_menu = self.toggle_panel_menu.addMenu(
840 'Run Snuffling')
841 self.toggle_panel_menu.addSeparator()
842 self.snuffling_help = help_menu.addMenu('Snuffling Help')
843 help_menu.addSeparator()
845 file_menu.addAction(
846 qg.QIcon.fromTheme('document-open'),
847 'Open waveform files...',
848 self.open_waveforms,
849 qg.QKeySequence.Open)
851 file_menu.addAction(
852 qg.QIcon.fromTheme('document-open'),
853 'Open waveform directory...',
854 self.open_waveform_directory)
856 file_menu.addAction(
857 'Open station files...',
858 self.open_stations)
860 file_menu.addAction(
861 'Open StationXML files...',
862 self.open_stations_xml)
864 file_menu.addAction(
865 'Open event file...',
866 self.read_events)
868 file_menu.addSeparator()
869 file_menu.addAction(
870 'Open marker file...',
871 self.read_markers)
873 file_menu.addAction(
874 qg.QIcon.fromTheme('document-save'),
875 'Save markers...',
876 self.write_markers,
877 qg.QKeySequence.Save)
879 file_menu.addAction(
880 qg.QIcon.fromTheme('document-save-as'),
881 'Save selected markers...',
882 self.write_selected_markers,
883 qg.QKeySequence.SaveAs)
885 file_menu.addSeparator()
886 file_menu.addAction(
887 qg.QIcon.fromTheme('document-print'),
888 'Print',
889 self.printit,
890 qg.QKeySequence.Print)
892 file_menu.addAction(
893 qg.QIcon.fromTheme('insert-image'),
894 'Save as SVG or PNG',
895 self.savesvg,
896 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
898 file_menu.addSeparator()
899 close = file_menu.addAction(
900 qg.QIcon.fromTheme('window-close'),
901 'Close',
902 self.myclose)
903 close.setShortcuts(
904 (qg.QKeySequence(qc.Qt.Key_Q),
905 qg.QKeySequence(qc.Qt.Key_X)))
907 # Scale Menu
908 menudef = [
909 ('Individual Scale',
910 lambda tr: tr.nslc_id,
911 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
912 ('Common Scale',
913 lambda tr: None,
914 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
915 ('Common Scale per Station',
916 lambda tr: (tr.network, tr.station),
917 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
918 ('Common Scale per Station Location',
919 lambda tr: (tr.network, tr.station, tr.location)),
920 ('Common Scale per Component',
921 lambda tr: (tr.channel)),
922 ]
924 self.menuitems_scaling = add_radiobuttongroup(
925 scale_menu, menudef, self.scalingmode_change,
926 default=self.config.trace_scale)
927 scale_menu.addSeparator()
929 self.scaling_key = self.menuitems_scaling[0][1]
930 self.scaling_hooks = {}
931 self.scalingmode_change()
933 menudef = [
934 ('Scaling based on Minimum and Maximum', 'minmax'),
935 ('Scaling based on Mean ± 2x Std. Deviation', 2),
936 ('Scaling based on Mean ± 4x Std. Deviation', 4),
937 ]
939 self.menuitems_scaling_base = add_radiobuttongroup(
940 scale_menu, menudef, self.scaling_base_change)
942 self.scaling_base = self.menuitems_scaling_base[0][1]
943 scale_menu.addSeparator()
945 self.menuitem_fixscalerange = scale_menu.addAction(
946 'Fix Scale Ranges')
947 self.menuitem_fixscalerange.setCheckable(True)
949 # Sort Menu
950 def sector_dist(sta):
951 if sta.dist_m is None:
952 return None, None
953 else:
954 return (
955 sector_int(round((sta.azimuth+15.)/30.)),
956 m_float(sta.dist_m))
958 menudef = [
959 ('Sort by Names',
960 lambda tr: (),
961 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
962 ('Sort by Distance',
963 lambda tr: self.station_attrib(
964 tr,
965 lambda sta: (m_float_or_none(sta.dist_m),),
966 lambda tr: (None,)),
967 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
968 ('Sort by Azimuth',
969 lambda tr: self.station_attrib(
970 tr,
971 lambda sta: (deg_float_or_none(sta.azimuth),),
972 lambda tr: (None,))),
973 ('Sort by Distance in 12 Azimuthal Blocks',
974 lambda tr: self.station_attrib(
975 tr,
976 sector_dist,
977 lambda tr: (None, None))),
978 ('Sort by Backazimuth',
979 lambda tr: self.station_attrib(
980 tr,
981 lambda sta: (deg_float_or_none(sta.backazimuth),),
982 lambda tr: (None,))),
983 ]
984 self.menuitems_ssorting = add_radiobuttongroup(
985 sort_menu, menudef, self.s_sortingmode_change)
986 sort_menu.addSeparator()
988 self._ssort = lambda tr: ()
990 self.menu.addSeparator()
992 menudef = [
993 ('Subsort by Network, Station, Location, Channel',
994 ((0, 1, 2, 3), # gathering
995 lambda tr: tr.location)), # coloring
996 ('Subsort by Network, Station, Channel, Location',
997 ((0, 1, 3, 2),
998 lambda tr: tr.channel)),
999 ('Subsort by Station, Network, Channel, Location',
1000 ((1, 0, 3, 2),
1001 lambda tr: tr.channel)),
1002 ('Subsort by Location, Network, Station, Channel',
1003 ((2, 0, 1, 3),
1004 lambda tr: tr.channel)),
1005 ('Subsort by Channel, Network, Station, Location',
1006 ((3, 0, 1, 2),
1007 lambda tr: (tr.network, tr.station, tr.location))),
1008 ('Subsort by Network, Station, Channel (Grouped by Location)',
1009 ((0, 1, 3),
1010 lambda tr: tr.location)),
1011 ('Subsort by Station, Network, Channel (Grouped by Location)',
1012 ((1, 0, 3),
1013 lambda tr: tr.location)),
1014 ]
1016 self.menuitems_sorting = add_radiobuttongroup(
1017 sort_menu, menudef, self.sortingmode_change)
1019 menudef = [(x.key, x.value) for x in
1020 self.config.visible_length_setting]
1022 # View menu
1023 self.menuitems_visible_length = add_radiobuttongroup(
1024 view_menu, menudef,
1025 self.visible_length_change)
1026 view_menu.addSeparator()
1028 view_modes = [
1029 ('Wiggle Plot', ViewMode.Wiggle),
1030 ('Waterfall', ViewMode.Waterfall)
1031 ]
1033 self.menuitems_viewmode = add_radiobuttongroup(
1034 view_menu, view_modes,
1035 self.viewmode_change, default=ViewMode.Wiggle)
1036 view_menu.addSeparator()
1038 self.menuitem_cliptraces = view_menu.addAction(
1039 'Clip Traces')
1040 self.menuitem_cliptraces.setCheckable(True)
1041 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1043 self.menuitem_showboxes = view_menu.addAction(
1044 'Show Boxes')
1045 self.menuitem_showboxes.setCheckable(True)
1046 self.menuitem_showboxes.setChecked(
1047 self.config.show_boxes)
1049 self.menuitem_colortraces = view_menu.addAction(
1050 'Color Traces')
1051 self.menuitem_colortraces.setCheckable(True)
1052 self.menuitem_antialias = view_menu.addAction(
1053 'Antialiasing')
1054 self.menuitem_antialias.setCheckable(True)
1056 view_menu.addSeparator()
1057 self.menuitem_showscalerange = view_menu.addAction(
1058 'Show Scale Ranges')
1059 self.menuitem_showscalerange.setCheckable(True)
1060 self.menuitem_showscalerange.setChecked(
1061 self.config.show_scale_ranges)
1063 self.menuitem_showscaleaxis = view_menu.addAction(
1064 'Show Scale Axes')
1065 self.menuitem_showscaleaxis.setCheckable(True)
1066 self.menuitem_showscaleaxis.setChecked(
1067 self.config.show_scale_axes)
1069 self.menuitem_showzeroline = view_menu.addAction(
1070 'Show Zero Lines')
1071 self.menuitem_showzeroline.setCheckable(True)
1073 view_menu.addSeparator()
1074 view_menu.addAction(
1075 qg.QIcon.fromTheme('view-fullscreen'),
1076 'Fullscreen',
1077 self.toggle_fullscreen,
1078 qg.QKeySequence(qc.Qt.Key_F11))
1080 # Options Menu
1081 self.menuitem_demean = options_menu.addAction('Demean')
1082 self.menuitem_demean.setCheckable(True)
1083 self.menuitem_demean.setChecked(self.config.demean)
1084 self.menuitem_demean.setShortcut(
1085 qg.QKeySequence(qc.Qt.Key_Underscore))
1087 self.menuitem_distances_3d = options_menu.addAction(
1088 '3D distances',
1089 self.distances_3d_changed)
1090 self.menuitem_distances_3d.setCheckable(True)
1092 self.menuitem_allowdownsampling = options_menu.addAction(
1093 'Allow Downsampling')
1094 self.menuitem_allowdownsampling.setCheckable(True)
1095 self.menuitem_allowdownsampling.setChecked(True)
1097 self.menuitem_degap = options_menu.addAction(
1098 'Allow Degapping')
1099 self.menuitem_degap.setCheckable(True)
1100 self.menuitem_degap.setChecked(True)
1102 options_menu.addSeparator()
1104 self.menuitem_fft_filtering = options_menu.addAction(
1105 'FFT Filtering')
1106 self.menuitem_fft_filtering.setCheckable(True)
1108 self.menuitem_lphp = options_menu.addAction(
1109 'Bandpass is Low- + Highpass')
1110 self.menuitem_lphp.setCheckable(True)
1111 self.menuitem_lphp.setChecked(True)
1113 options_menu.addSeparator()
1114 self.menuitem_watch = options_menu.addAction(
1115 'Watch Files')
1116 self.menuitem_watch.setCheckable(True)
1118 self.menuitem_liberal_fetch = options_menu.addAction(
1119 'Liberal Fetch Optimization')
1120 self.menuitem_liberal_fetch.setCheckable(True)
1122 self.visible_length = menudef[0][1]
1124 self.snufflings_menu.addAction(
1125 'Reload Snufflings',
1126 self.setup_snufflings)
1128 # Disable ShadowPileTest
1129 if False:
1130 test_action = self.menu.addAction(
1131 'Test',
1132 self.toggletest)
1133 test_action.setCheckable(True)
1135 help_menu.addAction(
1136 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1137 'Snuffler Controls',
1138 self.help,
1139 qg.QKeySequence(qc.Qt.Key_Question))
1141 help_menu.addAction(
1142 'About',
1143 self.about)
1145 self.time_projection = Projection()
1146 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1147 self.time_projection.set_out_range(0., self.width())
1149 self.gather = None
1151 self.trace_filter = None
1152 self.quick_filter = None
1153 self.quick_filter_patterns = None, None
1154 self.blacklist = []
1156 self.track_to_screen = Projection()
1157 self.track_to_nslc_ids = {}
1159 self.cached_vec = None
1160 self.cached_processed_traces = None
1162 self.timer = qc.QTimer(self)
1163 self.timer.timeout.connect(self.periodical)
1164 self.timer.setInterval(1000)
1165 self.timer.start()
1166 self.pile.add_listener(self)
1167 self.trace_styles = {}
1168 if self.get_squirrel() is None:
1169 self.determine_box_styles()
1171 self.setMouseTracking(True)
1173 user_home_dir = os.path.expanduser('~')
1174 self.snuffling_modules = {}
1175 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1176 self.default_snufflings = None
1177 self.snufflings = []
1179 self.stations = {}
1181 self.timer_draw = Timer()
1182 self.timer_cutout = Timer()
1183 self.time_spent_painting = 0.0
1184 self.time_last_painted = time.time()
1186 self.interactive_range_change_time = 0.0
1187 self.interactive_range_change_delay_time = 10.0
1188 self.follow_timer = None
1190 self.sortingmode_change_time = 0.0
1191 self.sortingmode_change_delay_time = None
1193 self.old_data_ranges = {}
1195 self.error_messages = {}
1196 self.return_tag = None
1197 self.wheel_pos = 60
1199 self.setAcceptDrops(True)
1200 self._paths_to_load = []
1202 self.tf_cache = {}
1204 self.waterfall = TraceWaterfall()
1205 self.waterfall_cmap = 'viridis'
1206 self.waterfall_clip_min = 0.
1207 self.waterfall_clip_max = 1.
1208 self.waterfall_show_absolute = False
1209 self.waterfall_integrate = False
1210 self.view_mode = ViewMode.Wiggle
1212 self.automatic_updates = True
1214 self.closing = False
1215 self.paint_timer = qc.QTimer(self)
1216 self.paint_timer.timeout.connect(self.reset_updates)
1217 self.paint_timer.setInterval(20)
1218 self.paint_timer.start()
1220 @qc.pyqtSlot()
1221 def reset_updates(self):
1222 if not self.updatesEnabled():
1223 self.setUpdatesEnabled(True)
1225 def fail(self, reason):
1226 box = qw.QMessageBox(self)
1227 box.setText(reason)
1228 box.exec_()
1230 def set_trace_filter(self, filter_func):
1231 self.trace_filter = filter_func
1232 self.sortingmode_change()
1234 def update_trace_filter(self):
1235 if self.blacklist:
1237 def blacklist_func(tr):
1238 return not pyrocko.util.match_nslc(
1239 self.blacklist, tr.nslc_id)
1241 else:
1242 blacklist_func = None
1244 if self.quick_filter is None and blacklist_func is None:
1245 self.set_trace_filter(None)
1246 elif self.quick_filter is None:
1247 self.set_trace_filter(blacklist_func)
1248 elif blacklist_func is None:
1249 self.set_trace_filter(self.quick_filter)
1250 else:
1251 self.set_trace_filter(
1252 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1254 def set_quick_filter(self, filter_func):
1255 self.quick_filter = filter_func
1256 self.update_trace_filter()
1258 def set_quick_filter_patterns(self, patterns, inputline=None):
1259 if patterns is not None:
1260 self.set_quick_filter(
1261 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1262 else:
1263 self.set_quick_filter(None)
1265 self.quick_filter_patterns = patterns, inputline
1267 def get_quick_filter_patterns(self):
1268 return self.quick_filter_patterns
1270 def add_blacklist_pattern(self, pattern):
1271 if pattern == 'empty':
1272 keys = set(self.pile.nslc_ids)
1273 trs = self.pile.all(
1274 tmin=self.tmin,
1275 tmax=self.tmax,
1276 load_data=False,
1277 degap=False)
1279 for tr in trs:
1280 if tr.nslc_id in keys:
1281 keys.remove(tr.nslc_id)
1283 for key in keys:
1284 xpattern = '.'.join(key)
1285 if xpattern not in self.blacklist:
1286 self.blacklist.append(xpattern)
1288 else:
1289 if pattern in self.blacklist:
1290 self.blacklist.remove(pattern)
1292 self.blacklist.append(pattern)
1294 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1295 self.update_trace_filter()
1297 def remove_blacklist_pattern(self, pattern):
1298 if pattern in self.blacklist:
1299 self.blacklist.remove(pattern)
1300 else:
1301 raise PileViewerMainException(
1302 'Pattern not found in blacklist.')
1304 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1305 self.update_trace_filter()
1307 def clear_blacklist(self):
1308 self.blacklist = []
1309 self.update_trace_filter()
1311 def ssort(self, tr):
1312 return self._ssort(tr)
1314 def station_key(self, x):
1315 return x.network, x.station
1317 def station_keys(self, x):
1318 return [
1319 (x.network, x.station, x.location),
1320 (x.network, x.station)]
1322 def station_attrib(self, tr, getter, default_getter):
1323 for sk in self.station_keys(tr):
1324 if sk in self.stations:
1325 station = self.stations[sk]
1326 return getter(station)
1328 return default_getter(tr)
1330 def get_station(self, sk):
1331 return self.stations[sk]
1333 def has_station(self, station):
1334 for sk in self.station_keys(station):
1335 if sk in self.stations:
1336 return True
1338 return False
1340 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1341 return self.station_attrib(
1342 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1344 def set_stations(self, stations):
1345 self.stations = {}
1346 self.add_stations(stations)
1348 def add_stations(self, stations):
1349 for station in stations:
1350 for sk in self.station_keys(station):
1351 self.stations[sk] = station
1353 ev = self.get_active_event()
1354 if ev:
1355 self.set_origin(ev)
1357 def add_event(self, event):
1358 marker = EventMarker(event)
1359 self.add_marker(marker)
1361 def add_events(self, events):
1362 markers = [EventMarker(e) for e in events]
1363 self.add_markers(markers)
1365 def set_event_marker_as_origin(self, ignore=None):
1366 selected = self.selected_markers()
1367 if not selected:
1368 self.fail('An event marker must be selected.')
1369 return
1371 m = selected[0]
1372 if not isinstance(m, EventMarker):
1373 self.fail('Selected marker is not an event.')
1374 return
1376 self.set_active_event_marker(m)
1378 def deactivate_event_marker(self):
1379 if self.active_event_marker:
1380 self.active_event_marker.active = False
1382 self.active_event_marker_changed.emit()
1383 self.active_event_marker = None
1385 def set_active_event_marker(self, event_marker):
1386 if self.active_event_marker:
1387 self.active_event_marker.active = False
1389 self.active_event_marker = event_marker
1390 event_marker.active = True
1391 event = event_marker.get_event()
1392 self.set_origin(event)
1393 self.active_event_marker_changed.emit()
1395 def set_active_event(self, event):
1396 for marker in self.markers:
1397 if isinstance(marker, EventMarker):
1398 if marker.get_event() is event:
1399 self.set_active_event_marker(marker)
1401 def get_active_event_marker(self):
1402 return self.active_event_marker
1404 def get_active_event(self):
1405 m = self.get_active_event_marker()
1406 if m is not None:
1407 return m.get_event()
1408 else:
1409 return None
1411 def get_active_markers(self):
1412 emarker = self.get_active_event_marker()
1413 if emarker is None:
1414 return None, []
1416 else:
1417 ev = emarker.get_event()
1418 pmarkers = [
1419 m for m in self.markers
1420 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1422 return emarker, pmarkers
1424 def set_origin(self, location):
1425 for station in self.stations.values():
1426 station.set_event_relative_data(
1427 location,
1428 distance_3d=self.menuitem_distances_3d.isChecked())
1430 self.sortingmode_change()
1432 def distances_3d_changed(self):
1433 ignore = self.menuitem_distances_3d.isChecked()
1434 self.set_event_marker_as_origin(ignore)
1436 def iter_snuffling_modules(self):
1437 pjoin = os.path.join
1438 for path in self.snuffling_paths:
1440 if not os.path.isdir(path):
1441 os.mkdir(path)
1443 for entry in os.listdir(path):
1444 directory = path
1445 fn = entry
1446 d = pjoin(path, entry)
1447 if os.path.isdir(d):
1448 directory = d
1449 if os.path.isfile(
1450 os.path.join(directory, 'snuffling.py')):
1451 fn = 'snuffling.py'
1453 if not fn.endswith('.py'):
1454 continue
1456 name = fn[:-3]
1458 if (directory, name) not in self.snuffling_modules:
1459 self.snuffling_modules[directory, name] = \
1460 pyrocko.gui.snuffling.SnufflingModule(
1461 directory, name, self)
1463 yield self.snuffling_modules[directory, name]
1465 def setup_snufflings(self):
1466 # user snufflings
1467 for mod in self.iter_snuffling_modules():
1468 try:
1469 mod.load_if_needed()
1470 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1471 logger.warning('Snuffling module "%s" is broken' % e)
1473 # load the default snufflings on first run
1474 if self.default_snufflings is None:
1475 self.default_snufflings = pyrocko.gui\
1476 .snufflings.__snufflings__()
1477 for snuffling in self.default_snufflings:
1478 self.add_snuffling(snuffling)
1480 def set_panel_parent(self, panel_parent):
1481 self.panel_parent = panel_parent
1483 def get_panel_parent(self):
1484 return self.panel_parent
1486 def add_snuffling(self, snuffling, reloaded=False):
1487 logger.debug('Adding snuffling %s' % snuffling.get_name())
1488 snuffling.init_gui(
1489 self, self.get_panel_parent(), self, reloaded=reloaded)
1490 self.snufflings.append(snuffling)
1491 self.update()
1493 def remove_snuffling(self, snuffling):
1494 snuffling.delete_gui()
1495 self.update()
1496 self.snufflings.remove(snuffling)
1497 snuffling.pre_destroy()
1499 def add_snuffling_menuitem(self, item):
1500 self.snufflings_menu.addAction(item)
1501 item.setParent(self.snufflings_menu)
1502 sort_actions(self.snufflings_menu)
1504 def remove_snuffling_menuitem(self, item):
1505 self.snufflings_menu.removeAction(item)
1507 def add_snuffling_help_menuitem(self, item):
1508 self.snuffling_help.addAction(item)
1509 item.setParent(self.snuffling_help)
1510 sort_actions(self.snuffling_help)
1512 def remove_snuffling_help_menuitem(self, item):
1513 self.snuffling_help.removeAction(item)
1515 def add_panel_toggler(self, item):
1516 self.toggle_panel_menu.addAction(item)
1517 item.setParent(self.toggle_panel_menu)
1518 sort_actions(self.toggle_panel_menu)
1520 def remove_panel_toggler(self, item):
1521 self.toggle_panel_menu.removeAction(item)
1523 def load(self, paths, regex=None, format='detect',
1524 cache_dir=None, force_cache=False):
1526 if cache_dir is None:
1527 cache_dir = pyrocko.config.config().cache_dir
1528 if isinstance(paths, str):
1529 paths = [paths]
1531 fns = pyrocko.util.select_files(
1532 paths, selector=None, include=regex, show_progress=False)
1534 if not fns:
1535 return
1537 cache = pyrocko.pile.get_cache(cache_dir)
1539 t = [time.time()]
1541 def update_bar(label, value):
1542 pbs = self.parent().get_progressbars()
1543 if label.lower() == 'looking at files':
1544 label = 'Looking at %i files' % len(fns)
1545 else:
1546 label = 'Scanning %i files' % len(fns)
1548 return pbs.set_status(label, value)
1550 def update_progress(label, i, n):
1551 abort = False
1553 qw.qApp.processEvents()
1554 if n != 0:
1555 perc = i*100/n
1556 else:
1557 perc = 100
1558 abort |= update_bar(label, perc)
1559 abort |= self.window().is_closing()
1561 tnow = time.time()
1562 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1563 self.update()
1564 t[0] = tnow
1566 return abort
1568 self.automatic_updates = False
1570 self.pile.load_files(
1571 sorted(fns),
1572 filename_attributes=regex,
1573 cache=cache,
1574 fileformat=format,
1575 show_progress=False,
1576 update_progress=update_progress)
1578 self.automatic_updates = True
1579 self.update()
1581 def load_queued(self):
1582 if not self._paths_to_load:
1583 return
1584 paths = self._paths_to_load
1585 self._paths_to_load = []
1586 self.load(paths)
1588 def load_soon(self, paths):
1589 self._paths_to_load.extend(paths)
1590 qc.QTimer.singleShot(200, self.load_queued)
1592 def open_waveforms(self):
1593 caption = 'Select one or more files to open'
1595 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1596 self, caption, options=qfiledialog_options))
1598 if fns:
1599 self.load(list(str(fn) for fn in fns))
1601 def open_waveform_directory(self):
1602 caption = 'Select directory to scan for waveform files'
1604 dn = qw.QFileDialog.getExistingDirectory(
1605 self, caption, options=qfiledialog_options)
1607 if dn:
1608 self.load([str(dn)])
1610 def open_stations(self, fns=None):
1611 caption = 'Select one or more Pyrocko station files to open'
1613 if not fns:
1614 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1615 self, caption, options=qfiledialog_options))
1617 try:
1618 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1619 for stat in stations:
1620 self.add_stations(stat)
1622 except Exception as e:
1623 self.fail('Failed to read station file: %s' % str(e))
1625 def open_stations_xml(self, fns=None):
1626 from pyrocko.io import stationxml
1628 caption = 'Select one or more StationXML files'
1629 if not fns:
1630 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1631 self, caption, options=qfiledialog_options,
1632 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1633 ';;All files (*)'))
1635 try:
1636 stations = [
1637 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1638 for x in fns]
1640 for stat in stations:
1641 self.add_stations(stat)
1643 except Exception as e:
1644 self.fail('Failed to read StationXML file: %s' % str(e))
1646 def add_traces(self, traces):
1647 if traces:
1648 mtf = pyrocko.pile.MemTracesFile(None, traces)
1649 self.pile.add_file(mtf)
1650 ticket = (self.pile, mtf)
1651 return ticket
1652 else:
1653 return (None, None)
1655 def release_data(self, tickets):
1656 for ticket in tickets:
1657 pile, mtf = ticket
1658 if pile is not None:
1659 pile.remove_file(mtf)
1661 def periodical(self):
1662 if self.menuitem_watch.isChecked():
1663 if self.pile.reload_modified():
1664 self.update()
1666 def get_pile(self):
1667 return self.pile
1669 def pile_changed(self, what):
1670 self.pile_has_changed = True
1671 self.pile_has_changed_signal.emit()
1672 if self.automatic_updates:
1673 self.update()
1675 def set_gathering(self, gather=None, color=None):
1677 if gather is None:
1678 def gather_func(tr):
1679 return tr.nslc_id
1681 gather = (0, 1, 2, 3)
1683 else:
1684 def gather_func(tr):
1685 return (
1686 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1688 if color is None:
1689 def color(tr):
1690 return tr.location
1692 self.gather = gather_func
1693 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1695 self.color_gather = color
1696 self.color_keys = self.pile.gather_keys(color)
1697 previous_ntracks = self.ntracks
1698 self.set_ntracks(len(keys))
1700 if self.shown_tracks_range is None or \
1701 previous_ntracks == 0 or \
1702 self.show_all:
1704 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1705 key_at_top = None
1706 n = high-low
1708 else:
1709 low, high = self.shown_tracks_range
1710 key_at_top = self.track_keys[low]
1711 n = high-low
1713 self.track_keys = sorted(keys)
1715 track_patterns = []
1716 for k in self.track_keys:
1717 pat = ['*', '*', '*', '*']
1718 for i, j in enumerate(gather):
1719 pat[j] = k[-len(gather)+i]
1721 track_patterns.append(pat)
1723 self.track_patterns = track_patterns
1725 if key_at_top is not None:
1726 try:
1727 ind = self.track_keys.index(key_at_top)
1728 low = ind
1729 high = low+n
1730 except Exception:
1731 pass
1733 self.set_tracks_range((low, high))
1735 self.key_to_row = dict(
1736 [(key, i) for (i, key) in enumerate(self.track_keys)])
1738 def inrange(x, r):
1739 return r[0] <= x and x < r[1]
1741 def trace_selector(trace):
1742 gt = self.gather(trace)
1743 return (
1744 gt in self.key_to_row and
1745 inrange(self.key_to_row[gt], self.shown_tracks_range))
1747 if self.trace_filter is not None:
1748 self.trace_selector = lambda x: \
1749 self.trace_filter(x) and trace_selector(x)
1750 else:
1751 self.trace_selector = trace_selector
1753 if self.tmin == working_system_time_range[0] and \
1754 self.tmax == working_system_time_range[1] or \
1755 self.show_all:
1757 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1758 if tmin is not None and tmax is not None:
1759 tlen = (tmax - tmin)
1760 tpad = tlen * 5./self.width()
1761 self.set_time_range(tmin-tpad, tmax+tpad)
1763 def set_time_range(self, tmin, tmax):
1764 if tmin is None:
1765 tmin = initial_time_range[0]
1767 if tmax is None:
1768 tmax = initial_time_range[1]
1770 if tmin > tmax:
1771 tmin, tmax = tmax, tmin
1773 if tmin == tmax:
1774 tmin -= 1.
1775 tmax += 1.
1777 tmin = max(working_system_time_range[0], tmin)
1778 tmax = min(working_system_time_range[1], tmax)
1780 min_deltat = self.content_deltat_range()[0]
1781 if (tmax - tmin < min_deltat):
1782 m = (tmin + tmax) / 2.
1783 tmin = m - min_deltat/2.
1784 tmax = m + min_deltat/2.
1786 self.time_projection.set_in_range(tmin, tmax)
1787 self.tmin, self.tmax = tmin, tmax
1789 def get_time_range(self):
1790 return self.tmin, self.tmax
1792 def ypart(self, y):
1793 if y < self.ax_height:
1794 return -1
1795 elif y > self.height()-self.ax_height:
1796 return 1
1797 else:
1798 return 0
1800 def time_fractional_digits(self):
1801 min_deltat = self.content_deltat_range()[0]
1802 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1804 def write_markers(self, fn=None):
1805 caption = "Choose a file name to write markers"
1806 if not fn:
1807 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1808 self, caption, options=qfiledialog_options))
1809 if fn:
1810 try:
1811 Marker.save_markers(
1812 self.markers, fn,
1813 fdigits=self.time_fractional_digits())
1815 except Exception as e:
1816 self.fail('Failed to write marker file: %s' % str(e))
1818 def write_selected_markers(self, fn=None):
1819 caption = "Choose a file name to write selected markers"
1820 if not fn:
1821 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1822 self, caption, options=qfiledialog_options))
1823 if fn:
1824 try:
1825 Marker.save_markers(
1826 self.iter_selected_markers(),
1827 fn,
1828 fdigits=self.time_fractional_digits())
1830 except Exception as e:
1831 self.fail('Failed to write marker file: %s' % str(e))
1833 def read_events(self, fn=None):
1834 '''
1835 Open QFileDialog to open, read and add
1836 :py:class:`pyrocko.model.Event` instances and their marker
1837 representation to the pile viewer.
1838 '''
1839 caption = "Selet one or more files to open"
1840 if not fn:
1841 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1842 self, caption, options=qfiledialog_options))
1843 if fn:
1844 try:
1845 self.add_events(pyrocko.model.load_events(fn))
1846 self.associate_phases_to_events()
1848 except Exception as e:
1849 self.fail('Failed to read event file: %s' % str(e))
1851 def read_markers(self, fn=None):
1852 '''
1853 Open QFileDialog to open, read and add markers to the pile viewer.
1854 '''
1855 caption = "Selet one or more marker files to open"
1856 if not fn:
1857 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1858 self, caption, options=qfiledialog_options))
1859 if fn:
1860 try:
1861 self.add_markers(Marker.load_markers(fn))
1862 self.associate_phases_to_events()
1864 except Exception as e:
1865 self.fail('Failed to read marker file: %s' % str(e))
1867 def associate_phases_to_events(self):
1868 associate_phases_to_events(self.markers)
1870 def add_marker(self, marker):
1871 # need index to inform QAbstactTableModel about upcoming change,
1872 # but have to restore current state in order to not cause problems
1873 self.markers.insert(marker)
1874 i = self.markers.remove(marker)
1876 self.begin_markers_add.emit(i, i)
1877 self.markers.insert(marker)
1878 self.end_markers_add.emit()
1879 self.markers_deltat_max = max(
1880 self.markers_deltat_max, marker.tmax - marker.tmin)
1882 def add_markers(self, markers):
1883 if not self.markers:
1884 self.begin_markers_add.emit(0, len(markers) - 1)
1885 self.markers.insert_many(markers)
1886 self.end_markers_add.emit()
1887 self.update_markers_deltat_max()
1888 else:
1889 for marker in markers:
1890 self.add_marker(marker)
1892 def update_markers_deltat_max(self):
1893 if self.markers:
1894 self.markers_deltat_max = max(
1895 marker.tmax - marker.tmin for marker in self.markers)
1897 def remove_marker(self, marker):
1898 '''
1899 Remove a ``marker`` from the :py:class:`PileViewer`.
1901 :param marker: :py:class:`Marker` (or subclass) instance
1902 '''
1904 if marker is self.active_event_marker:
1905 self.deactivate_event_marker()
1907 try:
1908 i = self.markers.index(marker)
1909 self.begin_markers_remove.emit(i, i)
1910 self.markers.remove_at(i)
1911 self.end_markers_remove.emit()
1912 except ValueError:
1913 pass
1915 def remove_markers(self, markers):
1916 '''
1917 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1919 :param markers: list of :py:class:`Marker` (or subclass)
1920 instances
1921 '''
1923 if markers is self.markers:
1924 markers = list(markers)
1926 for marker in markers:
1927 self.remove_marker(marker)
1929 self.update_markers_deltat_max()
1931 def remove_selected_markers(self):
1932 def delete_segment(istart, iend):
1933 self.begin_markers_remove.emit(istart, iend-1)
1934 for _ in range(iend - istart):
1935 self.markers.remove_at(istart)
1937 self.end_markers_remove.emit()
1939 istart = None
1940 ipos = 0
1941 markers = self.markers
1942 nmarkers = len(self.markers)
1943 while ipos < nmarkers:
1944 marker = markers[ipos]
1945 if marker.is_selected():
1946 if marker is self.active_event_marker:
1947 self.deactivate_event_marker()
1949 if istart is None:
1950 istart = ipos
1951 else:
1952 if istart is not None:
1953 delete_segment(istart, ipos)
1954 nmarkers -= ipos - istart
1955 ipos = istart - 1
1956 istart = None
1958 ipos += 1
1960 if istart is not None:
1961 delete_segment(istart, ipos)
1963 self.update_markers_deltat_max()
1965 def selected_markers(self):
1966 return [marker for marker in self.markers if marker.is_selected()]
1968 def iter_selected_markers(self):
1969 for marker in self.markers:
1970 if marker.is_selected():
1971 yield marker
1973 def get_markers(self):
1974 return self.markers
1976 def mousePressEvent(self, mouse_ev):
1977 self.show_all = False
1978 point = self.mapFromGlobal(mouse_ev.globalPos())
1980 if mouse_ev.button() == qc.Qt.LeftButton:
1981 marker = self.marker_under_cursor(point.x(), point.y())
1982 if self.picking:
1983 if self.picking_down is None:
1984 self.picking_down = (
1985 self.time_projection.rev(mouse_ev.x()),
1986 mouse_ev.y())
1988 elif marker is not None:
1989 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1990 self.deselect_all()
1991 marker.selected = True
1992 self.emit_selected_markers()
1993 self.update()
1994 else:
1995 self.track_start = mouse_ev.x(), mouse_ev.y()
1996 self.track_trange = self.tmin, self.tmax
1998 if mouse_ev.button() == qc.Qt.RightButton \
1999 and isinstance(self.menu, qw.QMenu):
2000 self.menu.exec_(qg.QCursor.pos())
2001 self.update_status()
2003 def mouseReleaseEvent(self, mouse_ev):
2004 if self.ignore_releases:
2005 self.ignore_releases -= 1
2006 return
2008 if self.picking:
2009 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2010 self.emit_selected_markers()
2012 if self.track_start:
2013 self.update()
2015 self.track_start = None
2016 self.track_trange = None
2017 self.update_status()
2019 def mouseDoubleClickEvent(self, mouse_ev):
2020 self.show_all = False
2021 self.start_picking(None)
2022 self.ignore_releases = 1
2024 def mouseMoveEvent(self, mouse_ev):
2025 self.setUpdatesEnabled(False)
2026 point = self.mapFromGlobal(mouse_ev.globalPos())
2028 if self.picking:
2029 self.update_picking(point.x(), point.y())
2031 elif self.track_start is not None:
2032 x0, y0 = self.track_start
2033 dx = (point.x() - x0)/float(self.width())
2034 dy = (point.y() - y0)/float(self.height())
2035 if self.ypart(y0) == 1:
2036 dy = 0
2038 tmin0, tmax0 = self.track_trange
2040 scale = math.exp(-dy*5.)
2041 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2042 frac = x0/float(self.width())
2043 dt = dx*(tmax0-tmin0)*scale
2045 self.interrupt_following()
2046 self.set_time_range(
2047 tmin0 - dt - dtr*frac,
2048 tmax0 - dt + dtr*(1.-frac))
2050 self.update()
2051 else:
2052 self.hoovering(point.x(), point.y())
2054 self.update_status()
2056 def nslc_ids_under_cursor(self, x, y):
2057 ftrack = self.track_to_screen.rev(y)
2058 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2059 return nslc_ids
2061 def marker_under_cursor(self, x, y):
2062 mouset = self.time_projection.rev(x)
2063 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2064 relevant_nslc_ids = None
2065 for marker in self.markers:
2066 if marker.kind not in self.visible_marker_kinds:
2067 continue
2069 if (abs(mouset-marker.tmin) < deltat or
2070 abs(mouset-marker.tmax) < deltat):
2072 if relevant_nslc_ids is None:
2073 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2075 marker_nslc_ids = marker.get_nslc_ids()
2076 if not marker_nslc_ids:
2077 return marker
2079 for nslc_id in marker_nslc_ids:
2080 if nslc_id in relevant_nslc_ids:
2081 return marker
2083 def hoovering(self, x, y):
2084 mouset = self.time_projection.rev(x)
2085 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2086 needupdate = False
2087 haveone = False
2088 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2089 for marker in self.markers:
2090 if marker.kind not in self.visible_marker_kinds:
2091 continue
2093 state = abs(mouset-marker.tmin) < deltat or \
2094 abs(mouset-marker.tmax) < deltat and not haveone
2096 if state:
2097 xstate = False
2099 marker_nslc_ids = marker.get_nslc_ids()
2100 if not marker_nslc_ids:
2101 xstate = True
2103 for nslc in relevant_nslc_ids:
2104 if marker.match_nslc(nslc):
2105 xstate = True
2107 state = xstate
2109 if state:
2110 haveone = True
2111 oldstate = marker.is_alerted()
2112 if oldstate != state:
2113 needupdate = True
2114 marker.set_alerted(state)
2115 if state:
2116 self.message = marker.hoover_message()
2118 if not haveone:
2119 self.message = None
2121 if needupdate:
2122 self.update()
2124 def event(self, event):
2125 if event.type() == qc.QEvent.KeyPress:
2126 self.keyPressEvent(event)
2127 return True
2128 else:
2129 return base.event(self, event)
2131 def keyPressEvent(self, key_event):
2132 self.show_all = False
2133 dt = self.tmax - self.tmin
2134 tmid = (self.tmin + self.tmax) / 2.
2136 key = key_event.key()
2137 try:
2138 keytext = str(key_event.text())
2139 except UnicodeEncodeError:
2140 return
2142 if key == qc.Qt.Key_Space:
2143 self.interrupt_following()
2144 self.set_time_range(self.tmin+dt, self.tmax+dt)
2146 elif key == qc.Qt.Key_Up:
2147 for m in self.selected_markers():
2148 if isinstance(m, PhaseMarker):
2149 if key_event.modifiers() & qc.Qt.ShiftModifier:
2150 p = 0
2151 else:
2152 p = 1 if m.get_polarity() != 1 else None
2153 m.set_polarity(p)
2155 elif key == qc.Qt.Key_Down:
2156 for m in self.selected_markers():
2157 if isinstance(m, PhaseMarker):
2158 if key_event.modifiers() & qc.Qt.ShiftModifier:
2159 p = 0
2160 else:
2161 p = -1 if m.get_polarity() != -1 else None
2162 m.set_polarity(p)
2164 elif key == qc.Qt.Key_B:
2165 dt = self.tmax - self.tmin
2166 self.interrupt_following()
2167 self.set_time_range(self.tmin-dt, self.tmax-dt)
2169 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2170 self.interrupt_following()
2172 tgo = None
2174 class TraceDummy(object):
2175 def __init__(self, marker):
2176 self._marker = marker
2178 @property
2179 def nslc_id(self):
2180 return self._marker.one_nslc()
2182 def marker_to_itrack(marker):
2183 try:
2184 return self.key_to_row.get(
2185 self.gather(TraceDummy(marker)), -1)
2187 except MarkerOneNSLCRequired:
2188 return -1
2190 emarker, pmarkers = self.get_active_markers()
2191 pmarkers = [
2192 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2193 pmarkers.sort(key=lambda m: (
2194 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2196 if key == qc.Qt.Key_Backtab:
2197 pmarkers.reverse()
2199 smarkers = self.selected_markers()
2200 iselected = []
2201 for sm in smarkers:
2202 try:
2203 iselected.append(pmarkers.index(sm))
2204 except ValueError:
2205 pass
2207 if iselected:
2208 icurrent = max(iselected) + 1
2209 else:
2210 icurrent = 0
2212 if icurrent < len(pmarkers):
2213 self.deselect_all()
2214 cmarker = pmarkers[icurrent]
2215 cmarker.selected = True
2216 tgo = cmarker.tmin
2217 if not self.tmin < tgo < self.tmax:
2218 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2220 itrack = marker_to_itrack(cmarker)
2221 if itrack != -1:
2222 if itrack < self.shown_tracks_range[0]:
2223 self.scroll_tracks(
2224 - (self.shown_tracks_range[0] - itrack))
2225 elif self.shown_tracks_range[1] <= itrack:
2226 self.scroll_tracks(
2227 itrack - self.shown_tracks_range[1]+1)
2229 if itrack not in self.track_to_nslc_ids:
2230 self.go_to_selection()
2232 elif keytext in ('p', 'n', 'P', 'N'):
2233 smarkers = self.selected_markers()
2234 tgo = None
2235 dir = str(keytext)
2236 if smarkers:
2237 tmid = smarkers[0].tmin
2238 for smarker in smarkers:
2239 if dir == 'n':
2240 tmid = max(smarker.tmin, tmid)
2241 else:
2242 tmid = min(smarker.tmin, tmid)
2244 tgo = tmid
2246 if dir.lower() == 'n':
2247 for marker in sorted(
2248 self.markers,
2249 key=operator.attrgetter('tmin')):
2251 t = marker.tmin
2252 if t > tmid and \
2253 marker.kind in self.visible_marker_kinds and \
2254 (dir == 'n' or
2255 isinstance(marker, EventMarker)):
2257 self.deselect_all()
2258 marker.selected = True
2259 tgo = t
2260 break
2261 else:
2262 for marker in sorted(
2263 self.markers,
2264 key=operator.attrgetter('tmin'),
2265 reverse=True):
2267 t = marker.tmin
2268 if t < tmid and \
2269 marker.kind in self.visible_marker_kinds and \
2270 (dir == 'p' or
2271 isinstance(marker, EventMarker)):
2272 self.deselect_all()
2273 marker.selected = True
2274 tgo = t
2275 break
2277 if tgo is not None:
2278 self.interrupt_following()
2279 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2281 elif keytext == 'r':
2282 if self.pile.reload_modified():
2283 self.reloaded = True
2285 elif keytext == 'R':
2286 self.setup_snufflings()
2288 elif key == qc.Qt.Key_Backspace:
2289 self.remove_selected_markers()
2291 elif keytext == 'a':
2292 for marker in self.markers:
2293 if ((self.tmin <= marker.tmin <= self.tmax or
2294 self.tmin <= marker.tmax <= self.tmax) and
2295 marker.kind in self.visible_marker_kinds):
2296 marker.selected = True
2297 else:
2298 marker.selected = False
2300 elif keytext == 'A':
2301 for marker in self.markers:
2302 if marker.kind in self.visible_marker_kinds:
2303 marker.selected = True
2305 elif keytext == 'd':
2306 self.deselect_all()
2308 elif keytext == 'E':
2309 self.deactivate_event_marker()
2311 elif keytext == 'e':
2312 markers = self.selected_markers()
2313 event_markers_in_spe = [
2314 marker for marker in markers
2315 if not isinstance(marker, PhaseMarker)]
2317 phase_markers = [
2318 marker for marker in markers
2319 if isinstance(marker, PhaseMarker)]
2321 if len(event_markers_in_spe) == 1:
2322 event_marker = event_markers_in_spe[0]
2323 if not isinstance(event_marker, EventMarker):
2324 nslcs = list(event_marker.nslc_ids)
2325 lat, lon = 0.0, 0.0
2326 old = self.get_active_event()
2327 if len(nslcs) == 1:
2328 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2329 elif old is not None:
2330 lat, lon = old.lat, old.lon
2332 event_marker.convert_to_event_marker(lat, lon)
2334 self.set_active_event_marker(event_marker)
2335 event = event_marker.get_event()
2336 for marker in phase_markers:
2337 marker.set_event(event)
2339 else:
2340 for marker in event_markers_in_spe:
2341 marker.convert_to_event_marker()
2343 elif keytext in ('0', '1', '2', '3', '4', '5'):
2344 for marker in self.selected_markers():
2345 marker.set_kind(int(keytext))
2346 self.emit_selected_markers()
2348 elif key in fkey_map:
2349 self.handle_fkeys(key)
2351 elif key == qc.Qt.Key_Escape:
2352 if self.picking:
2353 self.stop_picking(0, 0, abort=True)
2355 elif key == qc.Qt.Key_PageDown:
2356 self.scroll_tracks(
2357 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2359 elif key == qc.Qt.Key_PageUp:
2360 self.scroll_tracks(
2361 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2363 elif key == qc.Qt.Key_Plus:
2364 self.zoom_tracks(0., 1.)
2366 elif key == qc.Qt.Key_Minus:
2367 self.zoom_tracks(0., -1.)
2369 elif key == qc.Qt.Key_Equal:
2370 ntracks_shown = self.shown_tracks_range[1] - \
2371 self.shown_tracks_range[0]
2372 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2373 self.zoom_tracks(0., dtracks)
2375 elif key == qc.Qt.Key_Colon:
2376 self.want_input.emit()
2378 elif keytext == 'f':
2379 self.toggle_fullscreen()
2381 elif keytext == 'g':
2382 self.go_to_selection()
2384 elif keytext == 'G':
2385 self.go_to_selection(tight=True)
2387 elif keytext == 'm':
2388 self.toggle_marker_editor()
2390 elif keytext == 'c':
2391 self.toggle_main_controls()
2393 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2394 dir = 1
2395 amount = 1
2396 if key_event.key() == qc.Qt.Key_Left:
2397 dir = -1
2398 if key_event.modifiers() & qc.Qt.ShiftModifier:
2399 amount = 10
2400 self.nudge_selected_markers(dir*amount)
2401 else:
2402 super().keyPressEvent(key_event)
2404 if keytext != '' and keytext in 'degaApPnN':
2405 self.emit_selected_markers()
2407 self.update()
2408 self.update_status()
2410 def handle_fkeys(self, key):
2411 self.set_phase_kind(
2412 self.selected_markers(),
2413 fkey_map[key] + 1)
2414 self.emit_selected_markers()
2416 def emit_selected_markers(self):
2417 ibounds = []
2418 last_selected = False
2419 for imarker, marker in enumerate(self.markers):
2420 this_selected = marker.is_selected()
2421 if this_selected != last_selected:
2422 ibounds.append(imarker)
2424 last_selected = this_selected
2426 if last_selected:
2427 ibounds.append(len(self.markers))
2429 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2430 self.n_selected_markers = sum(
2431 chunk[1] - chunk[0] for chunk in chunks)
2432 self.marker_selection_changed.emit(chunks)
2434 def toggle_marker_editor(self):
2435 self.panel_parent.toggle_marker_editor()
2437 def toggle_main_controls(self):
2438 self.panel_parent.toggle_main_controls()
2440 def nudge_selected_markers(self, npixels):
2441 a, b = self.time_projection.ur
2442 c, d = self.time_projection.xr
2443 for marker in self.selected_markers():
2444 if not isinstance(marker, EventMarker):
2445 marker.tmin += npixels * (d-c)/b
2446 marker.tmax += npixels * (d-c)/b
2448 def toggle_fullscreen(self):
2449 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2450 self.window().windowState() & qc.Qt.WindowMaximized:
2451 self.window().showNormal()
2452 else:
2453 if macosx:
2454 self.window().showMaximized()
2455 else:
2456 self.window().showFullScreen()
2458 def about(self):
2459 fn = pyrocko.util.data_file('snuffler.png')
2460 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2461 txt = f.read()
2462 label = qw.QLabel(txt % {'logo': fn})
2463 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2464 self.show_doc('About', [label], target='tab')
2466 def help(self):
2467 class MyScrollArea(qw.QScrollArea):
2469 def sizeHint(self):
2470 s = qc.QSize()
2471 s.setWidth(self.widget().sizeHint().width())
2472 s.setHeight(self.widget().sizeHint().height())
2473 return s
2475 with open(pyrocko.util.data_file(
2476 'snuffler_help.html')) as f:
2477 hcheat = qw.QLabel(f.read())
2479 with open(pyrocko.util.data_file(
2480 'snuffler_help_epilog.html')) as f:
2481 hepilog = qw.QLabel(f.read())
2483 for h in [hcheat, hepilog]:
2484 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2485 h.setWordWrap(True)
2487 self.show_doc('Help', [hcheat, hepilog], target='panel')
2489 def show_doc(self, name, labels, target='panel'):
2490 scroller = qw.QScrollArea()
2491 frame = qw.QFrame(scroller)
2492 frame.setLineWidth(0)
2493 layout = qw.QVBoxLayout()
2494 layout.setContentsMargins(0, 0, 0, 0)
2495 layout.setSpacing(0)
2496 frame.setLayout(layout)
2497 scroller.setWidget(frame)
2498 scroller.setWidgetResizable(True)
2499 frame.setBackgroundRole(qg.QPalette.Base)
2500 for h in labels:
2501 h.setParent(frame)
2502 h.setMargin(3)
2503 h.setTextInteractionFlags(
2504 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2505 h.setBackgroundRole(qg.QPalette.Base)
2506 layout.addWidget(h)
2507 h.linkActivated.connect(
2508 self.open_link)
2510 if self.panel_parent is not None:
2511 if target == 'panel':
2512 self.panel_parent.add_panel(
2513 name, scroller, True, volatile=False)
2514 else:
2515 self.panel_parent.add_tab(name, scroller)
2517 def open_link(self, link):
2518 qg.QDesktopServices.openUrl(qc.QUrl(link))
2520 def wheelEvent(self, wheel_event):
2521 if use_pyqt5:
2522 self.wheel_pos += wheel_event.angleDelta().y()
2523 else:
2524 self.wheel_pos += wheel_event.delta()
2526 n = self.wheel_pos // 120
2527 self.wheel_pos = self.wheel_pos % 120
2528 if n == 0:
2529 return
2531 amount = max(
2532 1.,
2533 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2534 wdelta = amount * n
2536 trmin, trmax = self.track_to_screen.get_in_range()
2537 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2538 / (trmax-trmin)
2540 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2541 self.zoom_tracks(anchor, wdelta)
2542 else:
2543 self.scroll_tracks(-wdelta)
2545 def dragEnterEvent(self, event):
2546 if event.mimeData().hasUrls():
2547 if any(url.toLocalFile() for url in event.mimeData().urls()):
2548 event.setDropAction(qc.Qt.LinkAction)
2549 event.accept()
2551 def dropEvent(self, event):
2552 if event.mimeData().hasUrls():
2553 paths = list(
2554 str(url.toLocalFile()) for url in event.mimeData().urls())
2555 event.acceptProposedAction()
2556 self.load(paths)
2558 def get_phase_name(self, kind):
2559 return self.config.get_phase_name(kind)
2561 def set_phase_kind(self, markers, kind):
2562 phasename = self.get_phase_name(kind)
2564 for marker in markers:
2565 if isinstance(marker, PhaseMarker):
2566 if kind == 10:
2567 marker.convert_to_marker()
2568 else:
2569 marker.set_phasename(phasename)
2570 marker.set_event(self.get_active_event())
2572 elif isinstance(marker, EventMarker):
2573 pass
2575 else:
2576 if kind != 10:
2577 event = self.get_active_event()
2578 marker.convert_to_phase_marker(
2579 event, phasename, None, False)
2581 def set_ntracks(self, ntracks):
2582 if self.ntracks != ntracks:
2583 self.ntracks = ntracks
2584 if self.shown_tracks_range is not None:
2585 l, h = self.shown_tracks_range
2586 else:
2587 l, h = 0, self.ntracks
2589 self.tracks_range_changed.emit(self.ntracks, l, h)
2591 def set_tracks_range(self, range, start=None):
2593 low, high = range
2594 low = min(self.ntracks-1, low)
2595 high = min(self.ntracks, high)
2596 low = max(0, low)
2597 high = max(1, high)
2599 if start is None:
2600 start = float(low)
2602 if self.shown_tracks_range != (low, high):
2603 self.shown_tracks_range = low, high
2604 self.shown_tracks_start = start
2606 self.tracks_range_changed.emit(self.ntracks, low, high)
2608 def scroll_tracks(self, shift):
2609 shown = self.shown_tracks_range
2610 shiftmin = -shown[0]
2611 shiftmax = self.ntracks-shown[1]
2612 shift = max(shiftmin, shift)
2613 shift = min(shiftmax, shift)
2614 shown = shown[0] + shift, shown[1] + shift
2616 self.set_tracks_range((int(shown[0]), int(shown[1])))
2618 self.update()
2620 def zoom_tracks(self, anchor, delta):
2621 ntracks_shown = self.shown_tracks_range[1] \
2622 - self.shown_tracks_range[0]
2624 if (ntracks_shown == 1 and delta <= 0) or \
2625 (ntracks_shown == self.ntracks and delta >= 0):
2626 return
2628 ntracks_shown += int(round(delta))
2629 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2631 u = self.shown_tracks_start
2632 nu = max(0., u-anchor*delta)
2633 nv = nu + ntracks_shown
2634 if nv > self.ntracks:
2635 nu -= nv - self.ntracks
2636 nv -= nv - self.ntracks
2638 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2640 self.ntracks_shown_max = self.shown_tracks_range[1] \
2641 - self.shown_tracks_range[0]
2643 self.update()
2645 def content_time_range(self):
2646 pile = self.get_pile()
2647 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2648 if tmin is None:
2649 tmin = initial_time_range[0]
2650 if tmax is None:
2651 tmax = initial_time_range[1]
2653 return tmin, tmax
2655 def content_deltat_range(self):
2656 pile = self.get_pile()
2658 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2660 if deltatmin is None:
2661 deltatmin = 0.001
2663 if deltatmax is None:
2664 deltatmax = 1000.0
2666 return deltatmin, deltatmax
2668 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2669 if tmax < tmin:
2670 tmin, tmax = tmax, tmin
2672 deltatmin = self.content_deltat_range()[0]
2673 dt = deltatmin * self.visible_length * 0.95
2675 if dt == 0.0:
2676 dt = 1.0
2678 if tight:
2679 if tmax != tmin:
2680 dtm = tmax - tmin
2681 tmin -= dtm*0.1
2682 tmax += dtm*0.1
2683 return tmin, tmax
2684 else:
2685 tcenter = (tmin + tmax) / 2.
2686 tmin = tcenter - 0.5*dt
2687 tmax = tcenter + 0.5*dt
2688 return tmin, tmax
2690 if tmax-tmin < dt:
2691 vmin, vmax = self.get_time_range()
2692 dt = min(vmax - vmin, dt)
2694 tcenter = (tmin+tmax)/2.
2695 etmin, etmax = tmin, tmax
2696 tmin = min(etmin, tcenter - 0.5*dt)
2697 tmax = max(etmax, tcenter + 0.5*dt)
2698 dtm = tmax-tmin
2699 if etmin == tmin:
2700 tmin -= dtm*0.1
2701 if etmax == tmax:
2702 tmax += dtm*0.1
2704 else:
2705 dtm = tmax-tmin
2706 tmin -= dtm*0.1
2707 tmax += dtm*0.1
2709 return tmin, tmax
2711 def go_to_selection(self, tight=False):
2712 markers = self.selected_markers()
2713 if markers:
2714 tmax, tmin = self.content_time_range()
2715 for marker in markers:
2716 tmin = min(tmin, marker.tmin)
2717 tmax = max(tmax, marker.tmax)
2719 else:
2720 if tight:
2721 vmin, vmax = self.get_time_range()
2722 tmin = tmax = (vmin + vmax) / 2.
2723 else:
2724 tmin, tmax = self.content_time_range()
2726 tmin, tmax = self.make_good_looking_time_range(
2727 tmin, tmax, tight=tight)
2729 self.interrupt_following()
2730 self.set_time_range(tmin, tmax)
2731 self.update()
2733 def go_to_time(self, t, tlen=None):
2734 tmax = t
2735 if tlen is not None:
2736 tmax = t+tlen
2737 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2738 self.interrupt_following()
2739 self.set_time_range(tmin, tmax)
2740 self.update()
2742 def go_to_event_by_name(self, name):
2743 for marker in self.markers:
2744 if isinstance(marker, EventMarker):
2745 event = marker.get_event()
2746 if event.name and event.name.lower() == name.lower():
2747 tmin, tmax = self.make_good_looking_time_range(
2748 event.time, event.time)
2750 self.interrupt_following()
2751 self.set_time_range(tmin, tmax)
2753 def printit(self):
2754 from .qt_compat import qprint
2755 printer = qprint.QPrinter()
2756 printer.setOrientation(qprint.QPrinter.Landscape)
2758 dialog = qprint.QPrintDialog(printer, self)
2759 dialog.setWindowTitle('Print')
2761 if dialog.exec_() != qw.QDialog.Accepted:
2762 return
2764 painter = qg.QPainter()
2765 painter.begin(printer)
2766 page = printer.pageRect()
2767 self.drawit(
2768 painter, printmode=False, w=page.width(), h=page.height())
2770 painter.end()
2772 def savesvg(self, fn=None):
2774 if not fn:
2775 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
2776 self,
2777 'Save as SVG|PNG',
2778 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2779 'SVG|PNG (*.svg *.png)',
2780 options=qfiledialog_options))
2782 if fn == '':
2783 return
2785 fn = str(fn)
2787 if fn.lower().endswith('.svg'):
2788 try:
2789 w, h = 842, 595
2790 margin = 0.025
2791 m = max(w, h)*margin
2793 generator = qsvg.QSvgGenerator()
2794 generator.setFileName(fn)
2795 generator.setSize(qc.QSize(w, h))
2796 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2798 painter = qg.QPainter()
2799 painter.begin(generator)
2800 self.drawit(painter, printmode=False, w=w, h=h)
2801 painter.end()
2803 except Exception as e:
2804 self.fail('Failed to write SVG file: %s' % str(e))
2806 elif fn.lower().endswith('.png'):
2807 if use_pyqt5:
2808 pixmap = self.grab()
2809 else:
2810 pixmap = qg.QPixmap().grabWidget(self)
2812 try:
2813 pixmap.save(fn)
2815 except Exception as e:
2816 self.fail('Failed to write PNG file: %s' % str(e))
2818 else:
2819 self.fail(
2820 'Unsupported file type: filename must end with ".svg" or '
2821 '".png".')
2823 def paintEvent(self, paint_ev):
2824 '''
2825 Called by QT whenever widget needs to be painted.
2826 '''
2827 painter = qg.QPainter(self)
2829 if self.menuitem_antialias.isChecked():
2830 painter.setRenderHint(qg.QPainter.Antialiasing)
2832 self.drawit(painter)
2834 logger.debug(
2835 'Time spent drawing: '
2836 ' user:%.3f sys:%.3f children_user:%.3f'
2837 ' childred_sys:%.3f elapsed:%.3f' %
2838 (self.timer_draw - self.timer_cutout))
2840 logger.debug(
2841 'Time spent processing:'
2842 ' user:%.3f sys:%.3f children_user:%.3f'
2843 ' childred_sys:%.3f elapsed:%.3f' %
2844 self.timer_cutout.get())
2846 self.time_spent_painting = self.timer_draw.get()[-1]
2847 self.time_last_painted = time.time()
2849 def determine_box_styles(self):
2851 traces = list(self.pile.iter_traces())
2852 traces.sort(key=operator.attrgetter('full_id'))
2853 istyle = 0
2854 trace_styles = {}
2855 for itr, tr in enumerate(traces):
2856 if itr > 0:
2857 other = traces[itr-1]
2858 if not (
2859 other.nslc_id == tr.nslc_id
2860 and other.deltat == tr.deltat
2861 and abs(other.tmax - tr.tmin)
2862 < gap_lap_tolerance*tr.deltat):
2864 istyle += 1
2866 trace_styles[tr.full_id, tr.deltat] = istyle
2868 self.trace_styles = trace_styles
2870 def draw_trace_boxes(self, p, time_projection, track_projections):
2872 for v_projection in track_projections.values():
2873 v_projection.set_in_range(0., 1.)
2875 def selector(x):
2876 return x.overlaps(*time_projection.get_in_range())
2878 if self.trace_filter is not None:
2879 def tselector(x):
2880 return selector(x) and self.trace_filter(x)
2882 else:
2883 tselector = selector
2885 traces = list(self.pile.iter_traces(
2886 group_selector=selector, trace_selector=tselector))
2888 traces.sort(key=operator.attrgetter('full_id'))
2890 def drawbox(itrack, istyle, traces):
2891 v_projection = track_projections[itrack]
2892 dvmin = v_projection(0.)
2893 dvmax = v_projection(1.)
2894 dtmin = time_projection.clipped(traces[0].tmin)
2895 dtmax = time_projection.clipped(traces[-1].tmax)
2897 style = box_styles[istyle % len(box_styles)]
2898 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2899 p.fillRect(rect, style.fill_brush)
2900 p.setPen(style.frame_pen)
2901 p.drawRect(rect)
2903 traces_by_style = {}
2904 for itr, tr in enumerate(traces):
2905 gt = self.gather(tr)
2906 if gt not in self.key_to_row:
2907 continue
2909 itrack = self.key_to_row[gt]
2910 if itrack not in track_projections:
2911 continue
2913 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2915 if len(traces) < 500:
2916 drawbox(itrack, istyle, [tr])
2917 else:
2918 if (itrack, istyle) not in traces_by_style:
2919 traces_by_style[itrack, istyle] = []
2920 traces_by_style[itrack, istyle].append(tr)
2922 for (itrack, istyle), traces in traces_by_style.items():
2923 drawbox(itrack, istyle, traces)
2925 def draw_visible_markers(
2926 self, p, vcenter_projection, primary_pen):
2928 try:
2929 markers = self.markers.with_key_in_limited(
2930 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2932 except pyrocko.pile.TooMany:
2933 tmin = self.markers[0].tmin
2934 tmax = self.markers[-1].tmax
2935 umin_view, umax_view = self.time_projection.get_out_range()
2936 umin = max(umin_view, self.time_projection(tmin))
2937 umax = min(umax_view, self.time_projection(tmax))
2938 v0, _ = vcenter_projection.get_out_range()
2939 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2941 p.save()
2943 pen = qg.QPen(primary_pen)
2944 pen.setWidth(2)
2945 pen.setStyle(qc.Qt.DotLine)
2946 # pat = [5., 3.]
2947 # pen.setDashPattern(pat)
2948 p.setPen(pen)
2950 if self.n_selected_markers == len(self.markers):
2951 s_selected = ' (all selected)'
2952 elif self.n_selected_markers > 0:
2953 s_selected = ' (%i selected)' % self.n_selected_markers
2954 else:
2955 s_selected = ''
2957 draw_label(
2958 p, umin+10., v0-10.,
2959 '%i Markers' % len(self.markers) + s_selected,
2960 label_bg, 'LB')
2962 line = qc.QLineF(umin, v0, umax, v0)
2963 p.drawLine(line)
2964 p.restore()
2966 return
2968 for marker in markers:
2969 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2970 and marker.kind in self.visible_marker_kinds:
2972 marker.draw(
2973 p, self.time_projection, vcenter_projection,
2974 with_label=True)
2976 def get_squirrel(self):
2977 try:
2978 return self.pile._squirrel
2979 except AttributeError:
2980 return None
2982 def draw_coverage(self, p, time_projection, track_projections):
2983 sq = self.get_squirrel()
2984 if sq is None:
2985 return
2987 def drawbox(itrack, tmin, tmax, style):
2988 v_projection = track_projections[itrack]
2989 dvmin = v_projection(0.)
2990 dvmax = v_projection(1.)
2991 dtmin = time_projection.clipped(tmin)
2992 dtmax = time_projection.clipped(tmax)
2994 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2995 p.fillRect(rect, style.fill_brush)
2996 p.setPen(style.frame_pen)
2997 p.drawRect(rect)
2999 pattern_list = []
3000 pattern_to_itrack = {}
3001 for key in self.track_keys:
3002 itrack = self.key_to_row[key]
3003 if itrack not in track_projections:
3004 continue
3006 pattern = self.track_patterns[itrack]
3007 pattern_to_itrack[tuple(pattern)] = itrack
3008 pattern_list.append(pattern)
3010 vmin, vmax = self.get_time_range()
3012 for kind in ['waveform', 'waveform_promise']:
3013 for entry in sq.get_coverage(
3014 kind, vmin, vmax, pattern_list, limit=500):
3015 pattern, codes, deltat, tmin, tmax, cover_data = entry
3016 itrack = pattern_to_itrack[tuple(pattern)]
3018 if cover_data is None:
3019 drawbox(
3020 itrack, tmin, tmax,
3021 box_styles_coverage[kind][0])
3022 else:
3023 t = None
3024 pcount = 0
3025 for tb, count in cover_data:
3026 if t is not None and tb > t:
3027 if pcount > 0:
3028 drawbox(
3029 itrack, t, tb,
3030 box_styles_coverage[kind][
3031 min(len(box_styles_coverage)-1,
3032 pcount)])
3034 t = tb
3035 pcount = count
3037 def drawit(self, p, printmode=False, w=None, h=None):
3038 '''
3039 This performs the actual drawing.
3040 '''
3042 self.timer_draw.start()
3043 show_boxes = self.menuitem_showboxes.isChecked()
3044 sq = self.get_squirrel()
3046 if self.gather is None:
3047 self.set_gathering()
3049 if self.pile_has_changed:
3051 if not self.sortingmode_change_delayed():
3052 self.sortingmode_change()
3054 if show_boxes and sq is None:
3055 self.determine_box_styles()
3057 self.pile_has_changed = False
3059 if h is None:
3060 h = float(self.height())
3061 if w is None:
3062 w = float(self.width())
3064 if printmode:
3065 primary_color = (0, 0, 0)
3066 else:
3067 primary_color = pyrocko.plot.tango_colors['aluminium5']
3069 primary_pen = qg.QPen(qg.QColor(*primary_color))
3071 ax_h = float(self.ax_height)
3073 vbottom_ax_projection = Projection()
3074 vtop_ax_projection = Projection()
3075 vcenter_projection = Projection()
3077 self.time_projection.set_out_range(0., w)
3078 vbottom_ax_projection.set_out_range(h-ax_h, h)
3079 vtop_ax_projection.set_out_range(0., ax_h)
3080 vcenter_projection.set_out_range(ax_h, h-ax_h)
3081 vcenter_projection.set_in_range(0., 1.)
3082 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3084 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3085 track_projections = {}
3086 for i in range(*self.shown_tracks_range):
3087 proj = Projection()
3088 proj.set_out_range(
3089 self.track_to_screen(i+0.05),
3090 self.track_to_screen(i+1.-0.05))
3092 track_projections[i] = proj
3094 if self.tmin > self.tmax:
3095 return
3097 self.time_projection.set_in_range(self.tmin, self.tmax)
3098 vbottom_ax_projection.set_in_range(0, ax_h)
3100 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3102 yscaler = pyrocko.plot.AutoScaler()
3104 p.setPen(primary_pen)
3106 font = qg.QFont()
3107 font.setBold(True)
3109 axannotfont = qg.QFont()
3110 axannotfont.setBold(True)
3111 axannotfont.setPointSize(8)
3113 processed_traces = self.prepare_cutout2(
3114 self.tmin, self.tmax,
3115 trace_selector=self.trace_selector,
3116 degap=self.menuitem_degap.isChecked(),
3117 demean=self.menuitem_demean.isChecked())
3119 if not printmode and show_boxes:
3120 if (self.view_mode is ViewMode.Wiggle) \
3121 or (self.view_mode is ViewMode.Waterfall
3122 and not processed_traces):
3124 if sq is None:
3125 self.draw_trace_boxes(
3126 p, self.time_projection, track_projections)
3128 else:
3129 self.draw_coverage(
3130 p, self.time_projection, track_projections)
3132 p.setFont(font)
3133 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3135 color_lookup = dict(
3136 [(k, i) for (i, k) in enumerate(self.color_keys)])
3138 self.track_to_nslc_ids = {}
3139 nticks = 0
3140 annot_labels = []
3142 if self.view_mode is ViewMode.Waterfall and processed_traces:
3143 waterfall = self.waterfall
3144 waterfall.set_time_range(self.tmin, self.tmax)
3145 waterfall.set_traces(processed_traces)
3146 waterfall.set_cmap(self.waterfall_cmap)
3147 waterfall.set_integrate(self.waterfall_integrate)
3148 waterfall.set_clip(
3149 self.waterfall_clip_min, self.waterfall_clip_max)
3150 waterfall.show_absolute_values(
3151 self.waterfall_show_absolute)
3153 rect = qc.QRectF(
3154 0, self.ax_height,
3155 self.width(), self.height() - self.ax_height*2
3156 )
3157 waterfall.draw_waterfall(p, rect=rect)
3159 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3160 show_scales = self.menuitem_showscalerange.isChecked() \
3161 or self.menuitem_showscaleaxis.isChecked()
3163 fm = qg.QFontMetrics(axannotfont, p.device())
3164 trackheight = self.track_to_screen(1.-0.05) \
3165 - self.track_to_screen(0.05)
3167 nlinesavail = trackheight/float(fm.lineSpacing())
3169 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3170 if self.menuitem_showscaleaxis.isChecked() \
3171 else 15
3173 yscaler = pyrocko.plot.AutoScaler(
3174 no_exp_interval=(-3, 2), approx_ticks=nticks,
3175 snap=show_scales
3176 and not self.menuitem_showscaleaxis.isChecked())
3178 data_ranges = pyrocko.trace.minmax(
3179 processed_traces,
3180 key=self.scaling_key,
3181 mode=self.scaling_base)
3183 if not self.menuitem_fixscalerange.isChecked():
3184 self.old_data_ranges = data_ranges
3185 else:
3186 data_ranges.update(self.old_data_ranges)
3188 self.apply_scaling_hooks(data_ranges)
3190 trace_to_itrack = {}
3191 track_scaling_keys = {}
3192 track_scaling_colors = {}
3193 for trace in processed_traces:
3194 gt = self.gather(trace)
3195 if gt not in self.key_to_row:
3196 continue
3198 itrack = self.key_to_row[gt]
3199 if itrack not in track_projections:
3200 continue
3202 trace_to_itrack[trace] = itrack
3204 if itrack not in self.track_to_nslc_ids:
3205 self.track_to_nslc_ids[itrack] = set()
3207 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3209 if itrack not in track_scaling_keys:
3210 track_scaling_keys[itrack] = set()
3212 scaling_key = self.scaling_key(trace)
3213 track_scaling_keys[itrack].add(scaling_key)
3215 color = pyrocko.plot.color(
3216 color_lookup[self.color_gather(trace)])
3218 k = itrack, scaling_key
3219 if k not in track_scaling_colors \
3220 and self.menuitem_colortraces.isChecked():
3221 track_scaling_colors[k] = color
3222 else:
3223 track_scaling_colors[k] = primary_color
3225 # y axes, zero lines
3226 trace_projections = {}
3227 for itrack in list(track_projections.keys()):
3228 if itrack not in track_scaling_keys:
3229 continue
3230 uoff = 0
3231 for scaling_key in track_scaling_keys[itrack]:
3232 data_range = data_ranges[scaling_key]
3233 dymin, dymax = data_range
3234 ymin, ymax, yinc = yscaler.make_scale(
3235 (dymin/self.gain, dymax/self.gain))
3236 iexp = yscaler.make_exp(yinc)
3237 factor = 10**iexp
3238 trace_projection = track_projections[itrack].copy()
3239 trace_projection.set_in_range(ymax, ymin)
3240 trace_projections[itrack, scaling_key] = \
3241 trace_projection
3242 umin, umax = self.time_projection.get_out_range()
3243 vmin, vmax = trace_projection.get_out_range()
3244 umax_zeroline = umax
3245 uoffnext = uoff
3247 if show_scales:
3248 pen = qg.QPen(primary_pen)
3249 k = itrack, scaling_key
3250 if k in track_scaling_colors:
3251 c = qg.QColor(*track_scaling_colors[
3252 itrack, scaling_key])
3254 pen.setColor(c)
3256 p.setPen(pen)
3257 if nlinesavail > 3:
3258 if self.menuitem_showscaleaxis.isChecked():
3259 ymin_annot = math.ceil(ymin/yinc)*yinc
3260 ny_annot = int(
3261 math.floor(ymax/yinc)
3262 - math.ceil(ymin/yinc)) + 1
3264 for iy_annot in range(ny_annot):
3265 y = ymin_annot + iy_annot*yinc
3266 v = trace_projection(y)
3267 line = qc.QLineF(
3268 umax-10-uoff, v, umax-uoff, v)
3270 p.drawLine(line)
3271 if iy_annot == ny_annot - 1 \
3272 and iexp != 0:
3273 sexp = ' × ' \
3274 '10<sup>%i</sup>' % iexp
3275 else:
3276 sexp = ''
3278 snum = num_to_html(y/factor)
3279 lab = Label(
3280 p,
3281 umax-20-uoff,
3282 v, '%s%s' % (snum, sexp),
3283 label_bg=None,
3284 anchor='MR',
3285 font=axannotfont,
3286 color=c)
3288 uoffnext = max(
3289 lab.rect.width()+30., uoffnext)
3291 annot_labels.append(lab)
3292 if y == 0.:
3293 umax_zeroline = \
3294 umax - 20 \
3295 - lab.rect.width() - 10 \
3296 - uoff
3297 else:
3298 if not show_boxes:
3299 qpoints = make_QPolygonF(
3300 [umax-20-uoff,
3301 umax-10-uoff,
3302 umax-10-uoff,
3303 umax-20-uoff],
3304 [vmax, vmax, vmin, vmin])
3305 p.drawPolyline(qpoints)
3307 snum = num_to_html(ymin)
3308 labmin = Label(
3309 p, umax-15-uoff, vmax, snum,
3310 label_bg=None,
3311 anchor='BR',
3312 font=axannotfont,
3313 color=c)
3315 annot_labels.append(labmin)
3316 snum = num_to_html(ymax)
3317 labmax = Label(
3318 p, umax-15-uoff, vmin, snum,
3319 label_bg=None,
3320 anchor='TR',
3321 font=axannotfont,
3322 color=c)
3324 annot_labels.append(labmax)
3326 for lab in (labmin, labmax):
3327 uoffnext = max(
3328 lab.rect.width()+10., uoffnext)
3330 if self.menuitem_showzeroline.isChecked():
3331 v = trace_projection(0.)
3332 if vmin <= v <= vmax:
3333 line = qc.QLineF(umin, v, umax_zeroline, v)
3334 p.drawLine(line)
3336 uoff = uoffnext
3338 p.setFont(font)
3339 p.setPen(primary_pen)
3340 for trace in processed_traces:
3341 if self.view_mode is not ViewMode.Wiggle:
3342 break
3344 if trace not in trace_to_itrack:
3345 continue
3347 itrack = trace_to_itrack[trace]
3348 scaling_key = self.scaling_key(trace)
3349 trace_projection = trace_projections[
3350 itrack, scaling_key]
3352 vdata = trace_projection(trace.get_ydata())
3354 udata_min = float(self.time_projection(trace.tmin))
3355 udata_max = float(self.time_projection(
3356 trace.tmin+trace.deltat*(vdata.size-1)))
3357 udata = num.linspace(udata_min, udata_max, vdata.size)
3359 qpoints = make_QPolygonF(udata, vdata)
3361 umin, umax = self.time_projection.get_out_range()
3362 vmin, vmax = trace_projection.get_out_range()
3364 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3366 if self.menuitem_cliptraces.isChecked():
3367 p.setClipRect(trackrect)
3369 if self.menuitem_colortraces.isChecked():
3370 color = pyrocko.plot.color(
3371 color_lookup[self.color_gather(trace)])
3372 pen = qg.QPen(qg.QColor(*color), 1)
3373 p.setPen(pen)
3375 p.drawPolyline(qpoints)
3377 if self.floating_marker:
3378 self.floating_marker.draw_trace(
3379 self, p, trace,
3380 self.time_projection, trace_projection, 1.0)
3382 for marker in self.markers.with_key_in(
3383 self.tmin - self.markers_deltat_max,
3384 self.tmax):
3386 if marker.tmin < self.tmax \
3387 and self.tmin < marker.tmax \
3388 and marker.kind \
3389 in self.visible_marker_kinds:
3390 marker.draw_trace(
3391 self, p, trace, self.time_projection,
3392 trace_projection, 1.0)
3394 p.setPen(primary_pen)
3396 if self.menuitem_cliptraces.isChecked():
3397 p.setClipRect(0, 0, int(w), int(h))
3399 if self.floating_marker:
3400 self.floating_marker.draw(
3401 p, self.time_projection, vcenter_projection)
3403 self.draw_visible_markers(
3404 p, vcenter_projection, primary_pen)
3406 p.setPen(primary_pen)
3407 while font.pointSize() > 2:
3408 fm = qg.QFontMetrics(font, p.device())
3409 trackheight = self.track_to_screen(1.-0.05) \
3410 - self.track_to_screen(0.05)
3411 nlinesavail = trackheight/float(fm.lineSpacing())
3412 if nlinesavail > 1:
3413 break
3415 font.setPointSize(font.pointSize()-1)
3417 p.setFont(font)
3418 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3420 for key in self.track_keys:
3421 itrack = self.key_to_row[key]
3422 if itrack in track_projections:
3423 plabel = ' '.join(
3424 [str(x) for x in key if x is not None])
3425 lx = 10
3426 ly = self.track_to_screen(itrack+0.5)
3428 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3429 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3430 continue
3432 contains_cursor = \
3433 self.track_to_screen(itrack) \
3434 < mouse_pos.y() \
3435 < self.track_to_screen(itrack+1)
3437 if not contains_cursor:
3438 continue
3440 font_large = p.font()
3441 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3442 p.setFont(font_large)
3443 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3444 p.setFont(font)
3446 for lab in annot_labels:
3447 lab.draw()
3449 self.timer_draw.stop()
3451 def see_data_params(self):
3453 min_deltat = self.content_deltat_range()[0]
3455 # determine padding and downampling requirements
3456 if self.lowpass is not None:
3457 deltat_target = 1./self.lowpass * 0.25
3458 ndecimate = min(
3459 50,
3460 max(1, int(round(deltat_target / min_deltat))))
3461 tpad = 1./self.lowpass * 2.
3462 else:
3463 ndecimate = 1
3464 tpad = min_deltat*5.
3466 if self.highpass is not None:
3467 tpad = max(1./self.highpass * 2., tpad)
3469 nsee_points_per_trace = 5000*10
3470 tsee = ndecimate*nsee_points_per_trace*min_deltat
3472 return ndecimate, tpad, tsee
3474 def clean_update(self):
3475 self.cached_processed_traces = None
3476 self.update()
3478 def get_adequate_tpad(self):
3479 tpad = 0.
3480 for f in [self.highpass, self.lowpass]:
3481 if f is not None:
3482 tpad = max(tpad, 1.0/f)
3484 for snuffling in self.snufflings:
3485 if snuffling._post_process_hook_enabled \
3486 or snuffling._pre_process_hook_enabled:
3488 tpad = max(tpad, snuffling.get_tpad())
3490 return tpad
3492 def prepare_cutout2(
3493 self, tmin, tmax, trace_selector=None, degap=True,
3494 demean=True, nmax=6000):
3496 if self.pile.is_empty():
3497 return []
3499 nmax = self.visible_length
3501 self.timer_cutout.start()
3503 tsee = tmax-tmin
3504 min_deltat_wo_decimate = tsee/nmax
3505 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3507 min_deltat_allow = min_deltat_wo_decimate
3508 if self.lowpass is not None:
3509 target_deltat_lp = 0.25/self.lowpass
3510 if target_deltat_lp > min_deltat_wo_decimate:
3511 min_deltat_allow = min_deltat_w_decimate
3513 min_deltat_allow = math.exp(
3514 int(math.floor(math.log(min_deltat_allow))))
3516 tmin_ = tmin
3517 tmax_ = tmax
3519 # fetch more than needed?
3520 if self.menuitem_liberal_fetch.isChecked():
3521 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3522 tmin = math.floor(tmin/tlen) * tlen
3523 tmax = math.ceil(tmax/tlen) * tlen
3525 fft_filtering = self.menuitem_fft_filtering.isChecked()
3526 lphp = self.menuitem_lphp.isChecked()
3527 ads = self.menuitem_allowdownsampling.isChecked()
3529 tpad = self.get_adequate_tpad()
3530 tpad = max(tpad, tsee)
3532 # state vector to decide if cached traces can be used
3533 vec = (
3534 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3535 self.highpass, fft_filtering, lphp,
3536 min_deltat_allow, self.rotate, self.shown_tracks_range,
3537 ads, self.pile.get_update_count())
3539 if (self.cached_vec
3540 and self.cached_vec[0] <= vec[0]
3541 and vec[1] <= self.cached_vec[1]
3542 and vec[2:] == self.cached_vec[2:]
3543 and not (self.reloaded or self.menuitem_watch.isChecked())
3544 and self.cached_processed_traces is not None):
3546 logger.debug('Using cached traces')
3547 processed_traces = self.cached_processed_traces
3549 else:
3550 processed_traces = []
3551 if self.pile.deltatmax >= min_deltat_allow:
3553 def group_selector(gr):
3554 return gr.deltatmax >= min_deltat_allow
3556 if trace_selector is not None:
3557 def trace_selectorx(tr):
3558 return tr.deltat >= min_deltat_allow \
3559 and trace_selector(tr)
3560 else:
3561 def trace_selectorx(tr):
3562 return tr.deltat >= min_deltat_allow
3564 for traces in self.pile.chopper(
3565 tmin=tmin, tmax=tmax, tpad=tpad,
3566 want_incomplete=True,
3567 degap=degap,
3568 maxgap=gap_lap_tolerance,
3569 maxlap=gap_lap_tolerance,
3570 keep_current_files_open=True,
3571 group_selector=group_selector,
3572 trace_selector=trace_selectorx,
3573 accessor_id=id(self),
3574 snap=(math.floor, math.ceil),
3575 include_last=True):
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(qgl.QGLWidget)
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=False,
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)
4314 if use_opengl:
4315 self.viewer = GLPileViewerMain(
4316 pile,
4317 ntracks_shown_max=ntracks_shown_max,
4318 panel_parent=panel_parent,
4319 menu=self.menu)
4320 else:
4321 self.viewer = PileViewerMain(
4322 pile,
4323 ntracks_shown_max=ntracks_shown_max,
4324 panel_parent=panel_parent,
4325 menu=self.menu)
4327 self.marker_editor_sortable = marker_editor_sortable
4329 self.setFrameShape(qw.QFrame.StyledPanel)
4330 self.setFrameShadow(qw.QFrame.Sunken)
4332 self.input_area = qw.QFrame(self)
4333 ia_layout = qw.QGridLayout()
4334 ia_layout.setContentsMargins(11, 11, 11, 11)
4335 self.input_area.setLayout(ia_layout)
4337 self.inputline = LineEditWithAbort(self.input_area)
4338 self.inputline.returnPressed.connect(
4339 self.inputline_returnpressed)
4340 self.inputline.editingFinished.connect(
4341 self.inputline_finished)
4342 self.inputline.aborted.connect(
4343 self.inputline_aborted)
4345 self.inputline.history_down.connect(
4346 lambda: self.step_through_history(1))
4347 self.inputline.history_up.connect(
4348 lambda: self.step_through_history(-1))
4350 self.inputline.textEdited.connect(
4351 self.inputline_changed)
4353 self.inputline.setPlaceholderText(
4354 u'Quick commands: e.g. \'c HH?\' to select channels. '
4355 u'Use ↑ or ↓ to navigate.')
4356 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4357 self.input_area.hide()
4358 self.history = None
4360 self.inputline_error_str = None
4362 self.inputline_error = qw.QLabel()
4363 self.inputline_error.hide()
4365 ia_layout.addWidget(self.inputline, 0, 0)
4366 ia_layout.addWidget(self.inputline_error, 1, 0)
4367 layout.addWidget(self.input_area, 0, 0, 1, 2)
4368 layout.addWidget(self.viewer, 1, 0)
4370 pb = Progressbars(self)
4371 layout.addWidget(pb, 2, 0, 1, 2)
4372 self.progressbars = pb
4374 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4375 self.scrollbar = scrollbar
4376 layout.addWidget(scrollbar, 1, 1)
4377 self.scrollbar.valueChanged.connect(
4378 self.scrollbar_changed)
4380 self.block_scrollbar_changes = False
4382 self.viewer.want_input.connect(
4383 self.inputline_show)
4384 self.viewer.tracks_range_changed.connect(
4385 self.tracks_range_changed)
4386 self.viewer.pile_has_changed_signal.connect(
4387 self.adjust_controls)
4388 self.viewer.about_to_close.connect(
4389 self.save_inputline_history)
4391 self.setLayout(layout)
4393 def cleanup(self):
4394 self.viewer.cleanup()
4396 def get_progressbars(self):
4397 return self.progressbars
4399 def inputline_show(self):
4400 if not self.history:
4401 self.load_inputline_history()
4403 self.input_area.show()
4404 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4405 self.inputline.selectAll()
4407 def inputline_set_error(self, string):
4408 self.inputline_error_str = string
4409 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4410 self.inputline.selectAll()
4411 self.inputline_error.setText(string)
4412 self.input_area.show()
4413 self.inputline_error.show()
4415 def inputline_clear_error(self):
4416 if self.inputline_error_str:
4417 self.inputline.setPalette(qw.QApplication.palette())
4418 self.inputline_error_str = None
4419 self.inputline_error.clear()
4420 self.inputline_error.hide()
4422 def inputline_changed(self, line):
4423 self.viewer.inputline_changed(str(line))
4424 self.inputline_clear_error()
4426 def inputline_returnpressed(self):
4427 line = str(self.inputline.text())
4428 clearit, hideit, error = self.viewer.inputline_finished(line)
4430 if error:
4431 self.inputline_set_error(error)
4433 line = line.strip()
4435 if line != '' and not error:
4436 if not (len(self.history) >= 1 and line == self.history[-1]):
4437 self.history.append(line)
4439 if clearit:
4441 self.inputline.blockSignals(True)
4442 qpat, qinp = self.viewer.get_quick_filter_patterns()
4443 if qpat is None:
4444 self.inputline.clear()
4445 else:
4446 self.inputline.setText(qinp)
4447 self.inputline.blockSignals(False)
4449 if hideit and not error:
4450 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4451 self.input_area.hide()
4453 self.hist_ind = len(self.history)
4455 def inputline_aborted(self):
4456 '''
4457 Hide the input line.
4458 '''
4459 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4460 self.hist_ind = len(self.history)
4461 self.input_area.hide()
4463 def save_inputline_history(self):
4464 '''
4465 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4466 '''
4467 if not self.history:
4468 return
4470 conf = pyrocko.config
4471 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4472 with open(fn_hist, 'w') as f:
4473 i = min(100, len(self.history))
4474 for c in self.history[-i:]:
4475 f.write('%s\n' % c)
4477 def load_inputline_history(self):
4478 '''
4479 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4480 '''
4481 conf = pyrocko.config
4482 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4483 if not os.path.exists(fn_hist):
4484 with open(fn_hist, 'w+') as f:
4485 f.write('\n')
4487 with open(fn_hist, 'r') as f:
4488 self.history = [line.strip() for line in f.readlines()]
4490 self.hist_ind = len(self.history)
4492 def step_through_history(self, ud=1):
4493 '''
4494 Step through input line history and set the input line text.
4495 '''
4496 n = len(self.history)
4497 self.hist_ind += ud
4498 self.hist_ind %= (n + 1)
4499 if len(self.history) != 0 and self.hist_ind != n:
4500 self.inputline.setText(self.history[self.hist_ind])
4501 else:
4502 self.inputline.setText('')
4504 def inputline_finished(self):
4505 pass
4507 def tracks_range_changed(self, ntracks, ilo, ihi):
4508 if self.block_scrollbar_changes:
4509 return
4511 self.scrollbar.blockSignals(True)
4512 self.scrollbar.setPageStep(ihi-ilo)
4513 vmax = max(0, ntracks-(ihi-ilo))
4514 self.scrollbar.setRange(0, vmax)
4515 self.scrollbar.setValue(ilo)
4516 self.scrollbar.setHidden(vmax == 0)
4517 self.scrollbar.blockSignals(False)
4519 def scrollbar_changed(self, value):
4520 self.block_scrollbar_changes = True
4521 ilo = value
4522 ihi = ilo + self.scrollbar.pageStep()
4523 self.viewer.set_tracks_range((ilo, ihi))
4524 self.block_scrollbar_changes = False
4525 self.update_contents()
4527 def controls(self):
4528 frame = qw.QFrame(self)
4529 layout = qw.QGridLayout()
4530 frame.setLayout(layout)
4532 minfreq = 0.001
4533 maxfreq = 1000.0
4534 self.lowpass_control = ValControl(high_is_none=True)
4535 self.lowpass_control.setup(
4536 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4537 self.highpass_control = ValControl(low_is_none=True)
4538 self.highpass_control.setup(
4539 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4540 self.gain_control = ValControl()
4541 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4542 self.rot_control = LinValControl()
4543 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4544 self.colorbar_control = ColorbarControl(self)
4546 self.lowpass_control.valchange.connect(
4547 self.viewer.lowpass_change)
4548 self.highpass_control.valchange.connect(
4549 self.viewer.highpass_change)
4550 self.gain_control.valchange.connect(
4551 self.viewer.gain_change)
4552 self.rot_control.valchange.connect(
4553 self.viewer.rot_change)
4554 self.colorbar_control.cmap_changed.connect(
4555 self.viewer.waterfall_cmap_change
4556 )
4557 self.colorbar_control.clip_changed.connect(
4558 self.viewer.waterfall_clip_change
4559 )
4560 self.colorbar_control.show_absolute_toggled.connect(
4561 self.viewer.waterfall_show_absolute_change
4562 )
4563 self.colorbar_control.show_integrate_toggled.connect(
4564 self.viewer.waterfall_set_integrate
4565 )
4567 for icontrol, control in enumerate((
4568 self.highpass_control,
4569 self.lowpass_control,
4570 self.gain_control,
4571 self.rot_control,
4572 self.colorbar_control)):
4574 for iwidget, widget in enumerate(control.widgets()):
4575 layout.addWidget(widget, icontrol, iwidget)
4577 spacer = qw.QSpacerItem(
4578 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4579 layout.addItem(spacer, 4, 0, 1, 3)
4581 self.adjust_controls()
4582 self.viewer.viewmode_change(ViewMode.Wiggle)
4583 return frame
4585 def marker_editor(self):
4586 editor = pyrocko.gui.marker_editor.MarkerEditor(
4587 self, sortable=self.marker_editor_sortable)
4589 editor.set_viewer(self.get_view())
4590 editor.get_marker_model().dataChanged.connect(
4591 self.update_contents)
4592 return editor
4594 def adjust_controls(self):
4595 dtmin, dtmax = self.viewer.content_deltat_range()
4596 maxfreq = 0.5/dtmin
4597 minfreq = (0.5/dtmax)*0.001
4598 self.lowpass_control.set_range(minfreq, maxfreq)
4599 self.highpass_control.set_range(minfreq, maxfreq)
4601 def setup_snufflings(self):
4602 self.viewer.setup_snufflings()
4604 def get_view(self):
4605 return self.viewer
4607 def update_contents(self):
4608 self.viewer.update()
4610 def get_pile(self):
4611 return self.viewer.get_pile()
4613 def show_colorbar_ctrl(self, show):
4614 for w in self.colorbar_control.widgets():
4615 w.setVisible(show)
4617 def show_gain_ctrl(self, show):
4618 for w in self.gain_control.widgets():
4619 w.setVisible(show)