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_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_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
1161 self.cached_chopped_traces = {}
1163 self.timer = qc.QTimer(self)
1164 self.timer.timeout.connect(self.periodical)
1165 self.timer.setInterval(1000)
1166 self.timer.start()
1167 self.pile.add_listener(self)
1168 self.trace_styles = {}
1169 if self.get_squirrel() is None:
1170 self.determine_box_styles()
1172 self.setMouseTracking(True)
1174 user_home_dir = os.path.expanduser('~')
1175 self.snuffling_modules = {}
1176 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1177 self.default_snufflings = None
1178 self.snufflings = []
1180 self.stations = {}
1182 self.timer_draw = Timer()
1183 self.timer_cutout = Timer()
1184 self.time_spent_painting = 0.0
1185 self.time_last_painted = time.time()
1187 self.interactive_range_change_time = 0.0
1188 self.interactive_range_change_delay_time = 10.0
1189 self.follow_timer = None
1191 self.sortingmode_change_time = 0.0
1192 self.sortingmode_change_delay_time = None
1194 self.old_data_ranges = {}
1196 self.error_messages = {}
1197 self.return_tag = None
1198 self.wheel_pos = 60
1200 self.setAcceptDrops(True)
1201 self._paths_to_load = []
1203 self.tf_cache = {}
1205 self.waterfall = TraceWaterfall()
1206 self.waterfall_cmap = 'viridis'
1207 self.waterfall_clip_min = 0.
1208 self.waterfall_clip_max = 1.
1209 self.waterfall_show_absolute = False
1210 self.waterfall_integrate = False
1211 self.view_mode = ViewMode.Wiggle
1213 self.automatic_updates = True
1215 self.closing = False
1216 self.paint_timer = qc.QTimer(self)
1217 self.paint_timer.timeout.connect(self.reset_updates)
1218 self.paint_timer.setInterval(20)
1219 self.paint_timer.start()
1221 @qc.pyqtSlot()
1222 def reset_updates(self):
1223 if not self.updatesEnabled():
1224 self.setUpdatesEnabled(True)
1226 def fail(self, reason):
1227 box = qw.QMessageBox(self)
1228 box.setText(reason)
1229 box.exec_()
1231 def set_trace_filter(self, filter_func):
1232 self.trace_filter = filter_func
1233 self.sortingmode_change()
1235 def update_trace_filter(self):
1236 if self.blacklist:
1238 def blacklist_func(tr):
1239 return not pyrocko.util.match_nslc(
1240 self.blacklist, tr.nslc_id)
1242 else:
1243 blacklist_func = None
1245 if self.quick_filter is None and blacklist_func is None:
1246 self.set_trace_filter(None)
1247 elif self.quick_filter is None:
1248 self.set_trace_filter(blacklist_func)
1249 elif blacklist_func is None:
1250 self.set_trace_filter(self.quick_filter)
1251 else:
1252 self.set_trace_filter(
1253 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1255 def set_quick_filter(self, filter_func):
1256 self.quick_filter = filter_func
1257 self.update_trace_filter()
1259 def set_quick_filter_patterns(self, patterns, inputline=None):
1260 if patterns is not None:
1261 self.set_quick_filter(
1262 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1263 else:
1264 self.set_quick_filter(None)
1266 self.quick_filter_patterns = patterns, inputline
1268 def get_quick_filter_patterns(self):
1269 return self.quick_filter_patterns
1271 def add_blacklist_pattern(self, pattern):
1272 if pattern == 'empty':
1273 keys = set(self.pile.nslc_ids)
1274 trs = self.pile.all(
1275 tmin=self.tmin,
1276 tmax=self.tmax,
1277 load_data=False,
1278 degap=False)
1280 for tr in trs:
1281 if tr.nslc_id in keys:
1282 keys.remove(tr.nslc_id)
1284 for key in keys:
1285 xpattern = '.'.join(key)
1286 if xpattern not in self.blacklist:
1287 self.blacklist.append(xpattern)
1289 else:
1290 if pattern in self.blacklist:
1291 self.blacklist.remove(pattern)
1293 self.blacklist.append(pattern)
1295 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1296 self.update_trace_filter()
1298 def remove_blacklist_pattern(self, pattern):
1299 if pattern in self.blacklist:
1300 self.blacklist.remove(pattern)
1301 else:
1302 raise PileViewerMainException(
1303 'Pattern not found in blacklist.')
1305 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1306 self.update_trace_filter()
1308 def clear_blacklist(self):
1309 self.blacklist = []
1310 self.update_trace_filter()
1312 def ssort(self, tr):
1313 return self._ssort(tr)
1315 def station_key(self, x):
1316 return x.network, x.station
1318 def station_keys(self, x):
1319 return [
1320 (x.network, x.station, x.location),
1321 (x.network, x.station)]
1323 def station_attrib(self, tr, getter, default_getter):
1324 for sk in self.station_keys(tr):
1325 if sk in self.stations:
1326 station = self.stations[sk]
1327 return getter(station)
1329 return default_getter(tr)
1331 def get_station(self, sk):
1332 return self.stations[sk]
1334 def has_station(self, station):
1335 for sk in self.station_keys(station):
1336 if sk in self.stations:
1337 return True
1339 return False
1341 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1342 return self.station_attrib(
1343 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1345 def set_stations(self, stations):
1346 self.stations = {}
1347 self.add_stations(stations)
1349 def add_stations(self, stations):
1350 for station in stations:
1351 for sk in self.station_keys(station):
1352 self.stations[sk] = station
1354 ev = self.get_active_event()
1355 if ev:
1356 self.set_origin(ev)
1358 def add_event(self, event):
1359 marker = EventMarker(event)
1360 self.add_marker(marker)
1362 def add_events(self, events):
1363 markers = [EventMarker(e) for e in events]
1364 self.add_markers(markers)
1366 def set_event_marker_as_origin(self, ignore=None):
1367 selected = self.selected_markers()
1368 if not selected:
1369 self.fail('An event marker must be selected.')
1370 return
1372 m = selected[0]
1373 if not isinstance(m, EventMarker):
1374 self.fail('Selected marker is not an event.')
1375 return
1377 self.set_active_event_marker(m)
1379 def deactivate_event_marker(self):
1380 if self.active_event_marker:
1381 self.active_event_marker.active = False
1383 self.active_event_marker_changed.emit()
1384 self.active_event_marker = None
1386 def set_active_event_marker(self, event_marker):
1387 if self.active_event_marker:
1388 self.active_event_marker.active = False
1390 self.active_event_marker = event_marker
1391 event_marker.active = True
1392 event = event_marker.get_event()
1393 self.set_origin(event)
1394 self.active_event_marker_changed.emit()
1396 def set_active_event(self, event):
1397 for marker in self.markers:
1398 if isinstance(marker, EventMarker):
1399 if marker.get_event() is event:
1400 self.set_active_event_marker(marker)
1402 def get_active_event_marker(self):
1403 return self.active_event_marker
1405 def get_active_event(self):
1406 m = self.get_active_event_marker()
1407 if m is not None:
1408 return m.get_event()
1409 else:
1410 return None
1412 def get_active_markers(self):
1413 emarker = self.get_active_event_marker()
1414 if emarker is None:
1415 return None, []
1417 else:
1418 ev = emarker.get_event()
1419 pmarkers = [
1420 m for m in self.markers
1421 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1423 return emarker, pmarkers
1425 def set_origin(self, location):
1426 for station in self.stations.values():
1427 station.set_event_relative_data(
1428 location,
1429 distance_3d=self.menuitem_distances_3d.isChecked())
1431 self.sortingmode_change()
1433 def distances_3d_changed(self):
1434 ignore = self.menuitem_distances_3d.isChecked()
1435 self.set_event_marker_as_origin(ignore)
1437 def iter_snuffling_modules(self):
1438 pjoin = os.path.join
1439 for path in self.snuffling_paths:
1441 if not os.path.isdir(path):
1442 os.mkdir(path)
1444 for entry in os.listdir(path):
1445 directory = path
1446 fn = entry
1447 d = pjoin(path, entry)
1448 if os.path.isdir(d):
1449 directory = d
1450 if os.path.isfile(
1451 os.path.join(directory, 'snuffling.py')):
1452 fn = 'snuffling.py'
1454 if not fn.endswith('.py'):
1455 continue
1457 name = fn[:-3]
1459 if (directory, name) not in self.snuffling_modules:
1460 self.snuffling_modules[directory, name] = \
1461 pyrocko.gui.snuffling.SnufflingModule(
1462 directory, name, self)
1464 yield self.snuffling_modules[directory, name]
1466 def setup_snufflings(self):
1467 # user snufflings
1468 for mod in self.iter_snuffling_modules():
1469 try:
1470 mod.load_if_needed()
1471 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1472 logger.warning('Snuffling module "%s" is broken' % e)
1474 # load the default snufflings on first run
1475 if self.default_snufflings is None:
1476 self.default_snufflings = pyrocko.gui\
1477 .snufflings.__snufflings__()
1478 for snuffling in self.default_snufflings:
1479 self.add_snuffling(snuffling)
1481 def set_panel_parent(self, panel_parent):
1482 self.panel_parent = panel_parent
1484 def get_panel_parent(self):
1485 return self.panel_parent
1487 def add_snuffling(self, snuffling, reloaded=False):
1488 logger.debug('Adding snuffling %s' % snuffling.get_name())
1489 snuffling.init_gui(
1490 self, self.get_panel_parent(), self, reloaded=reloaded)
1491 self.snufflings.append(snuffling)
1492 self.update()
1494 def remove_snuffling(self, snuffling):
1495 snuffling.delete_gui()
1496 self.update()
1497 self.snufflings.remove(snuffling)
1498 snuffling.pre_destroy()
1500 def add_snuffling_menuitem(self, item):
1501 self.snufflings_menu.addAction(item)
1502 item.setParent(self.snufflings_menu)
1503 sort_actions(self.snufflings_menu)
1505 def remove_snuffling_menuitem(self, item):
1506 self.snufflings_menu.removeAction(item)
1508 def add_snuffling_help_menuitem(self, item):
1509 self.snuffling_help.addAction(item)
1510 item.setParent(self.snuffling_help)
1511 sort_actions(self.snuffling_help)
1513 def remove_snuffling_help_menuitem(self, item):
1514 self.snuffling_help.removeAction(item)
1516 def add_panel_toggler(self, item):
1517 self.toggle_panel_menu.addAction(item)
1518 item.setParent(self.toggle_panel_menu)
1519 sort_actions(self.toggle_panel_menu)
1521 def remove_panel_toggler(self, item):
1522 self.toggle_panel_menu.removeAction(item)
1524 def load(self, paths, regex=None, format='from_extension',
1525 cache_dir=None, force_cache=False):
1527 if cache_dir is None:
1528 cache_dir = pyrocko.config.config().cache_dir
1529 if isinstance(paths, str):
1530 paths = [paths]
1532 fns = pyrocko.util.select_files(
1533 paths, selector=None, include=regex, show_progress=False)
1535 if not fns:
1536 return
1538 cache = pyrocko.pile.get_cache(cache_dir)
1540 t = [time.time()]
1542 def update_bar(label, value):
1543 pbs = self.parent().get_progressbars()
1544 if label.lower() == 'looking at files':
1545 label = 'Looking at %i files' % len(fns)
1546 else:
1547 label = 'Scanning %i files' % len(fns)
1549 return pbs.set_status(label, value)
1551 def update_progress(label, i, n):
1552 abort = False
1554 qw.qApp.processEvents()
1555 if n != 0:
1556 perc = i*100/n
1557 else:
1558 perc = 100
1559 abort |= update_bar(label, perc)
1560 abort |= self.window().is_closing()
1562 tnow = time.time()
1563 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1564 self.update()
1565 t[0] = tnow
1567 return abort
1569 self.automatic_updates = False
1571 self.pile.load_files(
1572 sorted(fns),
1573 filename_attributes=regex,
1574 cache=cache,
1575 fileformat=format,
1576 show_progress=False,
1577 update_progress=update_progress)
1579 self.automatic_updates = True
1580 self.update()
1582 def load_queued(self):
1583 if not self._paths_to_load:
1584 return
1585 paths = self._paths_to_load
1586 self._paths_to_load = []
1587 self.load(paths)
1589 def load_soon(self, paths):
1590 self._paths_to_load.extend(paths)
1591 qc.QTimer.singleShot(200, self.load_queued)
1593 def open_waveforms(self):
1594 caption = 'Select one or more files to open'
1596 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1597 self, caption, options=qfiledialog_options))
1599 if fns:
1600 self.load(list(str(fn) for fn in fns))
1602 def open_waveform_directory(self):
1603 caption = 'Select directory to scan for waveform files'
1605 dn = qw.QFileDialog.getExistingDirectory(
1606 self, caption, options=qfiledialog_options)
1608 if dn:
1609 self.load([str(dn)])
1611 def open_stations(self, fns=None):
1612 caption = 'Select one or more Pyrocko station files to open'
1614 if not fns:
1615 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1616 self, caption, options=qfiledialog_options))
1618 try:
1619 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1620 for stat in stations:
1621 self.add_stations(stat)
1623 except Exception as e:
1624 self.fail('Failed to read station file: %s' % str(e))
1626 def open_stations_xml(self, fns=None):
1627 from pyrocko.io import stationxml
1629 caption = 'Select one or more StationXML files'
1630 if not fns:
1631 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1632 self, caption, options=qfiledialog_options,
1633 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1634 ';;All files (*)'))
1636 try:
1637 stations = [
1638 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1639 for x in fns]
1641 for stat in stations:
1642 self.add_stations(stat)
1644 except Exception as e:
1645 self.fail('Failed to read StationXML file: %s' % str(e))
1647 def add_traces(self, traces):
1648 if traces:
1649 mtf = pyrocko.pile.MemTracesFile(None, traces)
1650 self.pile.add_file(mtf)
1651 ticket = (self.pile, mtf)
1652 return ticket
1653 else:
1654 return (None, None)
1656 def release_data(self, tickets):
1657 for ticket in tickets:
1658 pile, mtf = ticket
1659 if pile is not None:
1660 pile.remove_file(mtf)
1662 def periodical(self):
1663 if self.menuitem_watch.isChecked():
1664 if self.pile.reload_modified():
1665 self.update()
1667 def get_pile(self):
1668 return self.pile
1670 def pile_changed(self, what):
1671 self.pile_has_changed = True
1672 self.pile_has_changed_signal.emit()
1673 if self.automatic_updates:
1674 self.update()
1676 def set_gathering(self, gather=None, color=None):
1678 if gather is None:
1679 def gather_func(tr):
1680 return tr.nslc_id
1682 gather = (0, 1, 2, 3)
1684 else:
1685 def gather_func(tr):
1686 return (
1687 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1689 if color is None:
1690 def color(tr):
1691 return tr.location
1693 self.gather = gather_func
1694 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1696 self.color_gather = color
1697 self.color_keys = self.pile.gather_keys(color)
1698 previous_ntracks = self.ntracks
1699 self.set_ntracks(len(keys))
1701 if self.shown_tracks_range is None or \
1702 previous_ntracks == 0 or \
1703 self.show_all:
1705 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1706 key_at_top = None
1707 n = high-low
1709 else:
1710 low, high = self.shown_tracks_range
1711 key_at_top = self.track_keys[low]
1712 n = high-low
1714 self.track_keys = sorted(keys)
1716 track_patterns = []
1717 for k in self.track_keys:
1718 pat = ['*', '*', '*', '*']
1719 for i, j in enumerate(gather):
1720 pat[j] = k[-len(gather)+i]
1722 track_patterns.append(pat)
1724 self.track_patterns = track_patterns
1726 if key_at_top is not None:
1727 try:
1728 ind = self.track_keys.index(key_at_top)
1729 low = ind
1730 high = low+n
1731 except Exception:
1732 pass
1734 self.set_tracks_range((low, high))
1736 self.key_to_row = dict(
1737 [(key, i) for (i, key) in enumerate(self.track_keys)])
1739 def inrange(x, r):
1740 return r[0] <= x and x < r[1]
1742 def trace_selector(trace):
1743 gt = self.gather(trace)
1744 return (
1745 gt in self.key_to_row and
1746 inrange(self.key_to_row[gt], self.shown_tracks_range))
1748 if self.trace_filter is not None:
1749 self.trace_selector = lambda x: \
1750 self.trace_filter(x) and trace_selector(x)
1751 else:
1752 self.trace_selector = trace_selector
1754 if self.tmin == working_system_time_range[0] and \
1755 self.tmax == working_system_time_range[1] or \
1756 self.show_all:
1758 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1759 if tmin is not None and tmax is not None:
1760 tlen = (tmax - tmin)
1761 tpad = tlen * 5./self.width()
1762 self.set_time_range(tmin-tpad, tmax+tpad)
1764 def set_time_range(self, tmin, tmax):
1765 if tmin is None:
1766 tmin = initial_time_range[0]
1768 if tmax is None:
1769 tmax = initial_time_range[1]
1771 if tmin > tmax:
1772 tmin, tmax = tmax, tmin
1774 if tmin == tmax:
1775 tmin -= 1.
1776 tmax += 1.
1778 tmin = max(working_system_time_range[0], tmin)
1779 tmax = min(working_system_time_range[1], tmax)
1781 min_deltat = self.content_deltat_range()[0]
1782 if (tmax - tmin < min_deltat):
1783 m = (tmin + tmax) / 2.
1784 tmin = m - min_deltat/2.
1785 tmax = m + min_deltat/2.
1787 self.time_projection.set_in_range(tmin, tmax)
1788 self.tmin, self.tmax = tmin, tmax
1790 def get_time_range(self):
1791 return self.tmin, self.tmax
1793 def ypart(self, y):
1794 if y < self.ax_height:
1795 return -1
1796 elif y > self.height()-self.ax_height:
1797 return 1
1798 else:
1799 return 0
1801 def time_fractional_digits(self):
1802 min_deltat = self.content_deltat_range()[0]
1803 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1805 def write_markers(self, fn=None):
1806 caption = "Choose a file name to write markers"
1807 if not fn:
1808 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1809 self, caption, options=qfiledialog_options))
1810 if fn:
1811 try:
1812 Marker.save_markers(
1813 self.markers, fn,
1814 fdigits=self.time_fractional_digits())
1816 except Exception as e:
1817 self.fail('Failed to write marker file: %s' % str(e))
1819 def write_selected_markers(self, fn=None):
1820 caption = "Choose a file name to write selected markers"
1821 if not fn:
1822 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1823 self, caption, options=qfiledialog_options))
1824 if fn:
1825 try:
1826 Marker.save_markers(
1827 self.iter_selected_markers(),
1828 fn,
1829 fdigits=self.time_fractional_digits())
1831 except Exception as e:
1832 self.fail('Failed to write marker file: %s' % str(e))
1834 def read_events(self, fn=None):
1835 '''
1836 Open QFileDialog to open, read and add
1837 :py:class:`pyrocko.model.Event` instances and their marker
1838 representation to the pile viewer.
1839 '''
1840 caption = "Selet one or more files to open"
1841 if not fn:
1842 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1843 self, caption, options=qfiledialog_options))
1844 if fn:
1845 try:
1846 self.add_events(pyrocko.model.load_events(fn))
1847 self.associate_phases_to_events()
1849 except Exception as e:
1850 self.fail('Failed to read event file: %s' % str(e))
1852 def read_markers(self, fn=None):
1853 '''
1854 Open QFileDialog to open, read and add markers to the pile viewer.
1855 '''
1856 caption = "Selet one or more marker files to open"
1857 if not fn:
1858 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1859 self, caption, options=qfiledialog_options))
1860 if fn:
1861 try:
1862 self.add_markers(Marker.load_markers(fn))
1863 self.associate_phases_to_events()
1865 except Exception as e:
1866 self.fail('Failed to read marker file: %s' % str(e))
1868 def associate_phases_to_events(self):
1869 associate_phases_to_events(self.markers)
1871 def add_marker(self, marker):
1872 # need index to inform QAbstactTableModel about upcoming change,
1873 # but have to restore current state in order to not cause problems
1874 self.markers.insert(marker)
1875 i = self.markers.remove(marker)
1877 self.begin_markers_add.emit(i, i)
1878 self.markers.insert(marker)
1879 self.end_markers_add.emit()
1880 self.markers_deltat_max = max(
1881 self.markers_deltat_max, marker.tmax - marker.tmin)
1883 def add_markers(self, markers):
1884 if not self.markers:
1885 self.begin_markers_add.emit(0, len(markers) - 1)
1886 self.markers.insert_many(markers)
1887 self.end_markers_add.emit()
1888 self.update_markers_deltat_max()
1889 else:
1890 for marker in markers:
1891 self.add_marker(marker)
1893 def update_markers_deltat_max(self):
1894 if self.markers:
1895 self.markers_deltat_max = max(
1896 marker.tmax - marker.tmin for marker in self.markers)
1898 def remove_marker(self, marker):
1899 '''
1900 Remove a ``marker`` from the :py:class:`PileViewer`.
1902 :param marker: :py:class:`Marker` (or subclass) instance
1903 '''
1905 if marker is self.active_event_marker:
1906 self.deactivate_event_marker()
1908 try:
1909 i = self.markers.index(marker)
1910 self.begin_markers_remove.emit(i, i)
1911 self.markers.remove_at(i)
1912 self.end_markers_remove.emit()
1913 except ValueError:
1914 pass
1916 def remove_markers(self, markers):
1917 '''
1918 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1920 :param markers: list of :py:class:`Marker` (or subclass)
1921 instances
1922 '''
1924 if markers is self.markers:
1925 markers = list(markers)
1927 for marker in markers:
1928 self.remove_marker(marker)
1930 self.update_markers_deltat_max()
1932 def remove_selected_markers(self):
1933 def delete_segment(istart, iend):
1934 self.begin_markers_remove.emit(istart, iend-1)
1935 for _ in range(iend - istart):
1936 self.markers.remove_at(istart)
1938 self.end_markers_remove.emit()
1940 istart = None
1941 ipos = 0
1942 markers = self.markers
1943 nmarkers = len(self.markers)
1944 while ipos < nmarkers:
1945 marker = markers[ipos]
1946 if marker.is_selected():
1947 if marker is self.active_event_marker:
1948 self.deactivate_event_marker()
1950 if istart is None:
1951 istart = ipos
1952 else:
1953 if istart is not None:
1954 delete_segment(istart, ipos)
1955 nmarkers -= ipos - istart
1956 ipos = istart - 1
1957 istart = None
1959 ipos += 1
1961 if istart is not None:
1962 delete_segment(istart, ipos)
1964 self.update_markers_deltat_max()
1966 def selected_markers(self):
1967 return [marker for marker in self.markers if marker.is_selected()]
1969 def iter_selected_markers(self):
1970 for marker in self.markers:
1971 if marker.is_selected():
1972 yield marker
1974 def get_markers(self):
1975 return self.markers
1977 def mousePressEvent(self, mouse_ev):
1978 self.show_all = False
1979 point = self.mapFromGlobal(mouse_ev.globalPos())
1981 if mouse_ev.button() == qc.Qt.LeftButton:
1982 marker = self.marker_under_cursor(point.x(), point.y())
1983 if self.picking:
1984 if self.picking_down is None:
1985 self.picking_down = (
1986 self.time_projection.rev(mouse_ev.x()),
1987 mouse_ev.y())
1989 elif marker is not None:
1990 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1991 self.deselect_all()
1992 marker.selected = True
1993 self.emit_selected_markers()
1994 self.update()
1995 else:
1996 self.track_start = mouse_ev.x(), mouse_ev.y()
1997 self.track_trange = self.tmin, self.tmax
1999 if mouse_ev.button() == qc.Qt.RightButton \
2000 and isinstance(self.menu, qw.QMenu):
2001 self.menu.exec_(qg.QCursor.pos())
2002 self.update_status()
2004 def mouseReleaseEvent(self, mouse_ev):
2005 if self.ignore_releases:
2006 self.ignore_releases -= 1
2007 return
2009 if self.picking:
2010 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2011 self.emit_selected_markers()
2013 if self.track_start:
2014 self.update()
2016 self.track_start = None
2017 self.track_trange = None
2018 self.update_status()
2020 def mouseDoubleClickEvent(self, mouse_ev):
2021 self.show_all = False
2022 self.start_picking(None)
2023 self.ignore_releases = 1
2025 def mouseMoveEvent(self, mouse_ev):
2026 self.setUpdatesEnabled(False)
2027 point = self.mapFromGlobal(mouse_ev.globalPos())
2029 if self.picking:
2030 self.update_picking(point.x(), point.y())
2032 elif self.track_start is not None:
2033 x0, y0 = self.track_start
2034 dx = (point.x() - x0)/float(self.width())
2035 dy = (point.y() - y0)/float(self.height())
2036 if self.ypart(y0) == 1:
2037 dy = 0
2039 tmin0, tmax0 = self.track_trange
2041 scale = math.exp(-dy*5.)
2042 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2043 frac = x0/float(self.width())
2044 dt = dx*(tmax0-tmin0)*scale
2046 self.interrupt_following()
2047 self.set_time_range(
2048 tmin0 - dt - dtr*frac,
2049 tmax0 - dt + dtr*(1.-frac))
2051 self.update()
2052 else:
2053 self.hoovering(point.x(), point.y())
2055 self.update_status()
2057 def nslc_ids_under_cursor(self, x, y):
2058 ftrack = self.track_to_screen.rev(y)
2059 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2060 return nslc_ids
2062 def marker_under_cursor(self, x, y):
2063 mouset = self.time_projection.rev(x)
2064 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2065 relevant_nslc_ids = None
2066 for marker in self.markers:
2067 if marker.kind not in self.visible_marker_kinds:
2068 continue
2070 if (abs(mouset-marker.tmin) < deltat or
2071 abs(mouset-marker.tmax) < deltat):
2073 if relevant_nslc_ids is None:
2074 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2076 marker_nslc_ids = marker.get_nslc_ids()
2077 if not marker_nslc_ids:
2078 return marker
2080 for nslc_id in marker_nslc_ids:
2081 if nslc_id in relevant_nslc_ids:
2082 return marker
2084 def hoovering(self, x, y):
2085 mouset = self.time_projection.rev(x)
2086 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2087 needupdate = False
2088 haveone = False
2089 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2090 for marker in self.markers:
2091 if marker.kind not in self.visible_marker_kinds:
2092 continue
2094 state = abs(mouset-marker.tmin) < deltat or \
2095 abs(mouset-marker.tmax) < deltat and not haveone
2097 if state:
2098 xstate = False
2100 marker_nslc_ids = marker.get_nslc_ids()
2101 if not marker_nslc_ids:
2102 xstate = True
2104 for nslc in relevant_nslc_ids:
2105 if marker.match_nslc(nslc):
2106 xstate = True
2108 state = xstate
2110 if state:
2111 haveone = True
2112 oldstate = marker.is_alerted()
2113 if oldstate != state:
2114 needupdate = True
2115 marker.set_alerted(state)
2116 if state:
2117 self.message = marker.hoover_message()
2119 if not haveone:
2120 self.message = None
2122 if needupdate:
2123 self.update()
2125 def event(self, event):
2126 if event.type() == qc.QEvent.KeyPress:
2127 self.keyPressEvent(event)
2128 return True
2129 else:
2130 return base.event(self, event)
2132 def keyPressEvent(self, key_event):
2133 self.show_all = False
2134 dt = self.tmax - self.tmin
2135 tmid = (self.tmin + self.tmax) / 2.
2137 key = key_event.key()
2138 try:
2139 keytext = str(key_event.text())
2140 except UnicodeEncodeError:
2141 return
2143 if key == qc.Qt.Key_Space:
2144 self.interrupt_following()
2145 self.set_time_range(self.tmin+dt, self.tmax+dt)
2147 elif key == qc.Qt.Key_Up:
2148 for m in self.selected_markers():
2149 if isinstance(m, PhaseMarker):
2150 if key_event.modifiers() & qc.Qt.ShiftModifier:
2151 p = 0
2152 else:
2153 p = 1 if m.get_polarity() != 1 else None
2154 m.set_polarity(p)
2156 elif key == qc.Qt.Key_Down:
2157 for m in self.selected_markers():
2158 if isinstance(m, PhaseMarker):
2159 if key_event.modifiers() & qc.Qt.ShiftModifier:
2160 p = 0
2161 else:
2162 p = -1 if m.get_polarity() != -1 else None
2163 m.set_polarity(p)
2165 elif key == qc.Qt.Key_B:
2166 dt = self.tmax - self.tmin
2167 self.interrupt_following()
2168 self.set_time_range(self.tmin-dt, self.tmax-dt)
2170 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2171 self.interrupt_following()
2173 tgo = None
2175 class TraceDummy(object):
2176 def __init__(self, marker):
2177 self._marker = marker
2179 @property
2180 def nslc_id(self):
2181 return self._marker.one_nslc()
2183 def marker_to_itrack(marker):
2184 try:
2185 return self.key_to_row.get(
2186 self.gather(TraceDummy(marker)), -1)
2188 except MarkerOneNSLCRequired:
2189 return -1
2191 emarker, pmarkers = self.get_active_markers()
2192 pmarkers = [
2193 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2194 pmarkers.sort(key=lambda m: (
2195 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2197 if key == qc.Qt.Key_Backtab:
2198 pmarkers.reverse()
2200 smarkers = self.selected_markers()
2201 iselected = []
2202 for sm in smarkers:
2203 try:
2204 iselected.append(pmarkers.index(sm))
2205 except ValueError:
2206 pass
2208 if iselected:
2209 icurrent = max(iselected) + 1
2210 else:
2211 icurrent = 0
2213 if icurrent < len(pmarkers):
2214 self.deselect_all()
2215 cmarker = pmarkers[icurrent]
2216 cmarker.selected = True
2217 tgo = cmarker.tmin
2218 if not self.tmin < tgo < self.tmax:
2219 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2221 itrack = marker_to_itrack(cmarker)
2222 if itrack != -1:
2223 if itrack < self.shown_tracks_range[0]:
2224 self.scroll_tracks(
2225 - (self.shown_tracks_range[0] - itrack))
2226 elif self.shown_tracks_range[1] <= itrack:
2227 self.scroll_tracks(
2228 itrack - self.shown_tracks_range[1]+1)
2230 if itrack not in self.track_to_nslc_ids:
2231 self.go_to_selection()
2233 elif keytext in ('p', 'n', 'P', 'N'):
2234 smarkers = self.selected_markers()
2235 tgo = None
2236 dir = str(keytext)
2237 if smarkers:
2238 tmid = smarkers[0].tmin
2239 for smarker in smarkers:
2240 if dir == 'n':
2241 tmid = max(smarker.tmin, tmid)
2242 else:
2243 tmid = min(smarker.tmin, tmid)
2245 tgo = tmid
2247 if dir.lower() == 'n':
2248 for marker in sorted(
2249 self.markers,
2250 key=operator.attrgetter('tmin')):
2252 t = marker.tmin
2253 if t > tmid and \
2254 marker.kind in self.visible_marker_kinds and \
2255 (dir == 'n' or
2256 isinstance(marker, EventMarker)):
2258 self.deselect_all()
2259 marker.selected = True
2260 tgo = t
2261 break
2262 else:
2263 for marker in sorted(
2264 self.markers,
2265 key=operator.attrgetter('tmin'),
2266 reverse=True):
2268 t = marker.tmin
2269 if t < tmid and \
2270 marker.kind in self.visible_marker_kinds and \
2271 (dir == 'p' or
2272 isinstance(marker, EventMarker)):
2273 self.deselect_all()
2274 marker.selected = True
2275 tgo = t
2276 break
2278 if tgo is not None:
2279 self.interrupt_following()
2280 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2282 elif keytext == 'r':
2283 if self.pile.reload_modified():
2284 self.reloaded = True
2286 elif keytext == 'R':
2287 self.setup_snufflings()
2289 elif key == qc.Qt.Key_Backspace:
2290 self.remove_selected_markers()
2292 elif keytext == 'a':
2293 for marker in self.markers:
2294 if ((self.tmin <= marker.tmin <= self.tmax or
2295 self.tmin <= marker.tmax <= self.tmax) and
2296 marker.kind in self.visible_marker_kinds):
2297 marker.selected = True
2298 else:
2299 marker.selected = False
2301 elif keytext == 'A':
2302 for marker in self.markers:
2303 if marker.kind in self.visible_marker_kinds:
2304 marker.selected = True
2306 elif keytext == 'd':
2307 self.deselect_all()
2309 elif keytext == 'E':
2310 self.deactivate_event_marker()
2312 elif keytext == 'e':
2313 markers = self.selected_markers()
2314 event_markers_in_spe = [
2315 marker for marker in markers
2316 if not isinstance(marker, PhaseMarker)]
2318 phase_markers = [
2319 marker for marker in markers
2320 if isinstance(marker, PhaseMarker)]
2322 if len(event_markers_in_spe) == 1:
2323 event_marker = event_markers_in_spe[0]
2324 if not isinstance(event_marker, EventMarker):
2325 nslcs = list(event_marker.nslc_ids)
2326 lat, lon = 0.0, 0.0
2327 old = self.get_active_event()
2328 if len(nslcs) == 1:
2329 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2330 elif old is not None:
2331 lat, lon = old.lat, old.lon
2333 event_marker.convert_to_event_marker(lat, lon)
2335 self.set_active_event_marker(event_marker)
2336 event = event_marker.get_event()
2337 for marker in phase_markers:
2338 marker.set_event(event)
2340 else:
2341 for marker in event_markers_in_spe:
2342 marker.convert_to_event_marker()
2344 elif keytext in ('0', '1', '2', '3', '4', '5'):
2345 for marker in self.selected_markers():
2346 marker.set_kind(int(keytext))
2347 self.emit_selected_markers()
2349 elif key in fkey_map:
2350 self.handle_fkeys(key)
2352 elif key == qc.Qt.Key_Escape:
2353 if self.picking:
2354 self.stop_picking(0, 0, abort=True)
2356 elif key == qc.Qt.Key_PageDown:
2357 self.scroll_tracks(
2358 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2360 elif key == qc.Qt.Key_PageUp:
2361 self.scroll_tracks(
2362 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2364 elif key == qc.Qt.Key_Plus:
2365 self.zoom_tracks(0., 1.)
2367 elif key == qc.Qt.Key_Minus:
2368 self.zoom_tracks(0., -1.)
2370 elif key == qc.Qt.Key_Equal:
2371 ntracks_shown = self.shown_tracks_range[1] - \
2372 self.shown_tracks_range[0]
2373 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2374 self.zoom_tracks(0., dtracks)
2376 elif key == qc.Qt.Key_Colon:
2377 self.want_input.emit()
2379 elif keytext == 'f':
2380 self.toggle_fullscreen()
2382 elif keytext == 'g':
2383 self.go_to_selection()
2385 elif keytext == 'G':
2386 self.go_to_selection(tight=True)
2388 elif keytext == 'm':
2389 self.toggle_marker_editor()
2391 elif keytext == 'c':
2392 self.toggle_main_controls()
2394 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2395 dir = 1
2396 amount = 1
2397 if key_event.key() == qc.Qt.Key_Left:
2398 dir = -1
2399 if key_event.modifiers() & qc.Qt.ShiftModifier:
2400 amount = 10
2401 self.nudge_selected_markers(dir*amount)
2402 else:
2403 super().keyPressEvent(key_event)
2405 if keytext != '' and keytext in 'degaApPnN':
2406 self.emit_selected_markers()
2408 self.update()
2409 self.update_status()
2411 def handle_fkeys(self, key):
2412 self.set_phase_kind(
2413 self.selected_markers(),
2414 fkey_map[key] + 1)
2415 self.emit_selected_markers()
2417 def emit_selected_markers(self):
2418 ibounds = []
2419 last_selected = False
2420 for imarker, marker in enumerate(self.markers):
2421 this_selected = marker.is_selected()
2422 if this_selected != last_selected:
2423 ibounds.append(imarker)
2425 last_selected = this_selected
2427 if last_selected:
2428 ibounds.append(len(self.markers))
2430 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2431 self.n_selected_markers = sum(
2432 chunk[1] - chunk[0] for chunk in chunks)
2433 self.marker_selection_changed.emit(chunks)
2435 def toggle_marker_editor(self):
2436 self.panel_parent.toggle_marker_editor()
2438 def toggle_main_controls(self):
2439 self.panel_parent.toggle_main_controls()
2441 def nudge_selected_markers(self, npixels):
2442 a, b = self.time_projection.ur
2443 c, d = self.time_projection.xr
2444 for marker in self.selected_markers():
2445 if not isinstance(marker, EventMarker):
2446 marker.tmin += npixels * (d-c)/b
2447 marker.tmax += npixels * (d-c)/b
2449 def toggle_fullscreen(self):
2450 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2451 self.window().windowState() & qc.Qt.WindowMaximized:
2452 self.window().showNormal()
2453 else:
2454 if macosx:
2455 self.window().showMaximized()
2456 else:
2457 self.window().showFullScreen()
2459 def about(self):
2460 fn = pyrocko.util.data_file('snuffler.png')
2461 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2462 txt = f.read()
2463 label = qw.QLabel(txt % {'logo': fn})
2464 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2465 self.show_doc('About', [label], target='tab')
2467 def help(self):
2468 class MyScrollArea(qw.QScrollArea):
2470 def sizeHint(self):
2471 s = qc.QSize()
2472 s.setWidth(self.widget().sizeHint().width())
2473 s.setHeight(self.widget().sizeHint().height())
2474 return s
2476 with open(pyrocko.util.data_file(
2477 'snuffler_help.html')) as f:
2478 hcheat = qw.QLabel(f.read())
2480 with open(pyrocko.util.data_file(
2481 'snuffler_help_epilog.html')) as f:
2482 hepilog = qw.QLabel(f.read())
2484 for h in [hcheat, hepilog]:
2485 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2486 h.setWordWrap(True)
2488 self.show_doc('Help', [hcheat, hepilog], target='panel')
2490 def show_doc(self, name, labels, target='panel'):
2491 scroller = qw.QScrollArea()
2492 frame = qw.QFrame(scroller)
2493 frame.setLineWidth(0)
2494 layout = qw.QVBoxLayout()
2495 layout.setContentsMargins(0, 0, 0, 0)
2496 layout.setSpacing(0)
2497 frame.setLayout(layout)
2498 scroller.setWidget(frame)
2499 scroller.setWidgetResizable(True)
2500 frame.setBackgroundRole(qg.QPalette.Base)
2501 for h in labels:
2502 h.setParent(frame)
2503 h.setMargin(3)
2504 h.setTextInteractionFlags(
2505 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2506 h.setBackgroundRole(qg.QPalette.Base)
2507 layout.addWidget(h)
2508 h.linkActivated.connect(
2509 self.open_link)
2511 if self.panel_parent is not None:
2512 if target == 'panel':
2513 self.panel_parent.add_panel(
2514 name, scroller, True, volatile=False)
2515 else:
2516 self.panel_parent.add_tab(name, scroller)
2518 def open_link(self, link):
2519 qg.QDesktopServices.openUrl(qc.QUrl(link))
2521 def wheelEvent(self, wheel_event):
2522 if use_pyqt5:
2523 self.wheel_pos += wheel_event.angleDelta().y()
2524 else:
2525 self.wheel_pos += wheel_event.delta()
2527 n = self.wheel_pos // 120
2528 self.wheel_pos = self.wheel_pos % 120
2529 if n == 0:
2530 return
2532 amount = max(
2533 1.,
2534 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2535 wdelta = amount * n
2537 trmin, trmax = self.track_to_screen.get_in_range()
2538 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2539 / (trmax-trmin)
2541 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2542 self.zoom_tracks(anchor, wdelta)
2543 else:
2544 self.scroll_tracks(-wdelta)
2546 def dragEnterEvent(self, event):
2547 if event.mimeData().hasUrls():
2548 if any(url.toLocalFile() for url in event.mimeData().urls()):
2549 event.setDropAction(qc.Qt.LinkAction)
2550 event.accept()
2552 def dropEvent(self, event):
2553 if event.mimeData().hasUrls():
2554 paths = list(
2555 str(url.toLocalFile()) for url in event.mimeData().urls())
2556 event.acceptProposedAction()
2557 self.load(paths)
2559 def get_phase_name(self, kind):
2560 return self.config.get_phase_name(kind)
2562 def set_phase_kind(self, markers, kind):
2563 phasename = self.get_phase_name(kind)
2565 for marker in markers:
2566 if isinstance(marker, PhaseMarker):
2567 if kind == 10:
2568 marker.convert_to_marker()
2569 else:
2570 marker.set_phasename(phasename)
2571 marker.set_event(self.get_active_event())
2573 elif isinstance(marker, EventMarker):
2574 pass
2576 else:
2577 if kind != 10:
2578 event = self.get_active_event()
2579 marker.convert_to_phase_marker(
2580 event, phasename, None, False)
2582 def set_ntracks(self, ntracks):
2583 if self.ntracks != ntracks:
2584 self.ntracks = ntracks
2585 if self.shown_tracks_range is not None:
2586 l, h = self.shown_tracks_range
2587 else:
2588 l, h = 0, self.ntracks
2590 self.tracks_range_changed.emit(self.ntracks, l, h)
2592 def set_tracks_range(self, range, start=None):
2594 low, high = range
2595 low = min(self.ntracks-1, low)
2596 high = min(self.ntracks, high)
2597 low = max(0, low)
2598 high = max(1, high)
2600 if start is None:
2601 start = float(low)
2603 if self.shown_tracks_range != (low, high):
2604 self.shown_tracks_range = low, high
2605 self.shown_tracks_start = start
2607 self.tracks_range_changed.emit(self.ntracks, low, high)
2609 def scroll_tracks(self, shift):
2610 shown = self.shown_tracks_range
2611 shiftmin = -shown[0]
2612 shiftmax = self.ntracks-shown[1]
2613 shift = max(shiftmin, shift)
2614 shift = min(shiftmax, shift)
2615 shown = shown[0] + shift, shown[1] + shift
2617 self.set_tracks_range((int(shown[0]), int(shown[1])))
2619 self.update()
2621 def zoom_tracks(self, anchor, delta):
2622 ntracks_shown = self.shown_tracks_range[1] \
2623 - self.shown_tracks_range[0]
2625 if (ntracks_shown == 1 and delta <= 0) or \
2626 (ntracks_shown == self.ntracks and delta >= 0):
2627 return
2629 ntracks_shown += int(round(delta))
2630 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2632 u = self.shown_tracks_start
2633 nu = max(0., u-anchor*delta)
2634 nv = nu + ntracks_shown
2635 if nv > self.ntracks:
2636 nu -= nv - self.ntracks
2637 nv -= nv - self.ntracks
2639 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2641 self.ntracks_shown_max = self.shown_tracks_range[1] \
2642 - self.shown_tracks_range[0]
2644 self.update()
2646 def content_time_range(self):
2647 pile = self.get_pile()
2648 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2649 if tmin is None:
2650 tmin = initial_time_range[0]
2651 if tmax is None:
2652 tmax = initial_time_range[1]
2654 return tmin, tmax
2656 def content_deltat_range(self):
2657 pile = self.get_pile()
2659 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2661 if deltatmin is None:
2662 deltatmin = 0.001
2664 if deltatmax is None:
2665 deltatmax = 1000.0
2667 return deltatmin, deltatmax
2669 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2670 if tmax < tmin:
2671 tmin, tmax = tmax, tmin
2673 deltatmin = self.content_deltat_range()[0]
2674 dt = deltatmin * self.visible_length * 0.95
2676 if dt == 0.0:
2677 dt = 1.0
2679 if tight:
2680 if tmax != tmin:
2681 dtm = tmax - tmin
2682 tmin -= dtm*0.1
2683 tmax += dtm*0.1
2684 return tmin, tmax
2685 else:
2686 tcenter = (tmin + tmax) / 2.
2687 tmin = tcenter - 0.5*dt
2688 tmax = tcenter + 0.5*dt
2689 return tmin, tmax
2691 if tmax-tmin < dt:
2692 vmin, vmax = self.get_time_range()
2693 dt = min(vmax - vmin, dt)
2695 tcenter = (tmin+tmax)/2.
2696 etmin, etmax = tmin, tmax
2697 tmin = min(etmin, tcenter - 0.5*dt)
2698 tmax = max(etmax, tcenter + 0.5*dt)
2699 dtm = tmax-tmin
2700 if etmin == tmin:
2701 tmin -= dtm*0.1
2702 if etmax == tmax:
2703 tmax += dtm*0.1
2705 else:
2706 dtm = tmax-tmin
2707 tmin -= dtm*0.1
2708 tmax += dtm*0.1
2710 return tmin, tmax
2712 def go_to_selection(self, tight=False):
2713 markers = self.selected_markers()
2714 if markers:
2715 tmax, tmin = self.content_time_range()
2716 for marker in markers:
2717 tmin = min(tmin, marker.tmin)
2718 tmax = max(tmax, marker.tmax)
2720 else:
2721 if tight:
2722 vmin, vmax = self.get_time_range()
2723 tmin = tmax = (vmin + vmax) / 2.
2724 else:
2725 tmin, tmax = self.content_time_range()
2727 tmin, tmax = self.make_good_looking_time_range(
2728 tmin, tmax, tight=tight)
2730 self.interrupt_following()
2731 self.set_time_range(tmin, tmax)
2732 self.update()
2734 def go_to_time(self, t, tlen=None):
2735 tmax = t
2736 if tlen is not None:
2737 tmax = t+tlen
2738 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2739 self.interrupt_following()
2740 self.set_time_range(tmin, tmax)
2741 self.update()
2743 def go_to_event_by_name(self, name):
2744 for marker in self.markers:
2745 if isinstance(marker, EventMarker):
2746 event = marker.get_event()
2747 if event.name and event.name.lower() == name.lower():
2748 tmin, tmax = self.make_good_looking_time_range(
2749 event.time, event.time)
2751 self.interrupt_following()
2752 self.set_time_range(tmin, tmax)
2754 def printit(self):
2755 from .qt_compat import qprint
2756 printer = qprint.QPrinter()
2757 printer.setOrientation(qprint.QPrinter.Landscape)
2759 dialog = qprint.QPrintDialog(printer, self)
2760 dialog.setWindowTitle('Print')
2762 if dialog.exec_() != qw.QDialog.Accepted:
2763 return
2765 painter = qg.QPainter()
2766 painter.begin(printer)
2767 page = printer.pageRect()
2768 self.drawit(
2769 painter, printmode=False, w=page.width(), h=page.height())
2771 painter.end()
2773 def savesvg(self, fn=None):
2775 if not fn:
2776 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
2777 self,
2778 'Save as SVG|PNG',
2779 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2780 'SVG|PNG (*.svg *.png)',
2781 options=qfiledialog_options))
2783 if fn == '':
2784 return
2786 fn = str(fn)
2788 if fn.lower().endswith('.svg'):
2789 try:
2790 w, h = 842, 595
2791 margin = 0.025
2792 m = max(w, h)*margin
2794 generator = qsvg.QSvgGenerator()
2795 generator.setFileName(fn)
2796 generator.setSize(qc.QSize(w, h))
2797 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2799 painter = qg.QPainter()
2800 painter.begin(generator)
2801 self.drawit(painter, printmode=False, w=w, h=h)
2802 painter.end()
2804 except Exception as e:
2805 self.fail('Failed to write SVG file: %s' % str(e))
2807 elif fn.lower().endswith('.png'):
2808 if use_pyqt5:
2809 pixmap = self.grab()
2810 else:
2811 pixmap = qg.QPixmap().grabWidget(self)
2813 try:
2814 pixmap.save(fn)
2816 except Exception as e:
2817 self.fail('Failed to write PNG file: %s' % str(e))
2819 else:
2820 self.fail(
2821 'Unsupported file type: filename must end with ".svg" or '
2822 '".png".')
2824 def paintEvent(self, paint_ev):
2825 '''
2826 Called by QT whenever widget needs to be painted.
2827 '''
2828 painter = qg.QPainter(self)
2830 if self.menuitem_antialias.isChecked():
2831 painter.setRenderHint(qg.QPainter.Antialiasing)
2833 self.drawit(painter)
2835 logger.debug(
2836 'Time spent drawing: '
2837 ' user:%.3f sys:%.3f children_user:%.3f'
2838 ' childred_sys:%.3f elapsed:%.3f' %
2839 (self.timer_draw - self.timer_cutout))
2841 logger.debug(
2842 'Time spent processing:'
2843 ' user:%.3f sys:%.3f children_user:%.3f'
2844 ' childred_sys:%.3f elapsed:%.3f' %
2845 self.timer_cutout.get())
2847 self.time_spent_painting = self.timer_draw.get()[-1]
2848 self.time_last_painted = time.time()
2850 def determine_box_styles(self):
2852 traces = list(self.pile.iter_traces())
2853 traces.sort(key=operator.attrgetter('full_id'))
2854 istyle = 0
2855 trace_styles = {}
2856 for itr, tr in enumerate(traces):
2857 if itr > 0:
2858 other = traces[itr-1]
2859 if not (
2860 other.nslc_id == tr.nslc_id
2861 and other.deltat == tr.deltat
2862 and abs(other.tmax - tr.tmin)
2863 < gap_lap_tolerance*tr.deltat):
2865 istyle += 1
2867 trace_styles[tr.full_id, tr.deltat] = istyle
2869 self.trace_styles = trace_styles
2871 def draw_trace_boxes(self, p, time_projection, track_projections):
2873 for v_projection in track_projections.values():
2874 v_projection.set_in_range(0., 1.)
2876 def selector(x):
2877 return x.overlaps(*time_projection.get_in_range())
2879 if self.trace_filter is not None:
2880 def tselector(x):
2881 return selector(x) and self.trace_filter(x)
2883 else:
2884 tselector = selector
2886 traces = list(self.pile.iter_traces(
2887 group_selector=selector, trace_selector=tselector))
2889 traces.sort(key=operator.attrgetter('full_id'))
2891 def drawbox(itrack, istyle, traces):
2892 v_projection = track_projections[itrack]
2893 dvmin = v_projection(0.)
2894 dvmax = v_projection(1.)
2895 dtmin = time_projection.clipped(traces[0].tmin)
2896 dtmax = time_projection.clipped(traces[-1].tmax)
2898 style = box_styles[istyle % len(box_styles)]
2899 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2900 p.fillRect(rect, style.fill_brush)
2901 p.setPen(style.frame_pen)
2902 p.drawRect(rect)
2904 traces_by_style = {}
2905 for itr, tr in enumerate(traces):
2906 gt = self.gather(tr)
2907 if gt not in self.key_to_row:
2908 continue
2910 itrack = self.key_to_row[gt]
2911 if itrack not in track_projections:
2912 continue
2914 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2916 if len(traces) < 500:
2917 drawbox(itrack, istyle, [tr])
2918 else:
2919 if (itrack, istyle) not in traces_by_style:
2920 traces_by_style[itrack, istyle] = []
2921 traces_by_style[itrack, istyle].append(tr)
2923 for (itrack, istyle), traces in traces_by_style.items():
2924 drawbox(itrack, istyle, traces)
2926 def draw_visible_markers(
2927 self, p, vcenter_projection, primary_pen):
2929 try:
2930 markers = self.markers.with_key_in_limited(
2931 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2933 except pyrocko.pile.TooMany:
2934 tmin = self.markers[0].tmin
2935 tmax = self.markers[-1].tmax
2936 umin_view, umax_view = self.time_projection.get_out_range()
2937 umin = max(umin_view, self.time_projection(tmin))
2938 umax = min(umax_view, self.time_projection(tmax))
2939 v0, _ = vcenter_projection.get_out_range()
2940 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2942 p.save()
2944 pen = qg.QPen(primary_pen)
2945 pen.setWidth(2)
2946 pen.setStyle(qc.Qt.DotLine)
2947 # pat = [5., 3.]
2948 # pen.setDashPattern(pat)
2949 p.setPen(pen)
2951 if self.n_selected_markers == len(self.markers):
2952 s_selected = ' (all selected)'
2953 elif self.n_selected_markers > 0:
2954 s_selected = ' (%i selected)' % self.n_selected_markers
2955 else:
2956 s_selected = ''
2958 draw_label(
2959 p, umin+10., v0-10.,
2960 '%i Markers' % len(self.markers) + s_selected,
2961 label_bg, 'LB')
2963 line = qc.QLineF(umin, v0, umax, v0)
2964 p.drawLine(line)
2965 p.restore()
2967 return
2969 for marker in markers:
2970 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2971 and marker.kind in self.visible_marker_kinds:
2973 marker.draw(
2974 p, self.time_projection, vcenter_projection,
2975 with_label=True)
2977 def get_squirrel(self):
2978 try:
2979 return self.pile._squirrel
2980 except AttributeError:
2981 return None
2983 def draw_coverage(self, p, time_projection, track_projections):
2984 sq = self.get_squirrel()
2985 if sq is None:
2986 return
2988 def drawbox(itrack, tmin, tmax, style):
2989 v_projection = track_projections[itrack]
2990 dvmin = v_projection(0.)
2991 dvmax = v_projection(1.)
2992 dtmin = time_projection.clipped(tmin)
2993 dtmax = time_projection.clipped(tmax)
2995 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2996 p.fillRect(rect, style.fill_brush)
2997 p.setPen(style.frame_pen)
2998 p.drawRect(rect)
3000 pattern_list = []
3001 pattern_to_itrack = {}
3002 for key in self.track_keys:
3003 itrack = self.key_to_row[key]
3004 if itrack not in track_projections:
3005 continue
3007 pattern = self.track_patterns[itrack]
3008 pattern_to_itrack[tuple(pattern)] = itrack
3009 pattern_list.append(pattern)
3011 vmin, vmax = self.get_time_range()
3013 for kind in ['waveform', 'waveform_promise']:
3014 for entry in sq.get_coverage(
3015 kind, vmin, vmax, pattern_list, limit=500):
3016 pattern, codes, deltat, tmin, tmax, cover_data = entry
3017 itrack = pattern_to_itrack[tuple(pattern)]
3019 if cover_data is None:
3020 drawbox(
3021 itrack, tmin, tmax,
3022 box_styles_coverage[kind][0])
3023 else:
3024 t = None
3025 pcount = 0
3026 for tb, count in cover_data:
3027 if t is not None and tb > t:
3028 if pcount > 0:
3029 drawbox(
3030 itrack, t, tb,
3031 box_styles_coverage[kind][
3032 min(len(box_styles_coverage)-1,
3033 pcount)])
3035 t = tb
3036 pcount = count
3038 def drawit(self, p, printmode=False, w=None, h=None):
3039 '''
3040 This performs the actual drawing.
3041 '''
3043 self.timer_draw.start()
3044 show_boxes = self.menuitem_showboxes.isChecked()
3045 sq = self.get_squirrel()
3047 if self.gather is None:
3048 self.set_gathering()
3050 if self.pile_has_changed:
3052 if not self.sortingmode_change_delayed():
3053 self.sortingmode_change()
3055 if show_boxes and sq is None:
3056 self.determine_box_styles()
3058 self.pile_has_changed = False
3060 if h is None:
3061 h = float(self.height())
3062 if w is None:
3063 w = float(self.width())
3065 if printmode:
3066 primary_color = (0, 0, 0)
3067 else:
3068 primary_color = pyrocko.plot.tango_colors['aluminium5']
3070 primary_pen = qg.QPen(qg.QColor(*primary_color))
3072 ax_h = float(self.ax_height)
3074 vbottom_ax_projection = Projection()
3075 vtop_ax_projection = Projection()
3076 vcenter_projection = Projection()
3078 self.time_projection.set_out_range(0., w)
3079 vbottom_ax_projection.set_out_range(h-ax_h, h)
3080 vtop_ax_projection.set_out_range(0., ax_h)
3081 vcenter_projection.set_out_range(ax_h, h-ax_h)
3082 vcenter_projection.set_in_range(0., 1.)
3083 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3085 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3086 track_projections = {}
3087 for i in range(*self.shown_tracks_range):
3088 proj = Projection()
3089 proj.set_out_range(
3090 self.track_to_screen(i+0.05),
3091 self.track_to_screen(i+1.-0.05))
3093 track_projections[i] = proj
3095 if self.tmin > self.tmax:
3096 return
3098 self.time_projection.set_in_range(self.tmin, self.tmax)
3099 vbottom_ax_projection.set_in_range(0, ax_h)
3101 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3103 yscaler = pyrocko.plot.AutoScaler()
3105 p.setPen(primary_pen)
3107 font = qg.QFont()
3108 font.setBold(True)
3110 axannotfont = qg.QFont()
3111 axannotfont.setBold(True)
3112 axannotfont.setPointSize(8)
3114 processed_traces = self.prepare_cutout2(
3115 self.tmin, self.tmax,
3116 trace_selector=self.trace_selector,
3117 degap=self.menuitem_degap.isChecked(),
3118 demean=self.menuitem_demean.isChecked())
3120 if not printmode and show_boxes:
3121 if (self.view_mode is ViewMode.Wiggle) \
3122 or (self.view_mode is ViewMode.Waterfall
3123 and not processed_traces):
3125 if sq is None:
3126 self.draw_trace_boxes(
3127 p, self.time_projection, track_projections)
3129 else:
3130 self.draw_coverage(
3131 p, self.time_projection, track_projections)
3133 p.setFont(font)
3134 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3136 color_lookup = dict(
3137 [(k, i) for (i, k) in enumerate(self.color_keys)])
3139 self.track_to_nslc_ids = {}
3140 nticks = 0
3141 annot_labels = []
3143 if self.view_mode is ViewMode.Waterfall and processed_traces:
3144 waterfall = self.waterfall
3145 waterfall.set_time_range(self.tmin, self.tmax)
3146 waterfall.set_traces(processed_traces)
3147 waterfall.set_cmap(self.waterfall_cmap)
3148 waterfall.set_integrate(self.waterfall_integrate)
3149 waterfall.set_clip(
3150 self.waterfall_clip_min, self.waterfall_clip_max)
3151 waterfall.show_absolute_values(
3152 self.waterfall_show_absolute)
3154 rect = qc.QRectF(
3155 0, self.ax_height,
3156 self.width(), self.height() - self.ax_height*2
3157 )
3158 waterfall.draw_waterfall(p, rect=rect)
3160 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3161 show_scales = self.menuitem_showscalerange.isChecked() \
3162 or self.menuitem_showscaleaxis.isChecked()
3164 fm = qg.QFontMetrics(axannotfont, p.device())
3165 trackheight = self.track_to_screen(1.-0.05) \
3166 - self.track_to_screen(0.05)
3168 nlinesavail = trackheight/float(fm.lineSpacing())
3170 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3171 if self.menuitem_showscaleaxis.isChecked() \
3172 else 15
3174 yscaler = pyrocko.plot.AutoScaler(
3175 no_exp_interval=(-3, 2), approx_ticks=nticks,
3176 snap=show_scales
3177 and not self.menuitem_showscaleaxis.isChecked())
3179 data_ranges = pyrocko.trace.minmax(
3180 processed_traces,
3181 key=self.scaling_key,
3182 mode=self.scaling_base)
3184 if not self.menuitem_fixscalerange.isChecked():
3185 self.old_data_ranges = data_ranges
3186 else:
3187 data_ranges.update(self.old_data_ranges)
3189 self.apply_scaling_hooks(data_ranges)
3191 trace_to_itrack = {}
3192 track_scaling_keys = {}
3193 track_scaling_colors = {}
3194 for trace in processed_traces:
3195 gt = self.gather(trace)
3196 if gt not in self.key_to_row:
3197 continue
3199 itrack = self.key_to_row[gt]
3200 if itrack not in track_projections:
3201 continue
3203 trace_to_itrack[trace] = itrack
3205 if itrack not in self.track_to_nslc_ids:
3206 self.track_to_nslc_ids[itrack] = set()
3208 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3210 if itrack not in track_scaling_keys:
3211 track_scaling_keys[itrack] = set()
3213 scaling_key = self.scaling_key(trace)
3214 track_scaling_keys[itrack].add(scaling_key)
3216 color = pyrocko.plot.color(
3217 color_lookup[self.color_gather(trace)])
3219 k = itrack, scaling_key
3220 if k not in track_scaling_colors \
3221 and self.menuitem_colortraces.isChecked():
3222 track_scaling_colors[k] = color
3223 else:
3224 track_scaling_colors[k] = primary_color
3226 # y axes, zero lines
3227 trace_projections = {}
3228 for itrack in list(track_projections.keys()):
3229 if itrack not in track_scaling_keys:
3230 continue
3231 uoff = 0
3232 for scaling_key in track_scaling_keys[itrack]:
3233 data_range = data_ranges[scaling_key]
3234 dymin, dymax = data_range
3235 ymin, ymax, yinc = yscaler.make_scale(
3236 (dymin/self.gain, dymax/self.gain))
3237 iexp = yscaler.make_exp(yinc)
3238 factor = 10**iexp
3239 trace_projection = track_projections[itrack].copy()
3240 trace_projection.set_in_range(ymax, ymin)
3241 trace_projections[itrack, scaling_key] = \
3242 trace_projection
3243 umin, umax = self.time_projection.get_out_range()
3244 vmin, vmax = trace_projection.get_out_range()
3245 umax_zeroline = umax
3246 uoffnext = uoff
3248 if show_scales:
3249 pen = qg.QPen(primary_pen)
3250 k = itrack, scaling_key
3251 if k in track_scaling_colors:
3252 c = qg.QColor(*track_scaling_colors[
3253 itrack, scaling_key])
3255 pen.setColor(c)
3257 p.setPen(pen)
3258 if nlinesavail > 3:
3259 if self.menuitem_showscaleaxis.isChecked():
3260 ymin_annot = math.ceil(ymin/yinc)*yinc
3261 ny_annot = int(
3262 math.floor(ymax/yinc)
3263 - math.ceil(ymin/yinc)) + 1
3265 for iy_annot in range(ny_annot):
3266 y = ymin_annot + iy_annot*yinc
3267 v = trace_projection(y)
3268 line = qc.QLineF(
3269 umax-10-uoff, v, umax-uoff, v)
3271 p.drawLine(line)
3272 if iy_annot == ny_annot - 1 \
3273 and iexp != 0:
3274 sexp = ' × ' \
3275 '10<sup>%i</sup>' % iexp
3276 else:
3277 sexp = ''
3279 snum = num_to_html(y/factor)
3280 lab = Label(
3281 p,
3282 umax-20-uoff,
3283 v, '%s%s' % (snum, sexp),
3284 label_bg=None,
3285 anchor='MR',
3286 font=axannotfont,
3287 color=c)
3289 uoffnext = max(
3290 lab.rect.width()+30., uoffnext)
3292 annot_labels.append(lab)
3293 if y == 0.:
3294 umax_zeroline = \
3295 umax - 20 \
3296 - lab.rect.width() - 10 \
3297 - uoff
3298 else:
3299 if not show_boxes:
3300 qpoints = make_QPolygonF(
3301 [umax-20-uoff,
3302 umax-10-uoff,
3303 umax-10-uoff,
3304 umax-20-uoff],
3305 [vmax, vmax, vmin, vmin])
3306 p.drawPolyline(qpoints)
3308 snum = num_to_html(ymin)
3309 labmin = Label(
3310 p, umax-15-uoff, vmax, snum,
3311 label_bg=None,
3312 anchor='BR',
3313 font=axannotfont,
3314 color=c)
3316 annot_labels.append(labmin)
3317 snum = num_to_html(ymax)
3318 labmax = Label(
3319 p, umax-15-uoff, vmin, snum,
3320 label_bg=None,
3321 anchor='TR',
3322 font=axannotfont,
3323 color=c)
3325 annot_labels.append(labmax)
3327 for lab in (labmin, labmax):
3328 uoffnext = max(
3329 lab.rect.width()+10., uoffnext)
3331 if self.menuitem_showzeroline.isChecked():
3332 v = trace_projection(0.)
3333 if vmin <= v <= vmax:
3334 line = qc.QLineF(umin, v, umax_zeroline, v)
3335 p.drawLine(line)
3337 uoff = uoffnext
3339 p.setFont(font)
3340 p.setPen(primary_pen)
3341 for trace in processed_traces:
3342 if self.view_mode is not ViewMode.Wiggle:
3343 break
3345 if trace not in trace_to_itrack:
3346 continue
3348 itrack = trace_to_itrack[trace]
3349 scaling_key = self.scaling_key(trace)
3350 trace_projection = trace_projections[
3351 itrack, scaling_key]
3353 vdata = trace_projection(trace.get_ydata())
3355 udata_min = float(self.time_projection(trace.tmin))
3356 udata_max = float(self.time_projection(
3357 trace.tmin+trace.deltat*(vdata.size-1)))
3358 udata = num.linspace(udata_min, udata_max, vdata.size)
3360 qpoints = make_QPolygonF(udata, vdata)
3362 umin, umax = self.time_projection.get_out_range()
3363 vmin, vmax = trace_projection.get_out_range()
3365 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3367 if self.menuitem_cliptraces.isChecked():
3368 p.setClipRect(trackrect)
3370 if self.menuitem_colortraces.isChecked():
3371 color = pyrocko.plot.color(
3372 color_lookup[self.color_gather(trace)])
3373 pen = qg.QPen(qg.QColor(*color), 1)
3374 p.setPen(pen)
3376 p.drawPolyline(qpoints)
3378 if self.floating_marker:
3379 self.floating_marker.draw_trace(
3380 self, p, trace,
3381 self.time_projection, trace_projection, 1.0)
3383 for marker in self.markers.with_key_in(
3384 self.tmin - self.markers_deltat_max,
3385 self.tmax):
3387 if marker.tmin < self.tmax \
3388 and self.tmin < marker.tmax \
3389 and marker.kind \
3390 in self.visible_marker_kinds:
3391 marker.draw_trace(
3392 self, p, trace, self.time_projection,
3393 trace_projection, 1.0)
3395 p.setPen(primary_pen)
3397 if self.menuitem_cliptraces.isChecked():
3398 p.setClipRect(0, 0, int(w), int(h))
3400 if self.floating_marker:
3401 self.floating_marker.draw(
3402 p, self.time_projection, vcenter_projection)
3404 self.draw_visible_markers(
3405 p, vcenter_projection, primary_pen)
3407 p.setPen(primary_pen)
3408 while font.pointSize() > 2:
3409 fm = qg.QFontMetrics(font, p.device())
3410 trackheight = self.track_to_screen(1.-0.05) \
3411 - self.track_to_screen(0.05)
3412 nlinesavail = trackheight/float(fm.lineSpacing())
3413 if nlinesavail > 1:
3414 break
3416 font.setPointSize(font.pointSize()-1)
3418 p.setFont(font)
3419 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3421 for key in self.track_keys:
3422 itrack = self.key_to_row[key]
3423 if itrack in track_projections:
3424 plabel = ' '.join(
3425 [str(x) for x in key if x is not None])
3426 lx = 10
3427 ly = self.track_to_screen(itrack+0.5)
3429 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3430 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3431 continue
3433 contains_cursor = \
3434 self.track_to_screen(itrack) \
3435 < mouse_pos.y() \
3436 < self.track_to_screen(itrack+1)
3438 if not contains_cursor:
3439 continue
3441 font_large = p.font()
3442 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3443 p.setFont(font_large)
3444 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3445 p.setFont(font)
3447 for lab in annot_labels:
3448 lab.draw()
3450 self.timer_draw.stop()
3452 def see_data_params(self):
3454 min_deltat = self.content_deltat_range()[0]
3456 # determine padding and downampling requirements
3457 if self.lowpass is not None:
3458 deltat_target = 1./self.lowpass * 0.25
3459 ndecimate = min(
3460 50,
3461 max(1, int(round(deltat_target / min_deltat))))
3462 tpad = 1./self.lowpass * 2.
3463 else:
3464 ndecimate = 1
3465 tpad = min_deltat*5.
3467 if self.highpass is not None:
3468 tpad = max(1./self.highpass * 2., tpad)
3470 nsee_points_per_trace = 5000*10
3471 tsee = ndecimate*nsee_points_per_trace*min_deltat
3473 return ndecimate, tpad, tsee
3475 def clean_update(self):
3476 self.cached_processed_traces = None
3477 self.cached_chopped_traces = {}
3478 self.update()
3480 def get_adequate_tpad(self):
3481 tpad = 0.
3482 for f in [self.highpass, self.lowpass]:
3483 if f is not None:
3484 tpad = max(tpad, 1.0/f)
3486 for snuffling in self.snufflings:
3487 if snuffling._post_process_hook_enabled \
3488 or snuffling._pre_process_hook_enabled:
3490 tpad = max(tpad, snuffling.get_tpad())
3492 return tpad
3494 def prepare_cutout2(
3495 self, tmin, tmax, trace_selector=None, degap=True,
3496 demean=True, nmax=6000):
3498 if self.pile.is_empty():
3499 return []
3501 nmax = self.visible_length
3503 self.timer_cutout.start()
3505 tsee = tmax-tmin
3506 min_deltat_wo_decimate = tsee/nmax
3507 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3509 min_deltat_allow = min_deltat_wo_decimate
3510 if self.lowpass is not None:
3511 target_deltat_lp = 0.25/self.lowpass
3512 if target_deltat_lp > min_deltat_wo_decimate:
3513 min_deltat_allow = min_deltat_w_decimate
3515 min_deltat_allow = math.exp(
3516 int(math.floor(math.log(min_deltat_allow))))
3518 tmin_ = tmin
3519 tmax_ = tmax
3521 # fetch more than needed?
3522 if self.menuitem_liberal_fetch.isChecked():
3523 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3524 tmin = math.floor(tmin/tlen) * tlen
3525 tmax = math.ceil(tmax/tlen) * tlen
3527 fft_filtering = self.menuitem_fft_filtering.isChecked()
3528 lphp = self.menuitem_lphp.isChecked()
3529 ads = self.menuitem_allowdownsampling.isChecked()
3531 tpad = self.get_adequate_tpad()
3532 tpad = max(tpad, tsee)
3534 # state vector to decide if cached traces can be used
3535 vec = (
3536 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3537 self.highpass, fft_filtering, lphp,
3538 min_deltat_allow, self.rotate, self.shown_tracks_range,
3539 ads, self.pile.get_update_count())
3541 dtmin = 0. if not self.cached_vec else tmin_ - self.cached_vec[0]
3542 dtmax = 0. if not self.cached_vec else tmax_ - self.cached_vec[1]
3544 if (self.cached_vec
3545 and self.cached_vec[0] <= vec[0]
3546 and vec[1] <= self.cached_vec[1]
3547 and vec[2:] == self.cached_vec[2:]
3548 and not (self.reloaded or self.menuitem_watch.isChecked())
3549 and self.cached_processed_traces is not None):
3551 logger.debug('Using cached traces')
3552 processed_traces = self.cached_processed_traces
3554 else:
3555 processed_traces = []
3556 if self.pile.deltatmax >= min_deltat_allow:
3558 def group_selector(gr):
3559 return gr.deltatmax >= min_deltat_allow
3561 if trace_selector is not None:
3562 def trace_selectorx(tr):
3563 return tr.deltat >= min_deltat_allow \
3564 and trace_selector(tr)
3565 else:
3566 def trace_selectorx(tr):
3567 return tr.deltat >= min_deltat_allow
3569 for traces in self.pile.chopper(
3570 tmin=tmin, tmax=tmax, tpad=tpad,
3571 want_incomplete=True,
3572 degap=degap,
3573 maxgap=gap_lap_tolerance,
3574 maxlap=gap_lap_tolerance,
3575 keep_current_files_open=True,
3576 group_selector=group_selector,
3577 trace_selector=trace_selectorx,
3578 accessor_id=id(self),
3579 snap=(math.floor, math.ceil),
3580 include_last=True):
3582 if demean:
3583 for tr in traces:
3584 if (tr.meta and tr.meta.get('tabu', False)):
3585 continue
3586 y = tr.get_ydata()
3587 tr.set_ydata(y - num.mean(y))
3589 traces = self.pre_process_hooks(traces)
3591 for trace in traces:
3593 if not (trace.meta
3594 and trace.meta.get('tabu', False)):
3596 if fft_filtering:
3597 but = pyrocko.response.ButterworthResponse
3598 multres = pyrocko.response.MultiplyResponse
3599 if self.lowpass is not None \
3600 or self.highpass is not None:
3602 it = num.arange(
3603 trace.data_len(), dtype=float)
3604 detr_data, m, b = detrend(
3605 it, trace.get_ydata())
3607 trace.set_ydata(detr_data)
3609 freqs, fdata = trace.spectrum(
3610 pad_to_pow2=True, tfade=None)
3612 nfreqs = fdata.size
3614 key = (trace.deltat, nfreqs)
3616 if key not in self.tf_cache:
3617 resps = []
3618 if self.lowpass is not None:
3619 resps.append(but(
3620 order=4,
3621 corner=self.lowpass,
3622 type='low'))
3624 if self.highpass is not None:
3625 resps.append(but(
3626 order=4,
3627 corner=self.highpass,
3628 type='high'))
3630 resp = multres(resps)
3631 self.tf_cache[key] = \
3632 resp.evaluate(freqs)
3634 filtered_data = num.fft.irfft(
3635 fdata*self.tf_cache[key]
3636 )[:trace.data_len()]
3638 retrended_data = retrend(
3639 it, filtered_data, m, b)
3641 trace.set_ydata(retrended_data)
3643 else:
3645 if ads and self.lowpass is not None:
3646 while trace.deltat \
3647 < min_deltat_wo_decimate:
3649 trace.downsample(2, demean=False)
3651 fmax = 0.5/trace.deltat
3652 if not lphp and (
3653 self.lowpass is not None
3654 and self.highpass is not None
3655 and self.lowpass < fmax
3656 and self.highpass < fmax
3657 and self.highpass < self.lowpass):
3659 trace.bandpass(
3660 2, self.highpass, self.lowpass)
3661 else:
3662 if self.lowpass is not None:
3663 if self.lowpass < 0.5/trace.deltat:
3664 trace.lowpass(
3665 4, self.lowpass,
3666 demean=False)
3668 if self.highpass is not None:
3669 if self.lowpass is None \
3670 or self.highpass \
3671 < self.lowpass:
3673 if self.highpass < \
3674 0.5/trace.deltat:
3675 trace.highpass(
3676 4, self.highpass,
3677 demean=False)
3679 processed_traces.append(trace)
3681 if self.rotate != 0.0:
3682 phi = self.rotate/180.*math.pi
3683 cphi = math.cos(phi)
3684 sphi = math.sin(phi)
3685 for a in processed_traces:
3686 for b in processed_traces:
3687 if (a.network == b.network
3688 and a.station == b.station
3689 and a.location == b.location
3690 and ((a.channel.lower().endswith('n')
3691 and b.channel.lower().endswith('e'))
3692 or (a.channel.endswith('1')
3693 and b.channel.endswith('2')))
3694 and abs(a.deltat-b.deltat) < a.deltat*0.001
3695 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3696 len(a.get_ydata()) == len(b.get_ydata())):
3698 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3699 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3700 a.set_ydata(aydata)
3701 b.set_ydata(bydata)
3703 processed_traces = self.post_process_hooks(processed_traces)
3705 self.cached_processed_traces = processed_traces
3706 self.cached_vec = vec
3708 chopped_traces = []
3709 for trace in processed_traces:
3710 chop_tmin = tmin_ - trace.deltat*4
3711 chop_tmax = tmax_ + trace.deltat*4
3712 trace_hash = trace.hash(unsafe=True)
3714 # Use cache if tmin and tmax have not changed
3715 if dtmin == 0. and dtmax == 0. \
3716 and trace_hash in self.cached_chopped_traces:
3717 ctrace = self.cached_chopped_traces[trace_hash]
3719 else:
3720 try:
3721 ctrace = trace.chop(
3722 chop_tmin, chop_tmax,
3723 inplace=False)
3725 except pyrocko.trace.NoData:
3726 continue
3728 if ctrace.data_len() < 2:
3729 continue
3731 self.cached_chopped_traces[trace_hash] = ctrace
3732 chopped_traces.append(ctrace)
3734 self.timer_cutout.stop()
3735 return chopped_traces
3737 def pre_process_hooks(self, traces):
3738 for snuffling in self.snufflings:
3739 if snuffling._pre_process_hook_enabled:
3740 traces = snuffling.pre_process_hook(traces)
3742 return traces
3744 def post_process_hooks(self, traces):
3745 for snuffling in self.snufflings:
3746 if snuffling._post_process_hook_enabled:
3747 traces = snuffling.post_process_hook(traces)
3749 return traces
3751 def visible_length_change(self, ignore=None):
3752 for menuitem, vlen in self.menuitems_visible_length:
3753 if menuitem.isChecked():
3754 self.visible_length = vlen
3756 def scaling_base_change(self, ignore=None):
3757 for menuitem, scaling_base in self.menuitems_scaling_base:
3758 if menuitem.isChecked():
3759 self.scaling_base = scaling_base
3761 def scalingmode_change(self, ignore=None):
3762 for menuitem, scaling_key in self.menuitems_scaling:
3763 if menuitem.isChecked():
3764 self.scaling_key = scaling_key
3765 self.update()
3767 def apply_scaling_hooks(self, data_ranges):
3768 for k in sorted(self.scaling_hooks.keys()):
3769 hook = self.scaling_hooks[k]
3770 hook(data_ranges)
3772 def viewmode_change(self, ignore=True):
3773 for item, mode in self.menuitems_viewmode:
3774 if item.isChecked():
3775 self.view_mode = mode
3776 break
3777 else:
3778 raise AttributeError('unknown view mode')
3780 items_waterfall_disabled = (
3781 self.menuitem_showscaleaxis,
3782 self.menuitem_showscalerange,
3783 self.menuitem_showzeroline,
3784 self.menuitem_colortraces,
3785 self.menuitem_cliptraces,
3786 *(itm[0] for itm in self.menuitems_visible_length)
3787 )
3789 if self.view_mode is ViewMode.Waterfall:
3790 self.parent().show_colorbar_ctrl(True)
3791 self.parent().show_gain_ctrl(False)
3793 for item in items_waterfall_disabled:
3794 item.setDisabled(True)
3796 self.visible_length = 180.
3797 else:
3798 self.parent().show_colorbar_ctrl(False)
3799 self.parent().show_gain_ctrl(True)
3801 for item in items_waterfall_disabled:
3802 item.setDisabled(False)
3804 self.visible_length_change()
3805 self.update()
3807 def set_scaling_hook(self, k, hook):
3808 self.scaling_hooks[k] = hook
3810 def remove_scaling_hook(self, k):
3811 del self.scaling_hooks[k]
3813 def remove_scaling_hooks(self):
3814 self.scaling_hooks = {}
3816 def s_sortingmode_change(self, ignore=None):
3817 for menuitem, valfunc in self.menuitems_ssorting:
3818 if menuitem.isChecked():
3819 self._ssort = valfunc
3821 self.sortingmode_change()
3823 def sortingmode_change(self, ignore=None):
3824 for menuitem, (gather, color) in self.menuitems_sorting:
3825 if menuitem.isChecked():
3826 self.set_gathering(gather, color)
3828 self.sortingmode_change_time = time.time()
3830 def lowpass_change(self, value, ignore=None):
3831 self.lowpass = value
3832 self.passband_check()
3833 self.tf_cache = {}
3834 self.update()
3836 def highpass_change(self, value, ignore=None):
3837 self.highpass = value
3838 self.passband_check()
3839 self.tf_cache = {}
3840 self.update()
3842 def passband_check(self):
3843 if self.highpass and self.lowpass \
3844 and self.highpass >= self.lowpass:
3846 self.message = 'Corner frequency of highpass larger than ' \
3847 'corner frequency of lowpass! I will now ' \
3848 'deactivate the highpass.'
3850 self.update_status()
3851 else:
3852 oldmess = self.message
3853 self.message = None
3854 if oldmess is not None:
3855 self.update_status()
3857 def gain_change(self, value, ignore):
3858 self.gain = value
3859 self.update()
3861 def rot_change(self, value, ignore):
3862 self.rotate = value
3863 self.update()
3865 def waterfall_cmap_change(self, cmap):
3866 self.waterfall_cmap = cmap
3867 self.update()
3869 def waterfall_clip_change(self, clip_min, clip_max):
3870 self.waterfall_clip_min = clip_min
3871 self.waterfall_clip_max = clip_max
3872 self.update()
3874 def waterfall_show_absolute_change(self, toggle):
3875 self.waterfall_show_absolute = toggle
3876 self.update()
3878 def waterfall_set_integrate(self, toggle):
3879 self.waterfall_integrate = toggle
3880 self.update()
3882 def set_selected_markers(self, markers):
3883 '''
3884 Set a list of markers selected
3886 :param markers: list of markers
3887 '''
3888 self.deselect_all()
3889 for m in markers:
3890 m.selected = True
3892 self.update()
3894 def deselect_all(self):
3895 for marker in self.markers:
3896 marker.selected = False
3898 def animate_picking(self):
3899 point = self.mapFromGlobal(qg.QCursor.pos())
3900 self.update_picking(point.x(), point.y(), doshift=True)
3902 def get_nslc_ids_for_track(self, ftrack):
3903 itrack = int(ftrack)
3904 return self.track_to_nslc_ids.get(itrack, [])
3906 def stop_picking(self, x, y, abort=False):
3907 if self.picking:
3908 self.update_picking(x, y, doshift=False)
3909 self.picking = None
3910 self.picking_down = None
3911 self.picking_timer.stop()
3912 self.picking_timer = None
3913 if not abort:
3914 self.add_marker(self.floating_marker)
3915 self.floating_marker.selected = True
3916 self.emit_selected_markers()
3918 self.floating_marker = None
3920 def start_picking(self, ignore):
3922 if not self.picking:
3923 self.deselect_all()
3924 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3925 point = self.mapFromGlobal(qg.QCursor.pos())
3927 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3928 self.picking.setGeometry(
3929 gpoint.x(), gpoint.y(), 1, self.height())
3930 t = self.time_projection.rev(point.x())
3932 ftrack = self.track_to_screen.rev(point.y())
3933 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3934 self.floating_marker = Marker(nslc_ids, t, t)
3935 self.floating_marker.selected = True
3937 self.picking_timer = qc.QTimer()
3938 self.picking_timer.timeout.connect(
3939 self.animate_picking)
3941 self.picking_timer.setInterval(50)
3942 self.picking_timer.start()
3944 def update_picking(self, x, y, doshift=False):
3945 if self.picking:
3946 mouset = self.time_projection.rev(x)
3947 dt = 0.0
3948 if mouset < self.tmin or mouset > self.tmax:
3949 if mouset < self.tmin:
3950 dt = -(self.tmin - mouset)
3951 else:
3952 dt = mouset - self.tmax
3953 ddt = self.tmax-self.tmin
3954 dt = max(dt, -ddt/10.)
3955 dt = min(dt, ddt/10.)
3957 x0 = x
3958 if self.picking_down is not None:
3959 x0 = self.time_projection(self.picking_down[0])
3961 w = abs(x-x0)
3962 x0 = min(x0, x)
3964 tmin, tmax = (
3965 self.time_projection.rev(x0),
3966 self.time_projection.rev(x0+w))
3968 tmin, tmax = (
3969 max(working_system_time_range[0], tmin),
3970 min(working_system_time_range[1], tmax))
3972 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3974 self.picking.setGeometry(
3975 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3977 ftrack = self.track_to_screen.rev(y)
3978 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3979 self.floating_marker.set(nslc_ids, tmin, tmax)
3981 if dt != 0.0 and doshift:
3982 self.interrupt_following()
3983 self.set_time_range(self.tmin+dt, self.tmax+dt)
3985 self.update()
3987 def update_status(self):
3989 if self.message is None:
3990 point = self.mapFromGlobal(qg.QCursor.pos())
3992 mouse_t = self.time_projection.rev(point.x())
3993 if not is_working_time(mouse_t):
3994 return
3996 if self.floating_marker:
3997 tmi, tma = (
3998 self.floating_marker.tmin,
3999 self.floating_marker.tmax)
4001 tt, ms = gmtime_x(tmi)
4003 if tmi == tma:
4004 message = mystrftime(
4005 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4006 tt=tt, milliseconds=ms)
4007 else:
4008 srange = '%g s' % (tma-tmi)
4009 message = mystrftime(
4010 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4011 tt=tt, milliseconds=ms)
4012 else:
4013 tt, ms = gmtime_x(mouse_t)
4015 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4016 else:
4017 message = self.message
4019 sb = self.window().statusBar()
4020 sb.clearMessage()
4021 sb.showMessage(message)
4023 def set_sortingmode_change_delay_time(self, dt):
4024 self.sortingmode_change_delay_time = dt
4026 def sortingmode_change_delayed(self):
4027 now = time.time()
4028 return (
4029 self.sortingmode_change_delay_time is not None
4030 and now - self.sortingmode_change_time
4031 < self.sortingmode_change_delay_time)
4033 def set_visible_marker_kinds(self, kinds):
4034 self.deselect_all()
4035 self.visible_marker_kinds = tuple(kinds)
4036 self.emit_selected_markers()
4038 def following(self):
4039 return self.follow_timer is not None \
4040 and not self.following_interrupted()
4042 def interrupt_following(self):
4043 self.interactive_range_change_time = time.time()
4045 def following_interrupted(self, now=None):
4046 if now is None:
4047 now = time.time()
4048 return now - self.interactive_range_change_time \
4049 < self.interactive_range_change_delay_time
4051 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4052 if tmax_start is None:
4053 tmax_start = time.time()
4054 self.show_all = False
4055 self.follow_time = tlen
4056 self.follow_timer = qc.QTimer(self)
4057 self.follow_timer.timeout.connect(
4058 self.follow_update)
4059 self.follow_timer.setInterval(interval)
4060 self.follow_timer.start()
4061 self.follow_started = time.time()
4062 self.follow_lapse = lapse
4063 self.follow_tshift = self.follow_started - tmax_start
4064 self.interactive_range_change_time = 0.0
4066 def unfollow(self):
4067 if self.follow_timer is not None:
4068 self.follow_timer.stop()
4069 self.follow_timer = None
4070 self.interactive_range_change_time = 0.0
4072 def follow_update(self):
4073 rnow = time.time()
4074 if self.follow_lapse is None:
4075 now = rnow
4076 else:
4077 now = self.follow_started + (rnow - self.follow_started) \
4078 * self.follow_lapse
4080 if self.following_interrupted(rnow):
4081 return
4082 self.set_time_range(
4083 now-self.follow_time-self.follow_tshift,
4084 now-self.follow_tshift)
4086 self.update()
4088 def myclose(self, return_tag=''):
4089 self.return_tag = return_tag
4090 self.window().close()
4092 def cleanup(self):
4093 self.about_to_close.emit()
4094 self.timer.stop()
4095 if self.follow_timer is not None:
4096 self.follow_timer.stop()
4098 for snuffling in list(self.snufflings):
4099 self.remove_snuffling(snuffling)
4101 def set_error_message(self, key, value):
4102 if value is None:
4103 if key in self.error_messages:
4104 del self.error_messages[key]
4105 else:
4106 self.error_messages[key] = value
4108 def inputline_changed(self, text):
4109 pass
4111 def inputline_finished(self, text):
4112 line = str(text)
4114 toks = line.split()
4115 clearit, hideit, error = False, True, None
4116 if len(toks) >= 1:
4117 command = toks[0].lower()
4119 try:
4120 quick_filter_commands = {
4121 'n': '%s.*.*.*',
4122 's': '*.%s.*.*',
4123 'l': '*.*.%s.*',
4124 'c': '*.*.*.%s'}
4126 if command in quick_filter_commands:
4127 if len(toks) >= 2:
4128 patterns = [
4129 quick_filter_commands[toks[0]] % pat
4130 for pat in toks[1:]]
4131 self.set_quick_filter_patterns(patterns, line)
4132 else:
4133 self.set_quick_filter_patterns(None)
4135 self.update()
4137 elif command in ('hide', 'unhide'):
4138 if len(toks) >= 2:
4139 patterns = []
4140 if len(toks) == 2:
4141 patterns = [toks[1]]
4142 elif len(toks) >= 3:
4143 x = {
4144 'n': '%s.*.*.*',
4145 's': '*.%s.*.*',
4146 'l': '*.*.%s.*',
4147 'c': '*.*.*.%s'}
4149 if toks[1] in x:
4150 patterns.extend(
4151 x[toks[1]] % tok for tok in toks[2:])
4153 for pattern in patterns:
4154 if command == 'hide':
4155 self.add_blacklist_pattern(pattern)
4156 else:
4157 self.remove_blacklist_pattern(pattern)
4159 elif command == 'unhide' and len(toks) == 1:
4160 self.clear_blacklist()
4162 clearit = True
4164 self.update()
4166 elif command == 'markers':
4167 if len(toks) == 2:
4168 if toks[1] == 'all':
4169 kinds = self.all_marker_kinds
4170 else:
4171 kinds = []
4172 for x in toks[1]:
4173 try:
4174 kinds.append(int(x))
4175 except Exception:
4176 pass
4178 self.set_visible_marker_kinds(kinds)
4180 elif len(toks) == 1:
4181 self.set_visible_marker_kinds(())
4183 self.update()
4185 elif command == 'scaling':
4186 if len(toks) == 2:
4187 hideit = False
4188 error = 'wrong number of arguments'
4190 if len(toks) >= 3:
4191 vmin, vmax = [
4192 pyrocko.model.float_or_none(x)
4193 for x in toks[-2:]]
4195 def upd(d, k, vmin, vmax):
4196 if k in d:
4197 if vmin is not None:
4198 d[k] = vmin, d[k][1]
4199 if vmax is not None:
4200 d[k] = d[k][0], vmax
4202 if len(toks) == 1:
4203 self.remove_scaling_hooks()
4205 elif len(toks) == 3:
4206 def hook(data_ranges):
4207 for k in data_ranges:
4208 upd(data_ranges, k, vmin, vmax)
4210 self.set_scaling_hook('_', hook)
4212 elif len(toks) == 4:
4213 pattern = toks[1]
4215 def hook(data_ranges):
4216 for k in pyrocko.util.match_nslcs(
4217 pattern, list(data_ranges.keys())):
4219 upd(data_ranges, k, vmin, vmax)
4221 self.set_scaling_hook(pattern, hook)
4223 elif command == 'goto':
4224 toks2 = line.split(None, 1)
4225 if len(toks2) == 2:
4226 arg = toks2[1]
4227 m = re.match(
4228 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4229 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4230 if m:
4231 tlen = None
4232 if not m.group(1):
4233 tlen = 12*32*24*60*60
4234 elif not m.group(2):
4235 tlen = 32*24*60*60
4236 elif not m.group(3):
4237 tlen = 24*60*60
4238 elif not m.group(4):
4239 tlen = 60*60
4240 elif not m.group(5):
4241 tlen = 60
4243 supl = '1970-01-01 00:00:00'
4244 if len(supl) > len(arg):
4245 arg = arg + supl[-(len(supl)-len(arg)):]
4246 t = pyrocko.util.str_to_time(arg)
4247 self.go_to_time(t, tlen=tlen)
4249 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4250 supl = '00:00:00'
4251 if len(supl) > len(arg):
4252 arg = arg + supl[-(len(supl)-len(arg)):]
4253 tmin, tmax = self.get_time_range()
4254 sdate = pyrocko.util.time_to_str(
4255 tmin/2.+tmax/2., format='%Y-%m-%d')
4256 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4257 self.go_to_time(t)
4259 elif arg == 'today':
4260 self.go_to_time(
4261 day_start(
4262 time.time()), tlen=24*60*60)
4264 elif arg == 'yesterday':
4265 self.go_to_time(
4266 day_start(
4267 time.time()-24*60*60), tlen=24*60*60)
4269 else:
4270 self.go_to_event_by_name(arg)
4272 else:
4273 raise PileViewerMainException(
4274 'No such command: %s' % command)
4276 except PileViewerMainException as e:
4277 error = str(e)
4278 hideit = False
4280 return clearit, hideit, error
4282 return PileViewerMain
4285PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4286GLPileViewerMain = MakePileViewerMainClass(qgl.QGLWidget)
4289class LineEditWithAbort(qw.QLineEdit):
4291 aborted = qc.pyqtSignal()
4292 history_down = qc.pyqtSignal()
4293 history_up = qc.pyqtSignal()
4295 def keyPressEvent(self, key_event):
4296 if key_event.key() == qc.Qt.Key_Escape:
4297 self.aborted.emit()
4298 elif key_event.key() == qc.Qt.Key_Down:
4299 self.history_down.emit()
4300 elif key_event.key() == qc.Qt.Key_Up:
4301 self.history_up.emit()
4302 else:
4303 return qw.QLineEdit.keyPressEvent(self, key_event)
4306class PileViewer(qw.QFrame):
4307 '''
4308 PileViewerMain + Controls + Inputline
4309 '''
4311 def __init__(
4312 self, pile,
4313 ntracks_shown_max=20,
4314 marker_editor_sortable=True,
4315 use_opengl=False,
4316 panel_parent=None,
4317 *args):
4319 qw.QFrame.__init__(self, *args)
4321 layout = qw.QGridLayout()
4322 layout.setContentsMargins(0, 0, 0, 0)
4323 layout.setSpacing(0)
4325 self.menu = PileViewerMenuBar(self)
4327 if use_opengl:
4328 self.viewer = GLPileViewerMain(
4329 pile,
4330 ntracks_shown_max=ntracks_shown_max,
4331 panel_parent=panel_parent,
4332 menu=self.menu)
4333 else:
4334 self.viewer = PileViewerMain(
4335 pile,
4336 ntracks_shown_max=ntracks_shown_max,
4337 panel_parent=panel_parent,
4338 menu=self.menu)
4340 self.marker_editor_sortable = marker_editor_sortable
4342 self.setFrameShape(qw.QFrame.StyledPanel)
4343 self.setFrameShadow(qw.QFrame.Sunken)
4345 self.input_area = qw.QFrame(self)
4346 ia_layout = qw.QGridLayout()
4347 ia_layout.setContentsMargins(11, 11, 11, 11)
4348 self.input_area.setLayout(ia_layout)
4350 self.inputline = LineEditWithAbort(self.input_area)
4351 self.inputline.returnPressed.connect(
4352 self.inputline_returnpressed)
4353 self.inputline.editingFinished.connect(
4354 self.inputline_finished)
4355 self.inputline.aborted.connect(
4356 self.inputline_aborted)
4358 self.inputline.history_down.connect(
4359 lambda: self.step_through_history(1))
4360 self.inputline.history_up.connect(
4361 lambda: self.step_through_history(-1))
4363 self.inputline.textEdited.connect(
4364 self.inputline_changed)
4366 self.inputline.setPlaceholderText(
4367 u'Quick commands: e.g. \'c HH?\' to select channels. '
4368 u'Use ↑ or ↓ to navigate.')
4369 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4370 self.input_area.hide()
4371 self.history = None
4373 self.inputline_error_str = None
4375 self.inputline_error = qw.QLabel()
4376 self.inputline_error.hide()
4378 ia_layout.addWidget(self.inputline, 0, 0)
4379 ia_layout.addWidget(self.inputline_error, 1, 0)
4380 layout.addWidget(self.input_area, 0, 0, 1, 2)
4381 layout.addWidget(self.viewer, 1, 0)
4383 pb = Progressbars(self)
4384 layout.addWidget(pb, 2, 0, 1, 2)
4385 self.progressbars = pb
4387 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4388 self.scrollbar = scrollbar
4389 layout.addWidget(scrollbar, 1, 1)
4390 self.scrollbar.valueChanged.connect(
4391 self.scrollbar_changed)
4393 self.block_scrollbar_changes = False
4395 self.viewer.want_input.connect(
4396 self.inputline_show)
4397 self.viewer.tracks_range_changed.connect(
4398 self.tracks_range_changed)
4399 self.viewer.pile_has_changed_signal.connect(
4400 self.adjust_controls)
4401 self.viewer.about_to_close.connect(
4402 self.save_inputline_history)
4404 self.setLayout(layout)
4406 def cleanup(self):
4407 self.viewer.cleanup()
4409 def get_progressbars(self):
4410 return self.progressbars
4412 def inputline_show(self):
4413 if not self.history:
4414 self.load_inputline_history()
4416 self.input_area.show()
4417 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4418 self.inputline.selectAll()
4420 def inputline_set_error(self, string):
4421 self.inputline_error_str = string
4422 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4423 self.inputline.selectAll()
4424 self.inputline_error.setText(string)
4425 self.input_area.show()
4426 self.inputline_error.show()
4428 def inputline_clear_error(self):
4429 if self.inputline_error_str:
4430 self.inputline.setPalette(qw.QApplication.palette())
4431 self.inputline_error_str = None
4432 self.inputline_error.clear()
4433 self.inputline_error.hide()
4435 def inputline_changed(self, line):
4436 self.viewer.inputline_changed(str(line))
4437 self.inputline_clear_error()
4439 def inputline_returnpressed(self):
4440 line = str(self.inputline.text())
4441 clearit, hideit, error = self.viewer.inputline_finished(line)
4443 if error:
4444 self.inputline_set_error(error)
4446 line = line.strip()
4448 if line != '' and not error:
4449 if not (len(self.history) >= 1 and line == self.history[-1]):
4450 self.history.append(line)
4452 if clearit:
4454 self.inputline.blockSignals(True)
4455 qpat, qinp = self.viewer.get_quick_filter_patterns()
4456 if qpat is None:
4457 self.inputline.clear()
4458 else:
4459 self.inputline.setText(qinp)
4460 self.inputline.blockSignals(False)
4462 if hideit and not error:
4463 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4464 self.input_area.hide()
4466 self.hist_ind = len(self.history)
4468 def inputline_aborted(self):
4469 '''
4470 Hide the input line.
4471 '''
4472 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4473 self.hist_ind = len(self.history)
4474 self.input_area.hide()
4476 def save_inputline_history(self):
4477 '''
4478 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4479 '''
4480 if not self.history:
4481 return
4483 conf = pyrocko.config
4484 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4485 with open(fn_hist, 'w') as f:
4486 i = min(100, len(self.history))
4487 for c in self.history[-i:]:
4488 f.write('%s\n' % c)
4490 def load_inputline_history(self):
4491 '''
4492 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4493 '''
4494 conf = pyrocko.config
4495 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4496 if not os.path.exists(fn_hist):
4497 with open(fn_hist, 'w+') as f:
4498 f.write('\n')
4500 with open(fn_hist, 'r') as f:
4501 self.history = [line.strip() for line in f.readlines()]
4503 self.hist_ind = len(self.history)
4505 def step_through_history(self, ud=1):
4506 '''
4507 Step through input line history and set the input line text.
4508 '''
4509 n = len(self.history)
4510 self.hist_ind += ud
4511 self.hist_ind %= (n + 1)
4512 if len(self.history) != 0 and self.hist_ind != n:
4513 self.inputline.setText(self.history[self.hist_ind])
4514 else:
4515 self.inputline.setText('')
4517 def inputline_finished(self):
4518 pass
4520 def tracks_range_changed(self, ntracks, ilo, ihi):
4521 if self.block_scrollbar_changes:
4522 return
4524 self.scrollbar.blockSignals(True)
4525 self.scrollbar.setPageStep(ihi-ilo)
4526 vmax = max(0, ntracks-(ihi-ilo))
4527 self.scrollbar.setRange(0, vmax)
4528 self.scrollbar.setValue(ilo)
4529 self.scrollbar.setHidden(vmax == 0)
4530 self.scrollbar.blockSignals(False)
4532 def scrollbar_changed(self, value):
4533 self.block_scrollbar_changes = True
4534 ilo = value
4535 ihi = ilo + self.scrollbar.pageStep()
4536 self.viewer.set_tracks_range((ilo, ihi))
4537 self.block_scrollbar_changes = False
4538 self.update_contents()
4540 def controls(self):
4541 frame = qw.QFrame(self)
4542 layout = qw.QGridLayout()
4543 frame.setLayout(layout)
4545 minfreq = 0.001
4546 maxfreq = 1000.0
4547 self.lowpass_control = ValControl(high_is_none=True)
4548 self.lowpass_control.setup(
4549 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4550 self.highpass_control = ValControl(low_is_none=True)
4551 self.highpass_control.setup(
4552 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4553 self.gain_control = ValControl()
4554 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4555 self.rot_control = LinValControl()
4556 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4557 self.colorbar_control = ColorbarControl(self)
4559 self.lowpass_control.valchange.connect(
4560 self.viewer.lowpass_change)
4561 self.highpass_control.valchange.connect(
4562 self.viewer.highpass_change)
4563 self.gain_control.valchange.connect(
4564 self.viewer.gain_change)
4565 self.rot_control.valchange.connect(
4566 self.viewer.rot_change)
4567 self.colorbar_control.cmap_changed.connect(
4568 self.viewer.waterfall_cmap_change
4569 )
4570 self.colorbar_control.clip_changed.connect(
4571 self.viewer.waterfall_clip_change
4572 )
4573 self.colorbar_control.show_absolute_toggled.connect(
4574 self.viewer.waterfall_show_absolute_change
4575 )
4576 self.colorbar_control.show_integrate_toggled.connect(
4577 self.viewer.waterfall_set_integrate
4578 )
4580 for icontrol, control in enumerate((
4581 self.highpass_control,
4582 self.lowpass_control,
4583 self.gain_control,
4584 self.rot_control,
4585 self.colorbar_control)):
4587 for iwidget, widget in enumerate(control.widgets()):
4588 layout.addWidget(widget, icontrol, iwidget)
4590 spacer = qw.QSpacerItem(
4591 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4592 layout.addItem(spacer, 4, 0, 1, 3)
4594 self.adjust_controls()
4595 self.viewer.viewmode_change(ViewMode.Wiggle)
4596 return frame
4598 def marker_editor(self):
4599 editor = pyrocko.gui.marker_editor.MarkerEditor(
4600 self, sortable=self.marker_editor_sortable)
4602 editor.set_viewer(self.get_view())
4603 editor.get_marker_model().dataChanged.connect(
4604 self.update_contents)
4605 return editor
4607 def adjust_controls(self):
4608 dtmin, dtmax = self.viewer.content_deltat_range()
4609 maxfreq = 0.5/dtmin
4610 minfreq = (0.5/dtmax)*0.001
4611 self.lowpass_control.set_range(minfreq, maxfreq)
4612 self.highpass_control.set_range(minfreq, maxfreq)
4614 def setup_snufflings(self):
4615 self.viewer.setup_snufflings()
4617 def get_view(self):
4618 return self.viewer
4620 def update_contents(self):
4621 self.viewer.update()
4623 def get_pile(self):
4624 return self.viewer.get_pile()
4626 def show_colorbar_ctrl(self, show):
4627 for w in self.colorbar_control.widgets():
4628 w.setVisible(show)
4630 def show_gain_ctrl(self, show):
4631 for w in self.gain_control.widgets():
4632 w.setVisible(show)