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
138 def __lt__(self, other):
139 if other is None:
140 return True
141 return float(self) < float(other)
143 def __gt__(self, other):
144 if other is None:
145 return False
146 return float(self) > float(other)
149def deg_float_or_none(x):
150 if x is None:
151 return None
152 else:
153 return deg_float(x)
156class sector_int(int):
158 def __str__(self):
159 return '[%i]' % self
161 def __lt__(self, other):
162 if other is None:
163 return True
164 return int(self) < int(other)
166 def __gt__(self, other):
167 if other is None:
168 return False
169 return int(self) > int(other)
172def num_to_html(num):
173 snum = '%g' % num
174 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
175 if m:
176 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
178 return snum
181gap_lap_tolerance = 5.
184class ViewMode(enum.Enum):
185 Wiggle = 1
186 Waterfall = 2
189class Timer(object):
190 def __init__(self):
191 self._start = None
192 self._stop = None
194 def start(self):
195 self._start = os.times()
197 def stop(self):
198 self._stop = os.times()
200 def get(self):
201 a = self._start
202 b = self._stop
203 if a is not None and b is not None:
204 return tuple([b[i] - a[i] for i in range(5)])
205 else:
206 return tuple([0.] * 5)
208 def __sub__(self, other):
209 a = self.get()
210 b = other.get()
211 return tuple([a[i] - b[i] for i in range(5)])
214class ObjectStyle(object):
215 def __init__(self, frame_pen, fill_brush):
216 self.frame_pen = frame_pen
217 self.fill_brush = fill_brush
220box_styles = []
221box_alpha = 100
222for color in 'orange skyblue butter chameleon chocolate plum ' \
223 'scarletred'.split():
225 box_styles.append(ObjectStyle(
226 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
227 qg.QBrush(qg.QColor(
228 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
229 ))
231box_styles_coverage = {}
233box_styles_coverage['waveform'] = [
234 ObjectStyle(
235 qg.QPen(
236 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
237 1, qc.Qt.DashLine),
238 qg.QBrush(qg.QColor(
239 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
240 ),
241 ObjectStyle(
242 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
243 qg.QBrush(qg.QColor(
244 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
245 ),
246 ObjectStyle(
247 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
248 qg.QBrush(qg.QColor(
249 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
250 )]
252box_styles_coverage['waveform_promise'] = [
253 ObjectStyle(
254 qg.QPen(
255 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
256 1, qc.Qt.DashLine),
257 qg.QBrush(qg.QColor(
258 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
259 ),
260 ObjectStyle(
261 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
262 qg.QBrush(qg.QColor(
263 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
264 ),
265 ObjectStyle(
266 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
267 qg.QBrush(qg.QColor(
268 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
269 )]
271sday = 60*60*24. # \
272smonth = 60*60*24*30. # | only used as approx. intervals...
273syear = 60*60*24*365. # /
275acceptable_tincs = num.array([
276 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
277 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
280working_system_time_range = \
281 pyrocko.util.working_system_time_range()
283initial_time_range = []
285try:
286 initial_time_range.append(
287 calendar.timegm((1950, 1, 1, 0, 0, 0)))
288except Exception:
289 initial_time_range.append(working_system_time_range[0])
291try:
292 initial_time_range.append(
293 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
294except Exception:
295 initial_time_range.append(working_system_time_range[1])
298def is_working_time(t):
299 return working_system_time_range[0] <= t and \
300 t <= working_system_time_range[1]
303def fancy_time_ax_format(inc):
304 l0_fmt_brief = ''
305 l2_fmt = ''
306 l2_trig = 0
307 if inc < 0.000001:
308 l0_fmt = '.%n'
309 l0_center = False
310 l1_fmt = '%H:%M:%S'
311 l1_trig = 6
312 l2_fmt = '%b %d, %Y'
313 l2_trig = 3
314 elif inc < 0.001:
315 l0_fmt = '.%u'
316 l0_center = False
317 l1_fmt = '%H:%M:%S'
318 l1_trig = 6
319 l2_fmt = '%b %d, %Y'
320 l2_trig = 3
321 elif inc < 1:
322 l0_fmt = '.%r'
323 l0_center = False
324 l1_fmt = '%H:%M:%S'
325 l1_trig = 6
326 l2_fmt = '%b %d, %Y'
327 l2_trig = 3
328 elif inc < 60:
329 l0_fmt = '%H:%M:%S'
330 l0_center = False
331 l1_fmt = '%b %d, %Y'
332 l1_trig = 3
333 elif inc < 3600:
334 l0_fmt = '%H:%M'
335 l0_center = False
336 l1_fmt = '%b %d, %Y'
337 l1_trig = 3
338 elif inc < sday:
339 l0_fmt = '%H:%M'
340 l0_center = False
341 l1_fmt = '%b %d, %Y'
342 l1_trig = 3
343 elif inc < smonth:
344 l0_fmt = '%a %d'
345 l0_fmt_brief = '%d'
346 l0_center = True
347 l1_fmt = '%b, %Y'
348 l1_trig = 2
349 elif inc < syear:
350 l0_fmt = '%b'
351 l0_center = True
352 l1_fmt = '%Y'
353 l1_trig = 1
354 else:
355 l0_fmt = '%Y'
356 l0_center = False
357 l1_fmt = ''
358 l1_trig = 0
360 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
363def day_start(timestamp):
364 tt = time.gmtime(int(timestamp))
365 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
366 return calendar.timegm(tts)
369def month_start(timestamp):
370 tt = time.gmtime(int(timestamp))
371 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
372 return calendar.timegm(tts)
375def year_start(timestamp):
376 tt = time.gmtime(int(timestamp))
377 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
378 return calendar.timegm(tts)
381def time_nice_value(inc0):
382 if inc0 < acceptable_tincs[0]:
383 return pyrocko.plot.nice_value(inc0)
384 elif inc0 > acceptable_tincs[-1]:
385 return pyrocko.plot.nice_value(inc0/syear)*syear
386 else:
387 i = num.argmin(num.abs(acceptable_tincs-inc0))
388 return acceptable_tincs[i]
391class TimeScaler(pyrocko.plot.AutoScaler):
392 def __init__(self):
393 pyrocko.plot.AutoScaler.__init__(self)
394 self.mode = 'min-max'
396 def make_scale(self, data_range):
397 assert self.mode in ('min-max', 'off'), \
398 'mode must be "min-max" or "off" for TimeScaler'
400 data_min = min(data_range)
401 data_max = max(data_range)
402 is_reverse = (data_range[0] > data_range[1])
404 mi, ma = data_min, data_max
405 nmi = mi
406 if self.mode != 'off':
407 nmi = mi - self.space*(ma-mi)
409 nma = ma
410 if self.mode != 'off':
411 nma = ma + self.space*(ma-mi)
413 mi, ma = nmi, nma
415 if mi == ma and self.mode != 'off':
416 mi -= 1.0
417 ma += 1.0
419 mi = max(working_system_time_range[0], mi)
420 ma = min(working_system_time_range[1], ma)
422 # make nice tick increment
423 if self.inc is not None:
424 inc = self.inc
425 else:
426 if self.approx_ticks > 0.:
427 inc = time_nice_value((ma-mi)/self.approx_ticks)
428 else:
429 inc = time_nice_value((ma-mi)*10.)
431 if inc == 0.0:
432 inc = 1.0
434 if is_reverse:
435 return ma, mi, -inc
436 else:
437 return mi, ma, inc
439 def make_ticks(self, data_range):
440 mi, ma, inc = self.make_scale(data_range)
442 is_reverse = False
443 if inc < 0:
444 mi, ma, inc = ma, mi, -inc
445 is_reverse = True
447 ticks = []
449 if inc < sday:
450 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
451 if inc < 0.001:
452 mi_day = hpfloat(mi_day)
454 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
455 if inc < 0.001:
456 base = hpfloat(base)
458 base_day = mi_day
459 i = 0
460 while True:
461 tick = base+i*inc
462 if tick > ma:
463 break
465 tick_day = day_start(tick)
466 if tick_day > base_day:
467 base_day = tick_day
468 base = base_day
469 i = 0
470 else:
471 ticks.append(tick)
472 i += 1
474 elif inc < smonth:
475 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
476 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
477 delta = datetime.timedelta(days=int(round(inc/sday)))
478 if mi_day == mi:
479 dt_base += delta
480 i = 0
481 while True:
482 current = dt_base + i*delta
483 tick = calendar.timegm(current.timetuple())
484 if tick > ma:
485 break
486 ticks.append(tick)
487 i += 1
489 elif inc < syear:
490 mi_month = month_start(max(
491 mi, working_system_time_range[0]+smonth*1.5))
493 y, m = time.gmtime(mi_month)[:2]
494 while True:
495 tick = calendar.timegm((y, m, 1, 0, 0, 0))
496 m += 1
497 if m > 12:
498 y, m = y+1, 1
500 if tick > ma:
501 break
503 if tick >= mi:
504 ticks.append(tick)
506 else:
507 mi_year = year_start(max(
508 mi, working_system_time_range[0]+syear*1.5))
510 incy = int(round(inc/syear))
511 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
513 while True:
514 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
515 y += incy
516 if tick > ma:
517 break
518 if tick >= mi:
519 ticks.append(tick)
521 if is_reverse:
522 ticks.reverse()
524 return ticks, inc
527def need_l1_tick(tt, ms, l1_trig):
528 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
531def tick_to_labels(tick, inc):
532 tt, ms = gmtime_x(tick)
533 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
534 fancy_time_ax_format(inc)
536 l0 = mystrftime(l0_fmt, tt, ms)
537 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
538 l1, l2 = None, None
539 if need_l1_tick(tt, ms, l1_trig):
540 l1 = mystrftime(l1_fmt, tt, ms)
541 if need_l1_tick(tt, ms, l2_trig):
542 l2 = mystrftime(l2_fmt, tt, ms)
544 return l0, l0_brief, l0_center, l1, l2
547def l1_l2_tick(tick, inc):
548 tt, ms = gmtime_x(tick)
549 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
550 fancy_time_ax_format(inc)
552 l1 = mystrftime(l1_fmt, tt, ms)
553 l2 = mystrftime(l2_fmt, tt, ms)
554 return l1, l2
557class TimeAx(TimeScaler):
558 def __init__(self, *args):
559 TimeScaler.__init__(self, *args)
561 def drawit(self, p, xprojection, yprojection):
562 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
563 p.setPen(pen)
564 font = qg.QFont()
565 font.setBold(True)
566 p.setFont(font)
567 fm = p.fontMetrics()
568 ticklen = 10
569 pad = 10
570 tmin, tmax = xprojection.get_in_range()
571 ticks, inc = self.make_ticks((tmin, tmax))
572 l1_hits = 0
573 l2_hits = 0
575 vmin, vmax = yprojection(0), yprojection(ticklen)
576 uumin, uumax = xprojection.get_out_range()
577 first_tick_with_label = None
578 for tick in ticks:
579 umin = xprojection(tick)
581 umin_approx_next = xprojection(tick+inc)
582 umax = xprojection(tick)
584 pinc_approx = umin_approx_next - umin
586 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
587 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
589 if tick == 0.0 and tmax - tmin < 3600*24:
590 # hide year at epoch (we assume that synthetic data is shown)
591 if l2:
592 l2 = None
593 elif l1:
594 l1 = None
596 if l0_center:
597 ushift = (umin_approx_next-umin)/2.
598 else:
599 ushift = 0.
601 for l0x in (l0, l0_brief, ''):
602 label0 = l0x
603 rect0 = fm.boundingRect(label0)
604 if rect0.width() <= pinc_approx*0.9:
605 break
607 if uumin+pad < umin-rect0.width()/2.+ushift and \
608 umin+rect0.width()/2.+ushift < uumax-pad:
610 if first_tick_with_label is None:
611 first_tick_with_label = tick
612 p.drawText(qc.QPointF(
613 umin-rect0.width()/2.+ushift,
614 vmin+rect0.height()+ticklen), label0)
616 if l1:
617 label1 = l1
618 rect1 = fm.boundingRect(label1)
619 if uumin+pad < umin-rect1.width()/2. and \
620 umin+rect1.width()/2. < uumax-pad:
622 p.drawText(qc.QPointF(
623 umin-rect1.width()/2.,
624 vmin+rect0.height()+rect1.height()+ticklen),
625 label1)
627 l1_hits += 1
629 if l2:
630 label2 = l2
631 rect2 = fm.boundingRect(label2)
632 if uumin+pad < umin-rect2.width()/2. and \
633 umin+rect2.width()/2. < uumax-pad:
635 p.drawText(qc.QPointF(
636 umin-rect2.width()/2.,
637 vmin+rect0.height()+rect1.height()+rect2.height() +
638 ticklen), label2)
640 l2_hits += 1
642 if first_tick_with_label is None:
643 first_tick_with_label = tmin
645 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
647 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
648 tmax - tmin < 3600*24:
650 # hide year at epoch (we assume that synthetic data is shown)
651 if l2:
652 l2 = None
653 elif l1:
654 l1 = None
656 if l1_hits == 0 and l1:
657 label1 = l1
658 rect1 = fm.boundingRect(label1)
659 p.drawText(qc.QPointF(
660 uumin+pad,
661 vmin+rect0.height()+rect1.height()+ticklen),
662 label1)
664 l1_hits += 1
666 if l2_hits == 0 and l2:
667 label2 = l2
668 rect2 = fm.boundingRect(label2)
669 p.drawText(qc.QPointF(
670 uumin+pad,
671 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
672 label2)
674 v = yprojection(0)
675 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
678class Projection(object):
679 def __init__(self):
680 self.xr = 0., 1.
681 self.ur = 0., 1.
683 def set_in_range(self, xmin, xmax):
684 if xmax == xmin:
685 xmax = xmin + 1.
687 self.xr = xmin, xmax
689 def get_in_range(self):
690 return self.xr
692 def set_out_range(self, umin, umax):
693 if umax == umin:
694 umax = umin + 1.
696 self.ur = umin, umax
698 def get_out_range(self):
699 return self.ur
701 def __call__(self, x):
702 umin, umax = self.ur
703 xmin, xmax = self.xr
704 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
706 def clipped(self, x):
707 umin, umax = self.ur
708 xmin, xmax = self.xr
709 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
711 def rev(self, u):
712 umin, umax = self.ur
713 xmin, xmax = self.xr
714 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
716 def copy(self):
717 return copy.copy(self)
720def add_radiobuttongroup(menu, menudef, target, default=None):
721 group = qw.QActionGroup(menu)
722 group.setExclusive(True)
723 menuitems = []
725 for name, value, *shortcut in menudef:
726 action = menu.addAction(name)
727 action.setCheckable(True)
728 action.setActionGroup(group)
729 if shortcut:
730 action.setShortcut(shortcut[0])
732 menuitems.append((action, value))
733 if default is not None and (
734 name.lower().replace(' ', '_') == default or
735 value == default):
736 action.setChecked(True)
738 group.triggered.connect(target)
740 if default is None:
741 menuitems[0][0].setChecked(True)
743 return menuitems
746def sort_actions(menu):
747 actions = [act for act in menu.actions() if not act.menu()]
748 for action in actions:
749 menu.removeAction(action)
750 actions.sort(key=lambda x: newstr(x.text()))
752 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
753 if help_action:
754 actions.insert(0, actions.pop(actions.index(help_action[0])))
755 for action in actions:
756 menu.addAction(action)
759fkey_map = dict(zip(
760 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
761 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
762 range(10)))
765class PileViewerMainException(Exception):
766 pass
769class PileViewerMenuBar(qw.QMenuBar):
770 ...
773class PileViewerMenu(qw.QMenu):
774 ...
777def MakePileViewerMainClass(base):
779 class PileViewerMain(base):
781 want_input = qc.pyqtSignal()
782 about_to_close = qc.pyqtSignal()
783 pile_has_changed_signal = qc.pyqtSignal()
784 tracks_range_changed = qc.pyqtSignal(int, int, int)
786 begin_markers_add = qc.pyqtSignal(int, int)
787 end_markers_add = qc.pyqtSignal()
788 begin_markers_remove = qc.pyqtSignal(int, int)
789 end_markers_remove = qc.pyqtSignal()
791 marker_selection_changed = qc.pyqtSignal(list)
792 active_event_marker_changed = qc.pyqtSignal()
794 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
795 menu=None):
796 if base == qgl.QGLWidget:
797 from OpenGL import GL # noqa
799 base.__init__(
800 self, qgl.QGLFormat(qgl.QGL.SampleBuffers), *args)
801 else:
802 base.__init__(self, *args)
804 self.pile = pile
805 self.ax_height = 80
806 self.panel_parent = panel_parent
808 self.click_tolerance = 5
810 self.ntracks_shown_max = ntracks_shown_max
811 self.initial_ntracks_shown_max = ntracks_shown_max
812 self.ntracks = 0
813 self.show_all = True
814 self.shown_tracks_range = None
815 self.track_start = None
816 self.track_trange = None
818 self.lowpass = None
819 self.highpass = None
820 self.gain = 1.0
821 self.rotate = 0.0
822 self.picking_down = None
823 self.picking = None
824 self.floating_marker = None
825 self.markers = pyrocko.pile.Sorted([], 'tmin')
826 self.markers_deltat_max = 0.
827 self.n_selected_markers = 0
828 self.all_marker_kinds = (0, 1, 2, 3, 4, 5)
829 self.visible_marker_kinds = self.all_marker_kinds
830 self.active_event_marker = None
831 self.ignore_releases = 0
832 self.message = None
833 self.reloaded = False
834 self.pile_has_changed = False
835 self.config = pyrocko.config.config('snuffler')
837 self.tax = TimeAx()
838 self.setBackgroundRole(qg.QPalette.Base)
839 self.setAutoFillBackground(True)
840 poli = qw.QSizePolicy(
841 qw.QSizePolicy.Expanding,
842 qw.QSizePolicy.Expanding)
844 self.setSizePolicy(poli)
845 self.setMinimumSize(300, 200)
846 self.setFocusPolicy(qc.Qt.ClickFocus)
848 self.menu = menu or PileViewerMenu(self)
850 file_menu = self.menu.addMenu('&File')
851 view_menu = self.menu.addMenu('&View')
852 options_menu = self.menu.addMenu('&Options')
853 scale_menu = self.menu.addMenu('&Scaling')
854 sort_menu = self.menu.addMenu('Sor&ting')
855 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
857 help_menu = self.menu.addMenu('&Help')
859 self.snufflings_menu = self.toggle_panel_menu.addMenu(
860 'Run Snuffling')
861 self.toggle_panel_menu.addSeparator()
862 self.snuffling_help = help_menu.addMenu('Snuffling Help')
863 help_menu.addSeparator()
865 file_menu.addAction(
866 qg.QIcon.fromTheme('document-open'),
867 'Open waveform files...',
868 self.open_waveforms,
869 qg.QKeySequence.Open)
871 file_menu.addAction(
872 qg.QIcon.fromTheme('document-open'),
873 'Open waveform directory...',
874 self.open_waveform_directory)
876 file_menu.addAction(
877 'Open station files...',
878 self.open_stations)
880 file_menu.addAction(
881 'Open StationXML files...',
882 self.open_stations_xml)
884 file_menu.addAction(
885 'Open event file...',
886 self.read_events)
888 file_menu.addSeparator()
889 file_menu.addAction(
890 'Open marker file...',
891 self.read_markers)
893 file_menu.addAction(
894 qg.QIcon.fromTheme('document-save'),
895 'Save markers...',
896 self.write_markers,
897 qg.QKeySequence.Save)
899 file_menu.addAction(
900 qg.QIcon.fromTheme('document-save-as'),
901 'Save selected markers...',
902 self.write_selected_markers,
903 qg.QKeySequence.SaveAs)
905 file_menu.addSeparator()
906 file_menu.addAction(
907 qg.QIcon.fromTheme('document-print'),
908 'Print',
909 self.printit,
910 qg.QKeySequence.Print)
912 file_menu.addAction(
913 qg.QIcon.fromTheme('insert-image'),
914 'Save as SVG or PNG',
915 self.savesvg,
916 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
918 file_menu.addSeparator()
919 close = file_menu.addAction(
920 qg.QIcon.fromTheme('window-close'),
921 'Close',
922 self.myclose)
923 close.setShortcuts(
924 (qg.QKeySequence(qc.Qt.Key_Q),
925 qg.QKeySequence(qc.Qt.Key_X)))
927 # Scale Menu
928 menudef = [
929 ('Individual Scale',
930 lambda tr: tr.nslc_id,
931 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
932 ('Common Scale',
933 lambda tr: None,
934 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
935 ('Common Scale per Station',
936 lambda tr: (tr.network, tr.station),
937 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
938 ('Common Scale per Station Location',
939 lambda tr: (tr.network, tr.station, tr.location)),
940 ('Common Scale per Component',
941 lambda tr: (tr.channel)),
942 ]
944 self.menuitems_scaling = add_radiobuttongroup(
945 scale_menu, menudef, self.scalingmode_change,
946 default=self.config.trace_scale)
947 scale_menu.addSeparator()
949 self.scaling_key = self.menuitems_scaling[0][1]
950 self.scaling_hooks = {}
951 self.scalingmode_change()
953 menudef = [
954 ('Scaling based on Minimum and Maximum', 'minmax'),
955 ('Scaling based on Mean ± 2x Std. Deviation', 2),
956 ('Scaling based on Mean ± 4x Std. Deviation', 4),
957 ]
959 self.menuitems_scaling_base = add_radiobuttongroup(
960 scale_menu, menudef, self.scaling_base_change)
962 self.scaling_base = self.menuitems_scaling_base[0][1]
963 scale_menu.addSeparator()
965 self.menuitem_fixscalerange = scale_menu.addAction(
966 'Fix Scale Ranges')
967 self.menuitem_fixscalerange.setCheckable(True)
969 # Sort Menu
970 def sector_dist(sta):
971 if sta.dist_m is None:
972 return None, None
973 else:
974 return (
975 sector_int(round((sta.azimuth+15.)/30.)),
976 m_float(sta.dist_m))
978 menudef = [
979 ('Sort by Names',
980 lambda tr: (),
981 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
982 ('Sort by Distance',
983 lambda tr: self.station_attrib(
984 tr,
985 lambda sta: (m_float_or_none(sta.dist_m),),
986 lambda tr: (None,)),
987 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
988 ('Sort by Azimuth',
989 lambda tr: self.station_attrib(
990 tr,
991 lambda sta: (deg_float_or_none(sta.azimuth),),
992 lambda tr: (None,))),
993 ('Sort by Distance in 12 Azimuthal Blocks',
994 lambda tr: self.station_attrib(
995 tr,
996 sector_dist,
997 lambda tr: (None, None))),
998 ('Sort by Backazimuth',
999 lambda tr: self.station_attrib(
1000 tr,
1001 lambda sta: (deg_float_or_none(sta.backazimuth),),
1002 lambda tr: (None,))),
1003 ]
1004 self.menuitems_ssorting = add_radiobuttongroup(
1005 sort_menu, menudef, self.s_sortingmode_change)
1006 sort_menu.addSeparator()
1008 self._ssort = lambda tr: ()
1010 self.menu.addSeparator()
1012 menudef = [
1013 ('Subsort by Network, Station, Location, Channel',
1014 ((0, 1, 2, 3), # gathering
1015 lambda tr: tr.location)), # coloring
1016 ('Subsort by Network, Station, Channel, Location',
1017 ((0, 1, 3, 2),
1018 lambda tr: tr.channel)),
1019 ('Subsort by Station, Network, Channel, Location',
1020 ((1, 0, 3, 2),
1021 lambda tr: tr.channel)),
1022 ('Subsort by Location, Network, Station, Channel',
1023 ((2, 0, 1, 3),
1024 lambda tr: tr.channel)),
1025 ('Subsort by Channel, Network, Station, Location',
1026 ((3, 0, 1, 2),
1027 lambda tr: (tr.network, tr.station, tr.location))),
1028 ('Subsort by Network, Station, Channel (Grouped by Location)',
1029 ((0, 1, 3),
1030 lambda tr: tr.location)),
1031 ('Subsort by Station, Network, Channel (Grouped by Location)',
1032 ((1, 0, 3),
1033 lambda tr: tr.location)),
1034 ]
1036 self.menuitems_sorting = add_radiobuttongroup(
1037 sort_menu, menudef, self.sortingmode_change)
1039 menudef = [(x.key, x.value) for x in
1040 self.config.visible_length_setting]
1042 # View menu
1043 self.menuitems_visible_length = add_radiobuttongroup(
1044 view_menu, menudef,
1045 self.visible_length_change)
1046 view_menu.addSeparator()
1048 view_modes = [
1049 ('Wiggle Plot', ViewMode.Wiggle),
1050 ('Waterfall', ViewMode.Waterfall)
1051 ]
1053 self.menuitems_viewmode = add_radiobuttongroup(
1054 view_menu, view_modes,
1055 self.viewmode_change, default=ViewMode.Wiggle)
1056 view_menu.addSeparator()
1058 self.menuitem_cliptraces = view_menu.addAction(
1059 'Clip Traces')
1060 self.menuitem_cliptraces.setCheckable(True)
1061 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1063 self.menuitem_showboxes = view_menu.addAction(
1064 'Show Boxes')
1065 self.menuitem_showboxes.setCheckable(True)
1066 self.menuitem_showboxes.setChecked(
1067 self.config.show_boxes)
1069 self.menuitem_colortraces = view_menu.addAction(
1070 'Color Traces')
1071 self.menuitem_colortraces.setCheckable(True)
1072 self.menuitem_antialias = view_menu.addAction(
1073 'Antialiasing')
1074 self.menuitem_antialias.setCheckable(True)
1076 view_menu.addSeparator()
1077 self.menuitem_showscalerange = view_menu.addAction(
1078 'Show Scale Ranges')
1079 self.menuitem_showscalerange.setCheckable(True)
1080 self.menuitem_showscalerange.setChecked(
1081 self.config.show_scale_ranges)
1083 self.menuitem_showscaleaxis = view_menu.addAction(
1084 'Show Scale Axes')
1085 self.menuitem_showscaleaxis.setCheckable(True)
1086 self.menuitem_showscaleaxis.setChecked(
1087 self.config.show_scale_axes)
1089 self.menuitem_showzeroline = view_menu.addAction(
1090 'Show Zero Lines')
1091 self.menuitem_showzeroline.setCheckable(True)
1093 view_menu.addSeparator()
1094 view_menu.addAction(
1095 qg.QIcon.fromTheme('view-fullscreen'),
1096 'Fullscreen',
1097 self.toggle_fullscreen,
1098 qg.QKeySequence(qc.Qt.Key_F11))
1100 # Options Menu
1101 self.menuitem_demean = options_menu.addAction('Demean')
1102 self.menuitem_demean.setCheckable(True)
1103 self.menuitem_demean.setChecked(self.config.demean)
1104 self.menuitem_demean.setShortcut(
1105 qg.QKeySequence(qc.Qt.Key_Underscore))
1107 self.menuitem_distances_3d = options_menu.addAction(
1108 '3D distances',
1109 self.distances_3d_changed)
1110 self.menuitem_distances_3d.setCheckable(True)
1112 self.menuitem_allowdownsampling = options_menu.addAction(
1113 'Allow Downsampling')
1114 self.menuitem_allowdownsampling.setCheckable(True)
1115 self.menuitem_allowdownsampling.setChecked(True)
1117 self.menuitem_degap = options_menu.addAction(
1118 'Allow Degapping')
1119 self.menuitem_degap.setCheckable(True)
1120 self.menuitem_degap.setChecked(True)
1122 options_menu.addSeparator()
1124 self.menuitem_fft_filtering = options_menu.addAction(
1125 'FFT Filtering')
1126 self.menuitem_fft_filtering.setCheckable(True)
1128 self.menuitem_lphp = options_menu.addAction(
1129 'Bandpass is Low- + Highpass')
1130 self.menuitem_lphp.setCheckable(True)
1131 self.menuitem_lphp.setChecked(True)
1133 options_menu.addSeparator()
1134 self.menuitem_watch = options_menu.addAction(
1135 'Watch Files')
1136 self.menuitem_watch.setCheckable(True)
1138 self.menuitem_liberal_fetch = options_menu.addAction(
1139 'Liberal Fetch Optimization')
1140 self.menuitem_liberal_fetch.setCheckable(True)
1142 self.visible_length = menudef[0][1]
1144 self.snufflings_menu.addAction(
1145 'Reload Snufflings',
1146 self.setup_snufflings)
1148 # Disable ShadowPileTest
1149 if False:
1150 test_action = self.menu.addAction(
1151 'Test',
1152 self.toggletest)
1153 test_action.setCheckable(True)
1155 help_menu.addAction(
1156 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1157 'Snuffler Controls',
1158 self.help,
1159 qg.QKeySequence(qc.Qt.Key_Question))
1161 help_menu.addAction(
1162 'About',
1163 self.about)
1165 self.time_projection = Projection()
1166 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1167 self.time_projection.set_out_range(0., self.width())
1169 self.gather = None
1171 self.trace_filter = None
1172 self.quick_filter = None
1173 self.quick_filter_patterns = None, None
1174 self.blacklist = []
1176 self.track_to_screen = Projection()
1177 self.track_to_nslc_ids = {}
1179 self.cached_vec = None
1180 self.cached_processed_traces = None
1182 self.timer = qc.QTimer(self)
1183 self.timer.timeout.connect(self.periodical)
1184 self.timer.setInterval(1000)
1185 self.timer.start()
1186 self.pile.add_listener(self)
1187 self.trace_styles = {}
1188 if self.get_squirrel() is None:
1189 self.determine_box_styles()
1191 self.setMouseTracking(True)
1193 user_home_dir = os.path.expanduser('~')
1194 self.snuffling_modules = {}
1195 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1196 self.default_snufflings = None
1197 self.snufflings = []
1199 self.stations = {}
1201 self.timer_draw = Timer()
1202 self.timer_cutout = Timer()
1203 self.time_spent_painting = 0.0
1204 self.time_last_painted = time.time()
1206 self.interactive_range_change_time = 0.0
1207 self.interactive_range_change_delay_time = 10.0
1208 self.follow_timer = None
1210 self.sortingmode_change_time = 0.0
1211 self.sortingmode_change_delay_time = None
1213 self.old_data_ranges = {}
1215 self.error_messages = {}
1216 self.return_tag = None
1217 self.wheel_pos = 60
1219 self.setAcceptDrops(True)
1220 self._paths_to_load = []
1222 self.tf_cache = {}
1224 self.waterfall = TraceWaterfall()
1225 self.waterfall_cmap = 'viridis'
1226 self.waterfall_clip_min = 0.
1227 self.waterfall_clip_max = 1.
1228 self.waterfall_show_absolute = False
1229 self.waterfall_integrate = False
1230 self.view_mode = ViewMode.Wiggle
1232 self.automatic_updates = True
1234 self.closing = False
1235 self.paint_timer = qc.QTimer(self)
1236 self.paint_timer.timeout.connect(self.reset_updates)
1237 self.paint_timer.setInterval(20)
1238 self.paint_timer.start()
1240 @qc.pyqtSlot()
1241 def reset_updates(self):
1242 if not self.updatesEnabled():
1243 self.setUpdatesEnabled(True)
1245 def fail(self, reason):
1246 box = qw.QMessageBox(self)
1247 box.setText(reason)
1248 box.exec_()
1250 def set_trace_filter(self, filter_func):
1251 self.trace_filter = filter_func
1252 self.sortingmode_change()
1254 def update_trace_filter(self):
1255 if self.blacklist:
1257 def blacklist_func(tr):
1258 return not pyrocko.util.match_nslc(
1259 self.blacklist, tr.nslc_id)
1261 else:
1262 blacklist_func = None
1264 if self.quick_filter is None and blacklist_func is None:
1265 self.set_trace_filter(None)
1266 elif self.quick_filter is None:
1267 self.set_trace_filter(blacklist_func)
1268 elif blacklist_func is None:
1269 self.set_trace_filter(self.quick_filter)
1270 else:
1271 self.set_trace_filter(
1272 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1274 def set_quick_filter(self, filter_func):
1275 self.quick_filter = filter_func
1276 self.update_trace_filter()
1278 def set_quick_filter_patterns(self, patterns, inputline=None):
1279 if patterns is not None:
1280 self.set_quick_filter(
1281 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1282 else:
1283 self.set_quick_filter(None)
1285 self.quick_filter_patterns = patterns, inputline
1287 def get_quick_filter_patterns(self):
1288 return self.quick_filter_patterns
1290 def add_blacklist_pattern(self, pattern):
1291 if pattern == 'empty':
1292 keys = set(self.pile.nslc_ids)
1293 trs = self.pile.all(
1294 tmin=self.tmin,
1295 tmax=self.tmax,
1296 load_data=False,
1297 degap=False)
1299 for tr in trs:
1300 if tr.nslc_id in keys:
1301 keys.remove(tr.nslc_id)
1303 for key in keys:
1304 xpattern = '.'.join(key)
1305 if xpattern not in self.blacklist:
1306 self.blacklist.append(xpattern)
1308 else:
1309 if pattern in self.blacklist:
1310 self.blacklist.remove(pattern)
1312 self.blacklist.append(pattern)
1314 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1315 self.update_trace_filter()
1317 def remove_blacklist_pattern(self, pattern):
1318 if pattern in self.blacklist:
1319 self.blacklist.remove(pattern)
1320 else:
1321 raise PileViewerMainException(
1322 'Pattern not found in blacklist.')
1324 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1325 self.update_trace_filter()
1327 def clear_blacklist(self):
1328 self.blacklist = []
1329 self.update_trace_filter()
1331 def ssort(self, tr):
1332 return self._ssort(tr)
1334 def station_key(self, x):
1335 return x.network, x.station
1337 def station_keys(self, x):
1338 return [
1339 (x.network, x.station, x.location),
1340 (x.network, x.station)]
1342 def station_attrib(self, tr, getter, default_getter):
1343 for sk in self.station_keys(tr):
1344 if sk in self.stations:
1345 station = self.stations[sk]
1346 return getter(station)
1348 return default_getter(tr)
1350 def get_station(self, sk):
1351 return self.stations[sk]
1353 def has_station(self, station):
1354 for sk in self.station_keys(station):
1355 if sk in self.stations:
1356 return True
1358 return False
1360 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1361 return self.station_attrib(
1362 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1364 def set_stations(self, stations):
1365 self.stations = {}
1366 self.add_stations(stations)
1368 def add_stations(self, stations):
1369 for station in stations:
1370 for sk in self.station_keys(station):
1371 self.stations[sk] = station
1373 ev = self.get_active_event()
1374 if ev:
1375 self.set_origin(ev)
1377 def add_event(self, event):
1378 marker = EventMarker(event)
1379 self.add_marker(marker)
1381 def add_events(self, events):
1382 markers = [EventMarker(e) for e in events]
1383 self.add_markers(markers)
1385 def set_event_marker_as_origin(self, ignore=None):
1386 selected = self.selected_markers()
1387 if not selected:
1388 self.fail('An event marker must be selected.')
1389 return
1391 m = selected[0]
1392 if not isinstance(m, EventMarker):
1393 self.fail('Selected marker is not an event.')
1394 return
1396 self.set_active_event_marker(m)
1398 def deactivate_event_marker(self):
1399 if self.active_event_marker:
1400 self.active_event_marker.active = False
1402 self.active_event_marker_changed.emit()
1403 self.active_event_marker = None
1405 def set_active_event_marker(self, event_marker):
1406 if self.active_event_marker:
1407 self.active_event_marker.active = False
1409 self.active_event_marker = event_marker
1410 event_marker.active = True
1411 event = event_marker.get_event()
1412 self.set_origin(event)
1413 self.active_event_marker_changed.emit()
1415 def set_active_event(self, event):
1416 for marker in self.markers:
1417 if isinstance(marker, EventMarker):
1418 if marker.get_event() is event:
1419 self.set_active_event_marker(marker)
1421 def get_active_event_marker(self):
1422 return self.active_event_marker
1424 def get_active_event(self):
1425 m = self.get_active_event_marker()
1426 if m is not None:
1427 return m.get_event()
1428 else:
1429 return None
1431 def get_active_markers(self):
1432 emarker = self.get_active_event_marker()
1433 if emarker is None:
1434 return None, []
1436 else:
1437 ev = emarker.get_event()
1438 pmarkers = [
1439 m for m in self.markers
1440 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1442 return emarker, pmarkers
1444 def set_origin(self, location):
1445 for station in self.stations.values():
1446 station.set_event_relative_data(
1447 location,
1448 distance_3d=self.menuitem_distances_3d.isChecked())
1450 self.sortingmode_change()
1452 def distances_3d_changed(self):
1453 ignore = self.menuitem_distances_3d.isChecked()
1454 self.set_event_marker_as_origin(ignore)
1456 def iter_snuffling_modules(self):
1457 pjoin = os.path.join
1458 for path in self.snuffling_paths:
1460 if not os.path.isdir(path):
1461 os.mkdir(path)
1463 for entry in os.listdir(path):
1464 directory = path
1465 fn = entry
1466 d = pjoin(path, entry)
1467 if os.path.isdir(d):
1468 directory = d
1469 if os.path.isfile(
1470 os.path.join(directory, 'snuffling.py')):
1471 fn = 'snuffling.py'
1473 if not fn.endswith('.py'):
1474 continue
1476 name = fn[:-3]
1478 if (directory, name) not in self.snuffling_modules:
1479 self.snuffling_modules[directory, name] = \
1480 pyrocko.gui.snuffling.SnufflingModule(
1481 directory, name, self)
1483 yield self.snuffling_modules[directory, name]
1485 def setup_snufflings(self):
1486 # user snufflings
1487 for mod in self.iter_snuffling_modules():
1488 try:
1489 mod.load_if_needed()
1490 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1491 logger.warning('Snuffling module "%s" is broken' % e)
1493 # load the default snufflings on first run
1494 if self.default_snufflings is None:
1495 self.default_snufflings = pyrocko.gui\
1496 .snufflings.__snufflings__()
1497 for snuffling in self.default_snufflings:
1498 self.add_snuffling(snuffling)
1500 def set_panel_parent(self, panel_parent):
1501 self.panel_parent = panel_parent
1503 def get_panel_parent(self):
1504 return self.panel_parent
1506 def add_snuffling(self, snuffling, reloaded=False):
1507 logger.debug('Adding snuffling %s' % snuffling.get_name())
1508 snuffling.init_gui(
1509 self, self.get_panel_parent(), self, reloaded=reloaded)
1510 self.snufflings.append(snuffling)
1511 self.update()
1513 def remove_snuffling(self, snuffling):
1514 snuffling.delete_gui()
1515 self.update()
1516 self.snufflings.remove(snuffling)
1517 snuffling.pre_destroy()
1519 def add_snuffling_menuitem(self, item):
1520 self.snufflings_menu.addAction(item)
1521 item.setParent(self.snufflings_menu)
1522 sort_actions(self.snufflings_menu)
1524 def remove_snuffling_menuitem(self, item):
1525 self.snufflings_menu.removeAction(item)
1527 def add_snuffling_help_menuitem(self, item):
1528 self.snuffling_help.addAction(item)
1529 item.setParent(self.snuffling_help)
1530 sort_actions(self.snuffling_help)
1532 def remove_snuffling_help_menuitem(self, item):
1533 self.snuffling_help.removeAction(item)
1535 def add_panel_toggler(self, item):
1536 self.toggle_panel_menu.addAction(item)
1537 item.setParent(self.toggle_panel_menu)
1538 sort_actions(self.toggle_panel_menu)
1540 def remove_panel_toggler(self, item):
1541 self.toggle_panel_menu.removeAction(item)
1543 def load(self, paths, regex=None, format='detect',
1544 cache_dir=None, force_cache=False):
1546 if cache_dir is None:
1547 cache_dir = pyrocko.config.config().cache_dir
1548 if isinstance(paths, str):
1549 paths = [paths]
1551 fns = pyrocko.util.select_files(
1552 paths, selector=None, include=regex, show_progress=False)
1554 if not fns:
1555 return
1557 cache = pyrocko.pile.get_cache(cache_dir)
1559 t = [time.time()]
1561 def update_bar(label, value):
1562 pbs = self.parent().get_progressbars()
1563 if label.lower() == 'looking at files':
1564 label = 'Looking at %i files' % len(fns)
1565 else:
1566 label = 'Scanning %i files' % len(fns)
1568 return pbs.set_status(label, value)
1570 def update_progress(label, i, n):
1571 abort = False
1573 qw.qApp.processEvents()
1574 if n != 0:
1575 perc = i*100/n
1576 else:
1577 perc = 100
1578 abort |= update_bar(label, perc)
1579 abort |= self.window().is_closing()
1581 tnow = time.time()
1582 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1583 self.update()
1584 t[0] = tnow
1586 return abort
1588 self.automatic_updates = False
1590 self.pile.load_files(
1591 sorted(fns),
1592 filename_attributes=regex,
1593 cache=cache,
1594 fileformat=format,
1595 show_progress=False,
1596 update_progress=update_progress)
1598 self.automatic_updates = True
1599 self.update()
1601 def load_queued(self):
1602 if not self._paths_to_load:
1603 return
1604 paths = self._paths_to_load
1605 self._paths_to_load = []
1606 self.load(paths)
1608 def load_soon(self, paths):
1609 self._paths_to_load.extend(paths)
1610 qc.QTimer.singleShot(200, self.load_queued)
1612 def open_waveforms(self):
1613 caption = 'Select one or more files to open'
1615 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1616 self, caption, options=qfiledialog_options))
1618 if fns:
1619 self.load(list(str(fn) for fn in fns))
1621 def open_waveform_directory(self):
1622 caption = 'Select directory to scan for waveform files'
1624 dn = qw.QFileDialog.getExistingDirectory(
1625 self, caption, options=qfiledialog_options)
1627 if dn:
1628 self.load([str(dn)])
1630 def open_stations(self, fns=None):
1631 caption = 'Select one or more Pyrocko station files to open'
1633 if not fns:
1634 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1635 self, caption, options=qfiledialog_options))
1637 try:
1638 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1639 for stat in stations:
1640 self.add_stations(stat)
1642 except Exception as e:
1643 self.fail('Failed to read station file: %s' % str(e))
1645 def open_stations_xml(self, fns=None):
1646 from pyrocko.io import stationxml
1648 caption = 'Select one or more StationXML files'
1649 if not fns:
1650 fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
1651 self, caption, options=qfiledialog_options,
1652 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1653 ';;All files (*)'))
1655 try:
1656 stations = [
1657 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1658 for x in fns]
1660 for stat in stations:
1661 self.add_stations(stat)
1663 except Exception as e:
1664 self.fail('Failed to read StationXML file: %s' % str(e))
1666 def add_traces(self, traces):
1667 if traces:
1668 mtf = pyrocko.pile.MemTracesFile(None, traces)
1669 self.pile.add_file(mtf)
1670 ticket = (self.pile, mtf)
1671 return ticket
1672 else:
1673 return (None, None)
1675 def release_data(self, tickets):
1676 for ticket in tickets:
1677 pile, mtf = ticket
1678 if pile is not None:
1679 pile.remove_file(mtf)
1681 def periodical(self):
1682 if self.menuitem_watch.isChecked():
1683 if self.pile.reload_modified():
1684 self.update()
1686 def get_pile(self):
1687 return self.pile
1689 def pile_changed(self, what):
1690 self.pile_has_changed = True
1691 self.pile_has_changed_signal.emit()
1692 if self.automatic_updates:
1693 self.update()
1695 def set_gathering(self, gather=None, color=None):
1697 if gather is None:
1698 def gather_func(tr):
1699 return tr.nslc_id
1701 gather = (0, 1, 2, 3)
1703 else:
1704 def gather_func(tr):
1705 return (
1706 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1708 if color is None:
1709 def color(tr):
1710 return tr.location
1712 self.gather = gather_func
1713 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1715 self.color_gather = color
1716 self.color_keys = self.pile.gather_keys(color)
1717 previous_ntracks = self.ntracks
1718 self.set_ntracks(len(keys))
1720 if self.shown_tracks_range is None or \
1721 previous_ntracks == 0 or \
1722 self.show_all:
1724 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1725 key_at_top = None
1726 n = high-low
1728 else:
1729 low, high = self.shown_tracks_range
1730 key_at_top = self.track_keys[low]
1731 n = high-low
1733 self.track_keys = sorted(keys)
1735 track_patterns = []
1736 for k in self.track_keys:
1737 pat = ['*', '*', '*', '*']
1738 for i, j in enumerate(gather):
1739 pat[j] = k[-len(gather)+i]
1741 track_patterns.append(pat)
1743 self.track_patterns = track_patterns
1745 if key_at_top is not None:
1746 try:
1747 ind = self.track_keys.index(key_at_top)
1748 low = ind
1749 high = low+n
1750 except Exception:
1751 pass
1753 self.set_tracks_range((low, high))
1755 self.key_to_row = dict(
1756 [(key, i) for (i, key) in enumerate(self.track_keys)])
1758 def inrange(x, r):
1759 return r[0] <= x and x < r[1]
1761 def trace_selector(trace):
1762 gt = self.gather(trace)
1763 return (
1764 gt in self.key_to_row and
1765 inrange(self.key_to_row[gt], self.shown_tracks_range))
1767 if self.trace_filter is not None:
1768 self.trace_selector = lambda x: \
1769 self.trace_filter(x) and trace_selector(x)
1770 else:
1771 self.trace_selector = trace_selector
1773 if self.tmin == working_system_time_range[0] and \
1774 self.tmax == working_system_time_range[1] or \
1775 self.show_all:
1777 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1778 if tmin is not None and tmax is not None:
1779 tlen = (tmax - tmin)
1780 tpad = tlen * 5./self.width()
1781 self.set_time_range(tmin-tpad, tmax+tpad)
1783 def set_time_range(self, tmin, tmax):
1784 if tmin is None:
1785 tmin = initial_time_range[0]
1787 if tmax is None:
1788 tmax = initial_time_range[1]
1790 if tmin > tmax:
1791 tmin, tmax = tmax, tmin
1793 if tmin == tmax:
1794 tmin -= 1.
1795 tmax += 1.
1797 tmin = max(working_system_time_range[0], tmin)
1798 tmax = min(working_system_time_range[1], tmax)
1800 min_deltat = self.content_deltat_range()[0]
1801 if (tmax - tmin < min_deltat):
1802 m = (tmin + tmax) / 2.
1803 tmin = m - min_deltat/2.
1804 tmax = m + min_deltat/2.
1806 self.time_projection.set_in_range(tmin, tmax)
1807 self.tmin, self.tmax = tmin, tmax
1809 def get_time_range(self):
1810 return self.tmin, self.tmax
1812 def ypart(self, y):
1813 if y < self.ax_height:
1814 return -1
1815 elif y > self.height()-self.ax_height:
1816 return 1
1817 else:
1818 return 0
1820 def time_fractional_digits(self):
1821 min_deltat = self.content_deltat_range()[0]
1822 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1824 def write_markers(self, fn=None):
1825 caption = "Choose a file name to write markers"
1826 if not fn:
1827 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1828 self, caption, options=qfiledialog_options))
1829 if fn:
1830 try:
1831 Marker.save_markers(
1832 self.markers, fn,
1833 fdigits=self.time_fractional_digits())
1835 except Exception as e:
1836 self.fail('Failed to write marker file: %s' % str(e))
1838 def write_selected_markers(self, fn=None):
1839 caption = "Choose a file name to write selected markers"
1840 if not fn:
1841 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
1842 self, caption, options=qfiledialog_options))
1843 if fn:
1844 try:
1845 Marker.save_markers(
1846 self.iter_selected_markers(),
1847 fn,
1848 fdigits=self.time_fractional_digits())
1850 except Exception as e:
1851 self.fail('Failed to write marker file: %s' % str(e))
1853 def read_events(self, fn=None):
1854 '''
1855 Open QFileDialog to open, read and add
1856 :py:class:`pyrocko.model.Event` instances and their marker
1857 representation to the pile viewer.
1858 '''
1859 caption = "Selet one or more files to open"
1860 if not fn:
1861 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1862 self, caption, options=qfiledialog_options))
1863 if fn:
1864 try:
1865 self.add_events(pyrocko.model.load_events(fn))
1866 self.associate_phases_to_events()
1868 except Exception as e:
1869 self.fail('Failed to read event file: %s' % str(e))
1871 def read_markers(self, fn=None):
1872 '''
1873 Open QFileDialog to open, read and add markers to the pile viewer.
1874 '''
1875 caption = "Selet one or more marker files to open"
1876 if not fn:
1877 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1878 self, caption, options=qfiledialog_options))
1879 if fn:
1880 try:
1881 self.add_markers(Marker.load_markers(fn))
1882 self.associate_phases_to_events()
1884 except Exception as e:
1885 self.fail('Failed to read marker file: %s' % str(e))
1887 def associate_phases_to_events(self):
1888 associate_phases_to_events(self.markers)
1890 def add_marker(self, marker):
1891 # need index to inform QAbstactTableModel about upcoming change,
1892 # but have to restore current state in order to not cause problems
1893 self.markers.insert(marker)
1894 i = self.markers.remove(marker)
1896 self.begin_markers_add.emit(i, i)
1897 self.markers.insert(marker)
1898 self.end_markers_add.emit()
1899 self.markers_deltat_max = max(
1900 self.markers_deltat_max, marker.tmax - marker.tmin)
1902 def add_markers(self, markers):
1903 if not self.markers:
1904 self.begin_markers_add.emit(0, len(markers) - 1)
1905 self.markers.insert_many(markers)
1906 self.end_markers_add.emit()
1907 self.update_markers_deltat_max()
1908 else:
1909 for marker in markers:
1910 self.add_marker(marker)
1912 def update_markers_deltat_max(self):
1913 if self.markers:
1914 self.markers_deltat_max = max(
1915 marker.tmax - marker.tmin for marker in self.markers)
1917 def remove_marker(self, marker):
1918 '''
1919 Remove a ``marker`` from the :py:class:`PileViewer`.
1921 :param marker: :py:class:`Marker` (or subclass) instance
1922 '''
1924 if marker is self.active_event_marker:
1925 self.deactivate_event_marker()
1927 try:
1928 i = self.markers.index(marker)
1929 self.begin_markers_remove.emit(i, i)
1930 self.markers.remove_at(i)
1931 self.end_markers_remove.emit()
1932 except ValueError:
1933 pass
1935 def remove_markers(self, markers):
1936 '''
1937 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1939 :param markers: list of :py:class:`Marker` (or subclass)
1940 instances
1941 '''
1943 if markers is self.markers:
1944 markers = list(markers)
1946 for marker in markers:
1947 self.remove_marker(marker)
1949 self.update_markers_deltat_max()
1951 def remove_selected_markers(self):
1952 def delete_segment(istart, iend):
1953 self.begin_markers_remove.emit(istart, iend-1)
1954 for _ in range(iend - istart):
1955 self.markers.remove_at(istart)
1957 self.end_markers_remove.emit()
1959 istart = None
1960 ipos = 0
1961 markers = self.markers
1962 nmarkers = len(self.markers)
1963 while ipos < nmarkers:
1964 marker = markers[ipos]
1965 if marker.is_selected():
1966 if marker is self.active_event_marker:
1967 self.deactivate_event_marker()
1969 if istart is None:
1970 istart = ipos
1971 else:
1972 if istart is not None:
1973 delete_segment(istart, ipos)
1974 nmarkers -= ipos - istart
1975 ipos = istart - 1
1976 istart = None
1978 ipos += 1
1980 if istart is not None:
1981 delete_segment(istart, ipos)
1983 self.update_markers_deltat_max()
1985 def selected_markers(self):
1986 return [marker for marker in self.markers if marker.is_selected()]
1988 def iter_selected_markers(self):
1989 for marker in self.markers:
1990 if marker.is_selected():
1991 yield marker
1993 def get_markers(self):
1994 return self.markers
1996 def mousePressEvent(self, mouse_ev):
1997 self.show_all = False
1998 point = self.mapFromGlobal(mouse_ev.globalPos())
2000 if mouse_ev.button() == qc.Qt.LeftButton:
2001 marker = self.marker_under_cursor(point.x(), point.y())
2002 if self.picking:
2003 if self.picking_down is None:
2004 self.picking_down = (
2005 self.time_projection.rev(mouse_ev.x()),
2006 mouse_ev.y())
2008 elif marker is not None:
2009 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
2010 self.deselect_all()
2011 marker.selected = True
2012 self.emit_selected_markers()
2013 self.update()
2014 else:
2015 self.track_start = mouse_ev.x(), mouse_ev.y()
2016 self.track_trange = self.tmin, self.tmax
2018 if mouse_ev.button() == qc.Qt.RightButton \
2019 and isinstance(self.menu, qw.QMenu):
2020 self.menu.exec_(qg.QCursor.pos())
2021 self.update_status()
2023 def mouseReleaseEvent(self, mouse_ev):
2024 if self.ignore_releases:
2025 self.ignore_releases -= 1
2026 return
2028 if self.picking:
2029 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2030 self.emit_selected_markers()
2032 if self.track_start:
2033 self.update()
2035 self.track_start = None
2036 self.track_trange = None
2037 self.update_status()
2039 def mouseDoubleClickEvent(self, mouse_ev):
2040 self.show_all = False
2041 self.start_picking(None)
2042 self.ignore_releases = 1
2044 def mouseMoveEvent(self, mouse_ev):
2045 self.setUpdatesEnabled(False)
2046 point = self.mapFromGlobal(mouse_ev.globalPos())
2048 if self.picking:
2049 self.update_picking(point.x(), point.y())
2051 elif self.track_start is not None:
2052 x0, y0 = self.track_start
2053 dx = (point.x() - x0)/float(self.width())
2054 dy = (point.y() - y0)/float(self.height())
2055 if self.ypart(y0) == 1:
2056 dy = 0
2058 tmin0, tmax0 = self.track_trange
2060 scale = math.exp(-dy*5.)
2061 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2062 frac = x0/float(self.width())
2063 dt = dx*(tmax0-tmin0)*scale
2065 self.interrupt_following()
2066 self.set_time_range(
2067 tmin0 - dt - dtr*frac,
2068 tmax0 - dt + dtr*(1.-frac))
2070 self.update()
2071 else:
2072 self.hoovering(point.x(), point.y())
2074 self.update_status()
2076 def nslc_ids_under_cursor(self, x, y):
2077 ftrack = self.track_to_screen.rev(y)
2078 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2079 return nslc_ids
2081 def marker_under_cursor(self, x, y):
2082 mouset = self.time_projection.rev(x)
2083 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2084 relevant_nslc_ids = None
2085 for marker in self.markers:
2086 if marker.kind not in self.visible_marker_kinds:
2087 continue
2089 if (abs(mouset-marker.tmin) < deltat or
2090 abs(mouset-marker.tmax) < deltat):
2092 if relevant_nslc_ids is None:
2093 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2095 marker_nslc_ids = marker.get_nslc_ids()
2096 if not marker_nslc_ids:
2097 return marker
2099 for nslc_id in marker_nslc_ids:
2100 if nslc_id in relevant_nslc_ids:
2101 return marker
2103 def hoovering(self, x, y):
2104 mouset = self.time_projection.rev(x)
2105 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2106 needupdate = False
2107 haveone = False
2108 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2109 for marker in self.markers:
2110 if marker.kind not in self.visible_marker_kinds:
2111 continue
2113 state = abs(mouset-marker.tmin) < deltat or \
2114 abs(mouset-marker.tmax) < deltat and not haveone
2116 if state:
2117 xstate = False
2119 marker_nslc_ids = marker.get_nslc_ids()
2120 if not marker_nslc_ids:
2121 xstate = True
2123 for nslc in relevant_nslc_ids:
2124 if marker.match_nslc(nslc):
2125 xstate = True
2127 state = xstate
2129 if state:
2130 haveone = True
2131 oldstate = marker.is_alerted()
2132 if oldstate != state:
2133 needupdate = True
2134 marker.set_alerted(state)
2135 if state:
2136 self.message = marker.hoover_message()
2138 if not haveone:
2139 self.message = None
2141 if needupdate:
2142 self.update()
2144 def event(self, event):
2145 if event.type() == qc.QEvent.KeyPress:
2146 self.keyPressEvent(event)
2147 return True
2148 else:
2149 return base.event(self, event)
2151 def keyPressEvent(self, key_event):
2152 self.show_all = False
2153 dt = self.tmax - self.tmin
2154 tmid = (self.tmin + self.tmax) / 2.
2156 key = key_event.key()
2157 try:
2158 keytext = str(key_event.text())
2159 except UnicodeEncodeError:
2160 return
2162 if key == qc.Qt.Key_Space:
2163 self.interrupt_following()
2164 self.set_time_range(self.tmin+dt, self.tmax+dt)
2166 elif key == qc.Qt.Key_Up:
2167 for m in self.selected_markers():
2168 if isinstance(m, PhaseMarker):
2169 if key_event.modifiers() & qc.Qt.ShiftModifier:
2170 p = 0
2171 else:
2172 p = 1 if m.get_polarity() != 1 else None
2173 m.set_polarity(p)
2175 elif key == qc.Qt.Key_Down:
2176 for m in self.selected_markers():
2177 if isinstance(m, PhaseMarker):
2178 if key_event.modifiers() & qc.Qt.ShiftModifier:
2179 p = 0
2180 else:
2181 p = -1 if m.get_polarity() != -1 else None
2182 m.set_polarity(p)
2184 elif key == qc.Qt.Key_B:
2185 dt = self.tmax - self.tmin
2186 self.interrupt_following()
2187 self.set_time_range(self.tmin-dt, self.tmax-dt)
2189 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2190 self.interrupt_following()
2192 tgo = None
2194 class TraceDummy(object):
2195 def __init__(self, marker):
2196 self._marker = marker
2198 @property
2199 def nslc_id(self):
2200 return self._marker.one_nslc()
2202 def marker_to_itrack(marker):
2203 try:
2204 return self.key_to_row.get(
2205 self.gather(TraceDummy(marker)), -1)
2207 except MarkerOneNSLCRequired:
2208 return -1
2210 emarker, pmarkers = self.get_active_markers()
2211 pmarkers = [
2212 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2213 pmarkers.sort(key=lambda m: (
2214 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2216 if key == qc.Qt.Key_Backtab:
2217 pmarkers.reverse()
2219 smarkers = self.selected_markers()
2220 iselected = []
2221 for sm in smarkers:
2222 try:
2223 iselected.append(pmarkers.index(sm))
2224 except ValueError:
2225 pass
2227 if iselected:
2228 icurrent = max(iselected) + 1
2229 else:
2230 icurrent = 0
2232 if icurrent < len(pmarkers):
2233 self.deselect_all()
2234 cmarker = pmarkers[icurrent]
2235 cmarker.selected = True
2236 tgo = cmarker.tmin
2237 if not self.tmin < tgo < self.tmax:
2238 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2240 itrack = marker_to_itrack(cmarker)
2241 if itrack != -1:
2242 if itrack < self.shown_tracks_range[0]:
2243 self.scroll_tracks(
2244 - (self.shown_tracks_range[0] - itrack))
2245 elif self.shown_tracks_range[1] <= itrack:
2246 self.scroll_tracks(
2247 itrack - self.shown_tracks_range[1]+1)
2249 if itrack not in self.track_to_nslc_ids:
2250 self.go_to_selection()
2252 elif keytext in ('p', 'n', 'P', 'N'):
2253 smarkers = self.selected_markers()
2254 tgo = None
2255 dir = str(keytext)
2256 if smarkers:
2257 tmid = smarkers[0].tmin
2258 for smarker in smarkers:
2259 if dir == 'n':
2260 tmid = max(smarker.tmin, tmid)
2261 else:
2262 tmid = min(smarker.tmin, tmid)
2264 tgo = tmid
2266 if dir.lower() == 'n':
2267 for marker in sorted(
2268 self.markers,
2269 key=operator.attrgetter('tmin')):
2271 t = marker.tmin
2272 if t > tmid and \
2273 marker.kind in self.visible_marker_kinds and \
2274 (dir == 'n' or
2275 isinstance(marker, EventMarker)):
2277 self.deselect_all()
2278 marker.selected = True
2279 tgo = t
2280 break
2281 else:
2282 for marker in sorted(
2283 self.markers,
2284 key=operator.attrgetter('tmin'),
2285 reverse=True):
2287 t = marker.tmin
2288 if t < tmid and \
2289 marker.kind in self.visible_marker_kinds and \
2290 (dir == 'p' or
2291 isinstance(marker, EventMarker)):
2292 self.deselect_all()
2293 marker.selected = True
2294 tgo = t
2295 break
2297 if tgo is not None:
2298 self.interrupt_following()
2299 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2301 elif keytext == 'r':
2302 if self.pile.reload_modified():
2303 self.reloaded = True
2305 elif keytext == 'R':
2306 self.setup_snufflings()
2308 elif key == qc.Qt.Key_Backspace:
2309 self.remove_selected_markers()
2311 elif keytext == 'a':
2312 for marker in self.markers:
2313 if ((self.tmin <= marker.tmin <= self.tmax or
2314 self.tmin <= marker.tmax <= self.tmax) and
2315 marker.kind in self.visible_marker_kinds):
2316 marker.selected = True
2317 else:
2318 marker.selected = False
2320 elif keytext == 'A':
2321 for marker in self.markers:
2322 if marker.kind in self.visible_marker_kinds:
2323 marker.selected = True
2325 elif keytext == 'd':
2326 self.deselect_all()
2328 elif keytext == 'E':
2329 self.deactivate_event_marker()
2331 elif keytext == 'e':
2332 markers = self.selected_markers()
2333 event_markers_in_spe = [
2334 marker for marker in markers
2335 if not isinstance(marker, PhaseMarker)]
2337 phase_markers = [
2338 marker for marker in markers
2339 if isinstance(marker, PhaseMarker)]
2341 if len(event_markers_in_spe) == 1:
2342 event_marker = event_markers_in_spe[0]
2343 if not isinstance(event_marker, EventMarker):
2344 nslcs = list(event_marker.nslc_ids)
2345 lat, lon = 0.0, 0.0
2346 old = self.get_active_event()
2347 if len(nslcs) == 1:
2348 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2349 elif old is not None:
2350 lat, lon = old.lat, old.lon
2352 event_marker.convert_to_event_marker(lat, lon)
2354 self.set_active_event_marker(event_marker)
2355 event = event_marker.get_event()
2356 for marker in phase_markers:
2357 marker.set_event(event)
2359 else:
2360 for marker in event_markers_in_spe:
2361 marker.convert_to_event_marker()
2363 elif keytext in ('0', '1', '2', '3', '4', '5'):
2364 for marker in self.selected_markers():
2365 marker.set_kind(int(keytext))
2366 self.emit_selected_markers()
2368 elif key in fkey_map:
2369 self.handle_fkeys(key)
2371 elif key == qc.Qt.Key_Escape:
2372 if self.picking:
2373 self.stop_picking(0, 0, abort=True)
2375 elif key == qc.Qt.Key_PageDown:
2376 self.scroll_tracks(
2377 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2379 elif key == qc.Qt.Key_PageUp:
2380 self.scroll_tracks(
2381 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2383 elif key == qc.Qt.Key_Plus:
2384 self.zoom_tracks(0., 1.)
2386 elif key == qc.Qt.Key_Minus:
2387 self.zoom_tracks(0., -1.)
2389 elif key == qc.Qt.Key_Equal:
2390 ntracks_shown = self.shown_tracks_range[1] - \
2391 self.shown_tracks_range[0]
2392 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2393 self.zoom_tracks(0., dtracks)
2395 elif key == qc.Qt.Key_Colon:
2396 self.want_input.emit()
2398 elif keytext == 'f':
2399 self.toggle_fullscreen()
2401 elif keytext == 'g':
2402 self.go_to_selection()
2404 elif keytext == 'G':
2405 self.go_to_selection(tight=True)
2407 elif keytext == 'm':
2408 self.toggle_marker_editor()
2410 elif keytext == 'c':
2411 self.toggle_main_controls()
2413 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2414 dir = 1
2415 amount = 1
2416 if key_event.key() == qc.Qt.Key_Left:
2417 dir = -1
2418 if key_event.modifiers() & qc.Qt.ShiftModifier:
2419 amount = 10
2420 self.nudge_selected_markers(dir*amount)
2421 else:
2422 super().keyPressEvent(key_event)
2424 if keytext != '' and keytext in 'degaApPnN':
2425 self.emit_selected_markers()
2427 self.update()
2428 self.update_status()
2430 def handle_fkeys(self, key):
2431 self.set_phase_kind(
2432 self.selected_markers(),
2433 fkey_map[key] + 1)
2434 self.emit_selected_markers()
2436 def emit_selected_markers(self):
2437 ibounds = []
2438 last_selected = False
2439 for imarker, marker in enumerate(self.markers):
2440 this_selected = marker.is_selected()
2441 if this_selected != last_selected:
2442 ibounds.append(imarker)
2444 last_selected = this_selected
2446 if last_selected:
2447 ibounds.append(len(self.markers))
2449 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2450 self.n_selected_markers = sum(
2451 chunk[1] - chunk[0] for chunk in chunks)
2452 self.marker_selection_changed.emit(chunks)
2454 def toggle_marker_editor(self):
2455 self.panel_parent.toggle_marker_editor()
2457 def toggle_main_controls(self):
2458 self.panel_parent.toggle_main_controls()
2460 def nudge_selected_markers(self, npixels):
2461 a, b = self.time_projection.ur
2462 c, d = self.time_projection.xr
2463 for marker in self.selected_markers():
2464 if not isinstance(marker, EventMarker):
2465 marker.tmin += npixels * (d-c)/b
2466 marker.tmax += npixels * (d-c)/b
2468 def toggle_fullscreen(self):
2469 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2470 self.window().windowState() & qc.Qt.WindowMaximized:
2471 self.window().showNormal()
2472 else:
2473 if macosx:
2474 self.window().showMaximized()
2475 else:
2476 self.window().showFullScreen()
2478 def about(self):
2479 fn = pyrocko.util.data_file('snuffler.png')
2480 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2481 txt = f.read()
2482 label = qw.QLabel(txt % {'logo': fn})
2483 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2484 self.show_doc('About', [label], target='tab')
2486 def help(self):
2487 class MyScrollArea(qw.QScrollArea):
2489 def sizeHint(self):
2490 s = qc.QSize()
2491 s.setWidth(self.widget().sizeHint().width())
2492 s.setHeight(self.widget().sizeHint().height())
2493 return s
2495 with open(pyrocko.util.data_file(
2496 'snuffler_help.html')) as f:
2497 hcheat = qw.QLabel(f.read())
2499 with open(pyrocko.util.data_file(
2500 'snuffler_help_epilog.html')) as f:
2501 hepilog = qw.QLabel(f.read())
2503 for h in [hcheat, hepilog]:
2504 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2505 h.setWordWrap(True)
2507 self.show_doc('Help', [hcheat, hepilog], target='panel')
2509 def show_doc(self, name, labels, target='panel'):
2510 scroller = qw.QScrollArea()
2511 frame = qw.QFrame(scroller)
2512 frame.setLineWidth(0)
2513 layout = qw.QVBoxLayout()
2514 layout.setContentsMargins(0, 0, 0, 0)
2515 layout.setSpacing(0)
2516 frame.setLayout(layout)
2517 scroller.setWidget(frame)
2518 scroller.setWidgetResizable(True)
2519 frame.setBackgroundRole(qg.QPalette.Base)
2520 for h in labels:
2521 h.setParent(frame)
2522 h.setMargin(3)
2523 h.setTextInteractionFlags(
2524 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2525 h.setBackgroundRole(qg.QPalette.Base)
2526 layout.addWidget(h)
2527 h.linkActivated.connect(
2528 self.open_link)
2530 if self.panel_parent is not None:
2531 if target == 'panel':
2532 self.panel_parent.add_panel(
2533 name, scroller, True, volatile=False)
2534 else:
2535 self.panel_parent.add_tab(name, scroller)
2537 def open_link(self, link):
2538 qg.QDesktopServices.openUrl(qc.QUrl(link))
2540 def wheelEvent(self, wheel_event):
2541 if use_pyqt5:
2542 self.wheel_pos += wheel_event.angleDelta().y()
2543 else:
2544 self.wheel_pos += wheel_event.delta()
2546 n = self.wheel_pos // 120
2547 self.wheel_pos = self.wheel_pos % 120
2548 if n == 0:
2549 return
2551 amount = max(
2552 1.,
2553 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2554 wdelta = amount * n
2556 trmin, trmax = self.track_to_screen.get_in_range()
2557 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2558 / (trmax-trmin)
2560 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2561 self.zoom_tracks(anchor, wdelta)
2562 else:
2563 self.scroll_tracks(-wdelta)
2565 def dragEnterEvent(self, event):
2566 if event.mimeData().hasUrls():
2567 if any(url.toLocalFile() for url in event.mimeData().urls()):
2568 event.setDropAction(qc.Qt.LinkAction)
2569 event.accept()
2571 def dropEvent(self, event):
2572 if event.mimeData().hasUrls():
2573 paths = list(
2574 str(url.toLocalFile()) for url in event.mimeData().urls())
2575 event.acceptProposedAction()
2576 self.load(paths)
2578 def get_phase_name(self, kind):
2579 return self.config.get_phase_name(kind)
2581 def set_phase_kind(self, markers, kind):
2582 phasename = self.get_phase_name(kind)
2584 for marker in markers:
2585 if isinstance(marker, PhaseMarker):
2586 if kind == 10:
2587 marker.convert_to_marker()
2588 else:
2589 marker.set_phasename(phasename)
2590 marker.set_event(self.get_active_event())
2592 elif isinstance(marker, EventMarker):
2593 pass
2595 else:
2596 if kind != 10:
2597 event = self.get_active_event()
2598 marker.convert_to_phase_marker(
2599 event, phasename, None, False)
2601 def set_ntracks(self, ntracks):
2602 if self.ntracks != ntracks:
2603 self.ntracks = ntracks
2604 if self.shown_tracks_range is not None:
2605 l, h = self.shown_tracks_range
2606 else:
2607 l, h = 0, self.ntracks
2609 self.tracks_range_changed.emit(self.ntracks, l, h)
2611 def set_tracks_range(self, range, start=None):
2613 low, high = range
2614 low = min(self.ntracks-1, low)
2615 high = min(self.ntracks, high)
2616 low = max(0, low)
2617 high = max(1, high)
2619 if start is None:
2620 start = float(low)
2622 if self.shown_tracks_range != (low, high):
2623 self.shown_tracks_range = low, high
2624 self.shown_tracks_start = start
2626 self.tracks_range_changed.emit(self.ntracks, low, high)
2628 def scroll_tracks(self, shift):
2629 shown = self.shown_tracks_range
2630 shiftmin = -shown[0]
2631 shiftmax = self.ntracks-shown[1]
2632 shift = max(shiftmin, shift)
2633 shift = min(shiftmax, shift)
2634 shown = shown[0] + shift, shown[1] + shift
2636 self.set_tracks_range((int(shown[0]), int(shown[1])))
2638 self.update()
2640 def zoom_tracks(self, anchor, delta):
2641 ntracks_shown = self.shown_tracks_range[1] \
2642 - self.shown_tracks_range[0]
2644 if (ntracks_shown == 1 and delta <= 0) or \
2645 (ntracks_shown == self.ntracks and delta >= 0):
2646 return
2648 ntracks_shown += int(round(delta))
2649 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2651 u = self.shown_tracks_start
2652 nu = max(0., u-anchor*delta)
2653 nv = nu + ntracks_shown
2654 if nv > self.ntracks:
2655 nu -= nv - self.ntracks
2656 nv -= nv - self.ntracks
2658 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2660 self.ntracks_shown_max = self.shown_tracks_range[1] \
2661 - self.shown_tracks_range[0]
2663 self.update()
2665 def content_time_range(self):
2666 pile = self.get_pile()
2667 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2668 if tmin is None:
2669 tmin = initial_time_range[0]
2670 if tmax is None:
2671 tmax = initial_time_range[1]
2673 return tmin, tmax
2675 def content_deltat_range(self):
2676 pile = self.get_pile()
2678 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2680 if deltatmin is None:
2681 deltatmin = 0.001
2683 if deltatmax is None:
2684 deltatmax = 1000.0
2686 return deltatmin, deltatmax
2688 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2689 if tmax < tmin:
2690 tmin, tmax = tmax, tmin
2692 deltatmin = self.content_deltat_range()[0]
2693 dt = deltatmin * self.visible_length * 0.95
2695 if dt == 0.0:
2696 dt = 1.0
2698 if tight:
2699 if tmax != tmin:
2700 dtm = tmax - tmin
2701 tmin -= dtm*0.1
2702 tmax += dtm*0.1
2703 return tmin, tmax
2704 else:
2705 tcenter = (tmin + tmax) / 2.
2706 tmin = tcenter - 0.5*dt
2707 tmax = tcenter + 0.5*dt
2708 return tmin, tmax
2710 if tmax-tmin < dt:
2711 vmin, vmax = self.get_time_range()
2712 dt = min(vmax - vmin, dt)
2714 tcenter = (tmin+tmax)/2.
2715 etmin, etmax = tmin, tmax
2716 tmin = min(etmin, tcenter - 0.5*dt)
2717 tmax = max(etmax, tcenter + 0.5*dt)
2718 dtm = tmax-tmin
2719 if etmin == tmin:
2720 tmin -= dtm*0.1
2721 if etmax == tmax:
2722 tmax += dtm*0.1
2724 else:
2725 dtm = tmax-tmin
2726 tmin -= dtm*0.1
2727 tmax += dtm*0.1
2729 return tmin, tmax
2731 def go_to_selection(self, tight=False):
2732 markers = self.selected_markers()
2733 if markers:
2734 tmax, tmin = self.content_time_range()
2735 for marker in markers:
2736 tmin = min(tmin, marker.tmin)
2737 tmax = max(tmax, marker.tmax)
2739 else:
2740 if tight:
2741 vmin, vmax = self.get_time_range()
2742 tmin = tmax = (vmin + vmax) / 2.
2743 else:
2744 tmin, tmax = self.content_time_range()
2746 tmin, tmax = self.make_good_looking_time_range(
2747 tmin, tmax, tight=tight)
2749 self.interrupt_following()
2750 self.set_time_range(tmin, tmax)
2751 self.update()
2753 def go_to_time(self, t, tlen=None):
2754 tmax = t
2755 if tlen is not None:
2756 tmax = t+tlen
2757 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2758 self.interrupt_following()
2759 self.set_time_range(tmin, tmax)
2760 self.update()
2762 def go_to_event_by_name(self, name):
2763 for marker in self.markers:
2764 if isinstance(marker, EventMarker):
2765 event = marker.get_event()
2766 if event.name and event.name.lower() == name.lower():
2767 tmin, tmax = self.make_good_looking_time_range(
2768 event.time, event.time)
2770 self.interrupt_following()
2771 self.set_time_range(tmin, tmax)
2773 def printit(self):
2774 from .qt_compat import qprint
2775 printer = qprint.QPrinter()
2776 printer.setOrientation(qprint.QPrinter.Landscape)
2778 dialog = qprint.QPrintDialog(printer, self)
2779 dialog.setWindowTitle('Print')
2781 if dialog.exec_() != qw.QDialog.Accepted:
2782 return
2784 painter = qg.QPainter()
2785 painter.begin(printer)
2786 page = printer.pageRect()
2787 self.drawit(
2788 painter, printmode=False, w=page.width(), h=page.height())
2790 painter.end()
2792 def savesvg(self, fn=None):
2794 if not fn:
2795 fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
2796 self,
2797 'Save as SVG|PNG',
2798 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2799 'SVG|PNG (*.svg *.png)',
2800 options=qfiledialog_options))
2802 if fn == '':
2803 return
2805 fn = str(fn)
2807 if fn.lower().endswith('.svg'):
2808 try:
2809 w, h = 842, 595
2810 margin = 0.025
2811 m = max(w, h)*margin
2813 generator = qsvg.QSvgGenerator()
2814 generator.setFileName(fn)
2815 generator.setSize(qc.QSize(w, h))
2816 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2818 painter = qg.QPainter()
2819 painter.begin(generator)
2820 self.drawit(painter, printmode=False, w=w, h=h)
2821 painter.end()
2823 except Exception as e:
2824 self.fail('Failed to write SVG file: %s' % str(e))
2826 elif fn.lower().endswith('.png'):
2827 if use_pyqt5:
2828 pixmap = self.grab()
2829 else:
2830 pixmap = qg.QPixmap().grabWidget(self)
2832 try:
2833 pixmap.save(fn)
2835 except Exception as e:
2836 self.fail('Failed to write PNG file: %s' % str(e))
2838 else:
2839 self.fail(
2840 'Unsupported file type: filename must end with ".svg" or '
2841 '".png".')
2843 def paintEvent(self, paint_ev):
2844 '''
2845 Called by QT whenever widget needs to be painted.
2846 '''
2847 painter = qg.QPainter(self)
2849 if self.menuitem_antialias.isChecked():
2850 painter.setRenderHint(qg.QPainter.Antialiasing)
2852 self.drawit(painter)
2854 logger.debug(
2855 'Time spent drawing: '
2856 ' user:%.3f sys:%.3f children_user:%.3f'
2857 ' childred_sys:%.3f elapsed:%.3f' %
2858 (self.timer_draw - self.timer_cutout))
2860 logger.debug(
2861 'Time spent processing:'
2862 ' user:%.3f sys:%.3f children_user:%.3f'
2863 ' childred_sys:%.3f elapsed:%.3f' %
2864 self.timer_cutout.get())
2866 self.time_spent_painting = self.timer_draw.get()[-1]
2867 self.time_last_painted = time.time()
2869 def determine_box_styles(self):
2871 traces = list(self.pile.iter_traces())
2872 traces.sort(key=operator.attrgetter('full_id'))
2873 istyle = 0
2874 trace_styles = {}
2875 for itr, tr in enumerate(traces):
2876 if itr > 0:
2877 other = traces[itr-1]
2878 if not (
2879 other.nslc_id == tr.nslc_id
2880 and other.deltat == tr.deltat
2881 and abs(other.tmax - tr.tmin)
2882 < gap_lap_tolerance*tr.deltat):
2884 istyle += 1
2886 trace_styles[tr.full_id, tr.deltat] = istyle
2888 self.trace_styles = trace_styles
2890 def draw_trace_boxes(self, p, time_projection, track_projections):
2892 for v_projection in track_projections.values():
2893 v_projection.set_in_range(0., 1.)
2895 def selector(x):
2896 return x.overlaps(*time_projection.get_in_range())
2898 if self.trace_filter is not None:
2899 def tselector(x):
2900 return selector(x) and self.trace_filter(x)
2902 else:
2903 tselector = selector
2905 traces = list(self.pile.iter_traces(
2906 group_selector=selector, trace_selector=tselector))
2908 traces.sort(key=operator.attrgetter('full_id'))
2910 def drawbox(itrack, istyle, traces):
2911 v_projection = track_projections[itrack]
2912 dvmin = v_projection(0.)
2913 dvmax = v_projection(1.)
2914 dtmin = time_projection.clipped(traces[0].tmin)
2915 dtmax = time_projection.clipped(traces[-1].tmax)
2917 style = box_styles[istyle % len(box_styles)]
2918 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2919 p.fillRect(rect, style.fill_brush)
2920 p.setPen(style.frame_pen)
2921 p.drawRect(rect)
2923 traces_by_style = {}
2924 for itr, tr in enumerate(traces):
2925 gt = self.gather(tr)
2926 if gt not in self.key_to_row:
2927 continue
2929 itrack = self.key_to_row[gt]
2930 if itrack not in track_projections:
2931 continue
2933 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2935 if len(traces) < 500:
2936 drawbox(itrack, istyle, [tr])
2937 else:
2938 if (itrack, istyle) not in traces_by_style:
2939 traces_by_style[itrack, istyle] = []
2940 traces_by_style[itrack, istyle].append(tr)
2942 for (itrack, istyle), traces in traces_by_style.items():
2943 drawbox(itrack, istyle, traces)
2945 def draw_visible_markers(
2946 self, p, vcenter_projection, primary_pen):
2948 try:
2949 markers = self.markers.with_key_in_limited(
2950 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2952 except pyrocko.pile.TooMany:
2953 tmin = self.markers[0].tmin
2954 tmax = self.markers[-1].tmax
2955 umin_view, umax_view = self.time_projection.get_out_range()
2956 umin = max(umin_view, self.time_projection(tmin))
2957 umax = min(umax_view, self.time_projection(tmax))
2958 v0, _ = vcenter_projection.get_out_range()
2959 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2961 p.save()
2963 pen = qg.QPen(primary_pen)
2964 pen.setWidth(2)
2965 pen.setStyle(qc.Qt.DotLine)
2966 # pat = [5., 3.]
2967 # pen.setDashPattern(pat)
2968 p.setPen(pen)
2970 if self.n_selected_markers == len(self.markers):
2971 s_selected = ' (all selected)'
2972 elif self.n_selected_markers > 0:
2973 s_selected = ' (%i selected)' % self.n_selected_markers
2974 else:
2975 s_selected = ''
2977 draw_label(
2978 p, umin+10., v0-10.,
2979 '%i Markers' % len(self.markers) + s_selected,
2980 label_bg, 'LB')
2982 line = qc.QLineF(umin, v0, umax, v0)
2983 p.drawLine(line)
2984 p.restore()
2986 return
2988 for marker in markers:
2989 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2990 and marker.kind in self.visible_marker_kinds:
2992 marker.draw(
2993 p, self.time_projection, vcenter_projection,
2994 with_label=True)
2996 def get_squirrel(self):
2997 try:
2998 return self.pile._squirrel
2999 except AttributeError:
3000 return None
3002 def draw_coverage(self, p, time_projection, track_projections):
3003 sq = self.get_squirrel()
3004 if sq is None:
3005 return
3007 def drawbox(itrack, tmin, tmax, style):
3008 v_projection = track_projections[itrack]
3009 dvmin = v_projection(0.)
3010 dvmax = v_projection(1.)
3011 dtmin = time_projection.clipped(tmin)
3012 dtmax = time_projection.clipped(tmax)
3014 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3015 p.fillRect(rect, style.fill_brush)
3016 p.setPen(style.frame_pen)
3017 p.drawRect(rect)
3019 pattern_list = []
3020 pattern_to_itrack = {}
3021 for key in self.track_keys:
3022 itrack = self.key_to_row[key]
3023 if itrack not in track_projections:
3024 continue
3026 pattern = self.track_patterns[itrack]
3027 pattern_to_itrack[tuple(pattern)] = itrack
3028 pattern_list.append(tuple(pattern))
3030 vmin, vmax = self.get_time_range()
3032 for kind in ['waveform', 'waveform_promise']:
3033 for coverage in sq.get_coverage(
3034 kind, vmin, vmax, pattern_list, limit=500):
3035 itrack = pattern_to_itrack[coverage.pattern.nslc]
3037 if coverage.changes is None:
3038 drawbox(
3039 itrack, coverage.tmin, coverage.tmax,
3040 box_styles_coverage[kind][0])
3041 else:
3042 t = None
3043 pcount = 0
3044 for tb, count in coverage.changes:
3045 if t is not None and tb > t:
3046 if pcount > 0:
3047 drawbox(
3048 itrack, t, tb,
3049 box_styles_coverage[kind][
3050 min(len(box_styles_coverage)-1,
3051 pcount)])
3053 t = tb
3054 pcount = count
3056 def drawit(self, p, printmode=False, w=None, h=None):
3057 '''
3058 This performs the actual drawing.
3059 '''
3061 self.timer_draw.start()
3062 show_boxes = self.menuitem_showboxes.isChecked()
3063 sq = self.get_squirrel()
3065 if self.gather is None:
3066 self.set_gathering()
3068 if self.pile_has_changed:
3070 if not self.sortingmode_change_delayed():
3071 self.sortingmode_change()
3073 if show_boxes and sq is None:
3074 self.determine_box_styles()
3076 self.pile_has_changed = False
3078 if h is None:
3079 h = float(self.height())
3080 if w is None:
3081 w = float(self.width())
3083 if printmode:
3084 primary_color = (0, 0, 0)
3085 else:
3086 primary_color = pyrocko.plot.tango_colors['aluminium5']
3088 primary_pen = qg.QPen(qg.QColor(*primary_color))
3090 ax_h = float(self.ax_height)
3092 vbottom_ax_projection = Projection()
3093 vtop_ax_projection = Projection()
3094 vcenter_projection = Projection()
3096 self.time_projection.set_out_range(0., w)
3097 vbottom_ax_projection.set_out_range(h-ax_h, h)
3098 vtop_ax_projection.set_out_range(0., ax_h)
3099 vcenter_projection.set_out_range(ax_h, h-ax_h)
3100 vcenter_projection.set_in_range(0., 1.)
3101 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3103 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3104 track_projections = {}
3105 for i in range(*self.shown_tracks_range):
3106 proj = Projection()
3107 proj.set_out_range(
3108 self.track_to_screen(i+0.05),
3109 self.track_to_screen(i+1.-0.05))
3111 track_projections[i] = proj
3113 if self.tmin > self.tmax:
3114 return
3116 self.time_projection.set_in_range(self.tmin, self.tmax)
3117 vbottom_ax_projection.set_in_range(0, ax_h)
3119 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3121 yscaler = pyrocko.plot.AutoScaler()
3123 p.setPen(primary_pen)
3125 font = qg.QFont()
3126 font.setBold(True)
3128 axannotfont = qg.QFont()
3129 axannotfont.setBold(True)
3130 axannotfont.setPointSize(8)
3132 processed_traces = self.prepare_cutout2(
3133 self.tmin, self.tmax,
3134 trace_selector=self.trace_selector,
3135 degap=self.menuitem_degap.isChecked(),
3136 demean=self.menuitem_demean.isChecked())
3138 if not printmode and show_boxes:
3139 if (self.view_mode is ViewMode.Wiggle) \
3140 or (self.view_mode is ViewMode.Waterfall
3141 and not processed_traces):
3143 if sq is None:
3144 self.draw_trace_boxes(
3145 p, self.time_projection, track_projections)
3147 else:
3148 self.draw_coverage(
3149 p, self.time_projection, track_projections)
3151 p.setFont(font)
3152 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3154 color_lookup = dict(
3155 [(k, i) for (i, k) in enumerate(self.color_keys)])
3157 self.track_to_nslc_ids = {}
3158 nticks = 0
3159 annot_labels = []
3161 if self.view_mode is ViewMode.Waterfall and processed_traces:
3162 waterfall = self.waterfall
3163 waterfall.set_time_range(self.tmin, self.tmax)
3164 waterfall.set_traces(processed_traces)
3165 waterfall.set_cmap(self.waterfall_cmap)
3166 waterfall.set_integrate(self.waterfall_integrate)
3167 waterfall.set_clip(
3168 self.waterfall_clip_min, self.waterfall_clip_max)
3169 waterfall.show_absolute_values(
3170 self.waterfall_show_absolute)
3172 rect = qc.QRectF(
3173 0, self.ax_height,
3174 self.width(), self.height() - self.ax_height*2
3175 )
3176 waterfall.draw_waterfall(p, rect=rect)
3178 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3179 show_scales = self.menuitem_showscalerange.isChecked() \
3180 or self.menuitem_showscaleaxis.isChecked()
3182 fm = qg.QFontMetrics(axannotfont, p.device())
3183 trackheight = self.track_to_screen(1.-0.05) \
3184 - self.track_to_screen(0.05)
3186 nlinesavail = trackheight/float(fm.lineSpacing())
3188 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3189 if self.menuitem_showscaleaxis.isChecked() \
3190 else 15
3192 yscaler = pyrocko.plot.AutoScaler(
3193 no_exp_interval=(-3, 2), approx_ticks=nticks,
3194 snap=show_scales
3195 and not self.menuitem_showscaleaxis.isChecked())
3197 data_ranges = pyrocko.trace.minmax(
3198 processed_traces,
3199 key=self.scaling_key,
3200 mode=self.scaling_base)
3202 if not self.menuitem_fixscalerange.isChecked():
3203 self.old_data_ranges = data_ranges
3204 else:
3205 data_ranges.update(self.old_data_ranges)
3207 self.apply_scaling_hooks(data_ranges)
3209 trace_to_itrack = {}
3210 track_scaling_keys = {}
3211 track_scaling_colors = {}
3212 for trace in processed_traces:
3213 gt = self.gather(trace)
3214 if gt not in self.key_to_row:
3215 continue
3217 itrack = self.key_to_row[gt]
3218 if itrack not in track_projections:
3219 continue
3221 trace_to_itrack[trace] = itrack
3223 if itrack not in self.track_to_nslc_ids:
3224 self.track_to_nslc_ids[itrack] = set()
3226 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3228 if itrack not in track_scaling_keys:
3229 track_scaling_keys[itrack] = set()
3231 scaling_key = self.scaling_key(trace)
3232 track_scaling_keys[itrack].add(scaling_key)
3234 color = pyrocko.plot.color(
3235 color_lookup[self.color_gather(trace)])
3237 k = itrack, scaling_key
3238 if k not in track_scaling_colors \
3239 and self.menuitem_colortraces.isChecked():
3240 track_scaling_colors[k] = color
3241 else:
3242 track_scaling_colors[k] = primary_color
3244 # y axes, zero lines
3245 trace_projections = {}
3246 for itrack in list(track_projections.keys()):
3247 if itrack not in track_scaling_keys:
3248 continue
3249 uoff = 0
3250 for scaling_key in track_scaling_keys[itrack]:
3251 data_range = data_ranges[scaling_key]
3252 dymin, dymax = data_range
3253 ymin, ymax, yinc = yscaler.make_scale(
3254 (dymin/self.gain, dymax/self.gain))
3255 iexp = yscaler.make_exp(yinc)
3256 factor = 10**iexp
3257 trace_projection = track_projections[itrack].copy()
3258 trace_projection.set_in_range(ymax, ymin)
3259 trace_projections[itrack, scaling_key] = \
3260 trace_projection
3261 umin, umax = self.time_projection.get_out_range()
3262 vmin, vmax = trace_projection.get_out_range()
3263 umax_zeroline = umax
3264 uoffnext = uoff
3266 if show_scales:
3267 pen = qg.QPen(primary_pen)
3268 k = itrack, scaling_key
3269 if k in track_scaling_colors:
3270 c = qg.QColor(*track_scaling_colors[
3271 itrack, scaling_key])
3273 pen.setColor(c)
3275 p.setPen(pen)
3276 if nlinesavail > 3:
3277 if self.menuitem_showscaleaxis.isChecked():
3278 ymin_annot = math.ceil(ymin/yinc)*yinc
3279 ny_annot = int(
3280 math.floor(ymax/yinc)
3281 - math.ceil(ymin/yinc)) + 1
3283 for iy_annot in range(ny_annot):
3284 y = ymin_annot + iy_annot*yinc
3285 v = trace_projection(y)
3286 line = qc.QLineF(
3287 umax-10-uoff, v, umax-uoff, v)
3289 p.drawLine(line)
3290 if iy_annot == ny_annot - 1 \
3291 and iexp != 0:
3292 sexp = ' × ' \
3293 '10<sup>%i</sup>' % iexp
3294 else:
3295 sexp = ''
3297 snum = num_to_html(y/factor)
3298 lab = Label(
3299 p,
3300 umax-20-uoff,
3301 v, '%s%s' % (snum, sexp),
3302 label_bg=None,
3303 anchor='MR',
3304 font=axannotfont,
3305 color=c)
3307 uoffnext = max(
3308 lab.rect.width()+30., uoffnext)
3310 annot_labels.append(lab)
3311 if y == 0.:
3312 umax_zeroline = \
3313 umax - 20 \
3314 - lab.rect.width() - 10 \
3315 - uoff
3316 else:
3317 if not show_boxes:
3318 qpoints = make_QPolygonF(
3319 [umax-20-uoff,
3320 umax-10-uoff,
3321 umax-10-uoff,
3322 umax-20-uoff],
3323 [vmax, vmax, vmin, vmin])
3324 p.drawPolyline(qpoints)
3326 snum = num_to_html(ymin)
3327 labmin = Label(
3328 p, umax-15-uoff, vmax, snum,
3329 label_bg=None,
3330 anchor='BR',
3331 font=axannotfont,
3332 color=c)
3334 annot_labels.append(labmin)
3335 snum = num_to_html(ymax)
3336 labmax = Label(
3337 p, umax-15-uoff, vmin, snum,
3338 label_bg=None,
3339 anchor='TR',
3340 font=axannotfont,
3341 color=c)
3343 annot_labels.append(labmax)
3345 for lab in (labmin, labmax):
3346 uoffnext = max(
3347 lab.rect.width()+10., uoffnext)
3349 if self.menuitem_showzeroline.isChecked():
3350 v = trace_projection(0.)
3351 if vmin <= v <= vmax:
3352 line = qc.QLineF(umin, v, umax_zeroline, v)
3353 p.drawLine(line)
3355 uoff = uoffnext
3357 p.setFont(font)
3358 p.setPen(primary_pen)
3359 for trace in processed_traces:
3360 if self.view_mode is not ViewMode.Wiggle:
3361 break
3363 if trace not in trace_to_itrack:
3364 continue
3366 itrack = trace_to_itrack[trace]
3367 scaling_key = self.scaling_key(trace)
3368 trace_projection = trace_projections[
3369 itrack, scaling_key]
3371 vdata = trace_projection(trace.get_ydata())
3373 udata_min = float(self.time_projection(trace.tmin))
3374 udata_max = float(self.time_projection(
3375 trace.tmin+trace.deltat*(vdata.size-1)))
3376 udata = num.linspace(udata_min, udata_max, vdata.size)
3378 qpoints = make_QPolygonF(udata, vdata)
3380 umin, umax = self.time_projection.get_out_range()
3381 vmin, vmax = trace_projection.get_out_range()
3383 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3385 if self.menuitem_cliptraces.isChecked():
3386 p.setClipRect(trackrect)
3388 if self.menuitem_colortraces.isChecked():
3389 color = pyrocko.plot.color(
3390 color_lookup[self.color_gather(trace)])
3391 pen = qg.QPen(qg.QColor(*color), 1)
3392 p.setPen(pen)
3394 p.drawPolyline(qpoints)
3396 if self.floating_marker:
3397 self.floating_marker.draw_trace(
3398 self, p, trace,
3399 self.time_projection, trace_projection, 1.0)
3401 for marker in self.markers.with_key_in(
3402 self.tmin - self.markers_deltat_max,
3403 self.tmax):
3405 if marker.tmin < self.tmax \
3406 and self.tmin < marker.tmax \
3407 and marker.kind \
3408 in self.visible_marker_kinds:
3409 marker.draw_trace(
3410 self, p, trace, self.time_projection,
3411 trace_projection, 1.0)
3413 p.setPen(primary_pen)
3415 if self.menuitem_cliptraces.isChecked():
3416 p.setClipRect(0, 0, int(w), int(h))
3418 if self.floating_marker:
3419 self.floating_marker.draw(
3420 p, self.time_projection, vcenter_projection)
3422 self.draw_visible_markers(
3423 p, vcenter_projection, primary_pen)
3425 p.setPen(primary_pen)
3426 while font.pointSize() > 2:
3427 fm = qg.QFontMetrics(font, p.device())
3428 trackheight = self.track_to_screen(1.-0.05) \
3429 - self.track_to_screen(0.05)
3430 nlinesavail = trackheight/float(fm.lineSpacing())
3431 if nlinesavail > 1:
3432 break
3434 font.setPointSize(font.pointSize()-1)
3436 p.setFont(font)
3437 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3439 for key in self.track_keys:
3440 itrack = self.key_to_row[key]
3441 if itrack in track_projections:
3442 plabel = ' '.join(
3443 [str(x) for x in key if x is not None])
3444 lx = 10
3445 ly = self.track_to_screen(itrack+0.5)
3447 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3448 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3449 continue
3451 contains_cursor = \
3452 self.track_to_screen(itrack) \
3453 < mouse_pos.y() \
3454 < self.track_to_screen(itrack+1)
3456 if not contains_cursor:
3457 continue
3459 font_large = p.font()
3460 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3461 p.setFont(font_large)
3462 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3463 p.setFont(font)
3465 for lab in annot_labels:
3466 lab.draw()
3468 self.timer_draw.stop()
3470 def see_data_params(self):
3472 min_deltat = self.content_deltat_range()[0]
3474 # determine padding and downampling requirements
3475 if self.lowpass is not None:
3476 deltat_target = 1./self.lowpass * 0.25
3477 ndecimate = min(
3478 50,
3479 max(1, int(round(deltat_target / min_deltat))))
3480 tpad = 1./self.lowpass * 2.
3481 else:
3482 ndecimate = 1
3483 tpad = min_deltat*5.
3485 if self.highpass is not None:
3486 tpad = max(1./self.highpass * 2., tpad)
3488 nsee_points_per_trace = 5000*10
3489 tsee = ndecimate*nsee_points_per_trace*min_deltat
3491 return ndecimate, tpad, tsee
3493 def clean_update(self):
3494 self.cached_processed_traces = None
3495 self.update()
3497 def get_adequate_tpad(self):
3498 tpad = 0.
3499 for f in [self.highpass, self.lowpass]:
3500 if f is not None:
3501 tpad = max(tpad, 1.0/f)
3503 for snuffling in self.snufflings:
3504 if snuffling._post_process_hook_enabled \
3505 or snuffling._pre_process_hook_enabled:
3507 tpad = max(tpad, snuffling.get_tpad())
3509 return tpad
3511 def prepare_cutout2(
3512 self, tmin, tmax, trace_selector=None, degap=True,
3513 demean=True, nmax=6000):
3515 if self.pile.is_empty():
3516 return []
3518 nmax = self.visible_length
3520 self.timer_cutout.start()
3522 tsee = tmax-tmin
3523 min_deltat_wo_decimate = tsee/nmax
3524 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3526 min_deltat_allow = min_deltat_wo_decimate
3527 if self.lowpass is not None:
3528 target_deltat_lp = 0.25/self.lowpass
3529 if target_deltat_lp > min_deltat_wo_decimate:
3530 min_deltat_allow = min_deltat_w_decimate
3532 min_deltat_allow = math.exp(
3533 int(math.floor(math.log(min_deltat_allow))))
3535 tmin_ = tmin
3536 tmax_ = tmax
3538 # fetch more than needed?
3539 if self.menuitem_liberal_fetch.isChecked():
3540 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3541 tmin = math.floor(tmin/tlen) * tlen
3542 tmax = math.ceil(tmax/tlen) * tlen
3544 fft_filtering = self.menuitem_fft_filtering.isChecked()
3545 lphp = self.menuitem_lphp.isChecked()
3546 ads = self.menuitem_allowdownsampling.isChecked()
3548 tpad = self.get_adequate_tpad()
3549 tpad = max(tpad, tsee)
3551 # state vector to decide if cached traces can be used
3552 vec = (
3553 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3554 self.highpass, fft_filtering, lphp,
3555 min_deltat_allow, self.rotate, self.shown_tracks_range,
3556 ads, self.pile.get_update_count())
3558 if (self.cached_vec
3559 and self.cached_vec[0] <= vec[0]
3560 and vec[1] <= self.cached_vec[1]
3561 and vec[2:] == self.cached_vec[2:]
3562 and not (self.reloaded or self.menuitem_watch.isChecked())
3563 and self.cached_processed_traces is not None):
3565 logger.debug('Using cached traces')
3566 processed_traces = self.cached_processed_traces
3568 else:
3569 processed_traces = []
3570 if self.pile.deltatmax >= min_deltat_allow:
3572 def group_selector(gr):
3573 return gr.deltatmax >= min_deltat_allow
3575 if trace_selector is not None:
3576 def trace_selectorx(tr):
3577 return tr.deltat >= min_deltat_allow \
3578 and trace_selector(tr)
3579 else:
3580 def trace_selectorx(tr):
3581 return tr.deltat >= min_deltat_allow
3583 for traces in self.pile.chopper(
3584 tmin=tmin, tmax=tmax, tpad=tpad,
3585 want_incomplete=True,
3586 degap=degap,
3587 maxgap=gap_lap_tolerance,
3588 maxlap=gap_lap_tolerance,
3589 keep_current_files_open=True,
3590 group_selector=group_selector,
3591 trace_selector=trace_selectorx,
3592 accessor_id=id(self),
3593 snap=(math.floor, math.ceil),
3594 include_last=True):
3596 if demean:
3597 for tr in traces:
3598 if (tr.meta and tr.meta.get('tabu', False)):
3599 continue
3600 y = tr.get_ydata()
3601 tr.set_ydata(y - num.mean(y))
3603 traces = self.pre_process_hooks(traces)
3605 for trace in traces:
3607 if not (trace.meta
3608 and trace.meta.get('tabu', False)):
3610 if fft_filtering:
3611 but = pyrocko.response.ButterworthResponse
3612 multres = pyrocko.response.MultiplyResponse
3613 if self.lowpass is not None \
3614 or self.highpass is not None:
3616 it = num.arange(
3617 trace.data_len(), dtype=float)
3618 detr_data, m, b = detrend(
3619 it, trace.get_ydata())
3621 trace.set_ydata(detr_data)
3623 freqs, fdata = trace.spectrum(
3624 pad_to_pow2=True, tfade=None)
3626 nfreqs = fdata.size
3628 key = (trace.deltat, nfreqs)
3630 if key not in self.tf_cache:
3631 resps = []
3632 if self.lowpass is not None:
3633 resps.append(but(
3634 order=4,
3635 corner=self.lowpass,
3636 type='low'))
3638 if self.highpass is not None:
3639 resps.append(but(
3640 order=4,
3641 corner=self.highpass,
3642 type='high'))
3644 resp = multres(resps)
3645 self.tf_cache[key] = \
3646 resp.evaluate(freqs)
3648 filtered_data = num.fft.irfft(
3649 fdata*self.tf_cache[key]
3650 )[:trace.data_len()]
3652 retrended_data = retrend(
3653 it, filtered_data, m, b)
3655 trace.set_ydata(retrended_data)
3657 else:
3659 if ads and self.lowpass is not None:
3660 while trace.deltat \
3661 < min_deltat_wo_decimate:
3663 trace.downsample(2, demean=False)
3665 fmax = 0.5/trace.deltat
3666 if not lphp and (
3667 self.lowpass is not None
3668 and self.highpass is not None
3669 and self.lowpass < fmax
3670 and self.highpass < fmax
3671 and self.highpass < self.lowpass):
3673 trace.bandpass(
3674 2, self.highpass, self.lowpass)
3675 else:
3676 if self.lowpass is not None:
3677 if self.lowpass < 0.5/trace.deltat:
3678 trace.lowpass(
3679 4, self.lowpass,
3680 demean=False)
3682 if self.highpass is not None:
3683 if self.lowpass is None \
3684 or self.highpass \
3685 < self.lowpass:
3687 if self.highpass < \
3688 0.5/trace.deltat:
3689 trace.highpass(
3690 4, self.highpass,
3691 demean=False)
3693 processed_traces.append(trace)
3695 if self.rotate != 0.0:
3696 phi = self.rotate/180.*math.pi
3697 cphi = math.cos(phi)
3698 sphi = math.sin(phi)
3699 for a in processed_traces:
3700 for b in processed_traces:
3701 if (a.network == b.network
3702 and a.station == b.station
3703 and a.location == b.location
3704 and ((a.channel.lower().endswith('n')
3705 and b.channel.lower().endswith('e'))
3706 or (a.channel.endswith('1')
3707 and b.channel.endswith('2')))
3708 and abs(a.deltat-b.deltat) < a.deltat*0.001
3709 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3710 len(a.get_ydata()) == len(b.get_ydata())):
3712 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3713 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3714 a.set_ydata(aydata)
3715 b.set_ydata(bydata)
3717 processed_traces = self.post_process_hooks(processed_traces)
3719 self.cached_processed_traces = processed_traces
3720 self.cached_vec = vec
3722 chopped_traces = []
3723 for trace in processed_traces:
3724 chop_tmin = tmin_ - trace.deltat*4
3725 chop_tmax = tmax_ + trace.deltat*4
3727 try:
3728 ctrace = trace.chop(
3729 chop_tmin, chop_tmax,
3730 inplace=False)
3732 except pyrocko.trace.NoData:
3733 continue
3735 if ctrace.data_len() < 2:
3736 continue
3738 chopped_traces.append(ctrace)
3740 self.timer_cutout.stop()
3741 return chopped_traces
3743 def pre_process_hooks(self, traces):
3744 for snuffling in self.snufflings:
3745 if snuffling._pre_process_hook_enabled:
3746 traces = snuffling.pre_process_hook(traces)
3748 return traces
3750 def post_process_hooks(self, traces):
3751 for snuffling in self.snufflings:
3752 if snuffling._post_process_hook_enabled:
3753 traces = snuffling.post_process_hook(traces)
3755 return traces
3757 def visible_length_change(self, ignore=None):
3758 for menuitem, vlen in self.menuitems_visible_length:
3759 if menuitem.isChecked():
3760 self.visible_length = vlen
3762 def scaling_base_change(self, ignore=None):
3763 for menuitem, scaling_base in self.menuitems_scaling_base:
3764 if menuitem.isChecked():
3765 self.scaling_base = scaling_base
3767 def scalingmode_change(self, ignore=None):
3768 for menuitem, scaling_key in self.menuitems_scaling:
3769 if menuitem.isChecked():
3770 self.scaling_key = scaling_key
3771 self.update()
3773 def apply_scaling_hooks(self, data_ranges):
3774 for k in sorted(self.scaling_hooks.keys()):
3775 hook = self.scaling_hooks[k]
3776 hook(data_ranges)
3778 def viewmode_change(self, ignore=True):
3779 for item, mode in self.menuitems_viewmode:
3780 if item.isChecked():
3781 self.view_mode = mode
3782 break
3783 else:
3784 raise AttributeError('unknown view mode')
3786 items_waterfall_disabled = (
3787 self.menuitem_showscaleaxis,
3788 self.menuitem_showscalerange,
3789 self.menuitem_showzeroline,
3790 self.menuitem_colortraces,
3791 self.menuitem_cliptraces,
3792 *(itm[0] for itm in self.menuitems_visible_length)
3793 )
3795 if self.view_mode is ViewMode.Waterfall:
3796 self.parent().show_colorbar_ctrl(True)
3797 self.parent().show_gain_ctrl(False)
3799 for item in items_waterfall_disabled:
3800 item.setDisabled(True)
3802 self.visible_length = 180.
3803 else:
3804 self.parent().show_colorbar_ctrl(False)
3805 self.parent().show_gain_ctrl(True)
3807 for item in items_waterfall_disabled:
3808 item.setDisabled(False)
3810 self.visible_length_change()
3811 self.update()
3813 def set_scaling_hook(self, k, hook):
3814 self.scaling_hooks[k] = hook
3816 def remove_scaling_hook(self, k):
3817 del self.scaling_hooks[k]
3819 def remove_scaling_hooks(self):
3820 self.scaling_hooks = {}
3822 def s_sortingmode_change(self, ignore=None):
3823 for menuitem, valfunc in self.menuitems_ssorting:
3824 if menuitem.isChecked():
3825 self._ssort = valfunc
3827 self.sortingmode_change()
3829 def sortingmode_change(self, ignore=None):
3830 for menuitem, (gather, color) in self.menuitems_sorting:
3831 if menuitem.isChecked():
3832 self.set_gathering(gather, color)
3834 self.sortingmode_change_time = time.time()
3836 def lowpass_change(self, value, ignore=None):
3837 self.lowpass = value
3838 self.passband_check()
3839 self.tf_cache = {}
3840 self.update()
3842 def highpass_change(self, value, ignore=None):
3843 self.highpass = value
3844 self.passband_check()
3845 self.tf_cache = {}
3846 self.update()
3848 def passband_check(self):
3849 if self.highpass and self.lowpass \
3850 and self.highpass >= self.lowpass:
3852 self.message = 'Corner frequency of highpass larger than ' \
3853 'corner frequency of lowpass! I will now ' \
3854 'deactivate the highpass.'
3856 self.update_status()
3857 else:
3858 oldmess = self.message
3859 self.message = None
3860 if oldmess is not None:
3861 self.update_status()
3863 def gain_change(self, value, ignore):
3864 self.gain = value
3865 self.update()
3867 def rot_change(self, value, ignore):
3868 self.rotate = value
3869 self.update()
3871 def waterfall_cmap_change(self, cmap):
3872 self.waterfall_cmap = cmap
3873 self.update()
3875 def waterfall_clip_change(self, clip_min, clip_max):
3876 self.waterfall_clip_min = clip_min
3877 self.waterfall_clip_max = clip_max
3878 self.update()
3880 def waterfall_show_absolute_change(self, toggle):
3881 self.waterfall_show_absolute = toggle
3882 self.update()
3884 def waterfall_set_integrate(self, toggle):
3885 self.waterfall_integrate = toggle
3886 self.update()
3888 def set_selected_markers(self, markers):
3889 '''
3890 Set a list of markers selected
3892 :param markers: list of markers
3893 '''
3894 self.deselect_all()
3895 for m in markers:
3896 m.selected = True
3898 self.update()
3900 def deselect_all(self):
3901 for marker in self.markers:
3902 marker.selected = False
3904 def animate_picking(self):
3905 point = self.mapFromGlobal(qg.QCursor.pos())
3906 self.update_picking(point.x(), point.y(), doshift=True)
3908 def get_nslc_ids_for_track(self, ftrack):
3909 itrack = int(ftrack)
3910 return self.track_to_nslc_ids.get(itrack, [])
3912 def stop_picking(self, x, y, abort=False):
3913 if self.picking:
3914 self.update_picking(x, y, doshift=False)
3915 self.picking = None
3916 self.picking_down = None
3917 self.picking_timer.stop()
3918 self.picking_timer = None
3919 if not abort:
3920 self.add_marker(self.floating_marker)
3921 self.floating_marker.selected = True
3922 self.emit_selected_markers()
3924 self.floating_marker = None
3926 def start_picking(self, ignore):
3928 if not self.picking:
3929 self.deselect_all()
3930 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3931 point = self.mapFromGlobal(qg.QCursor.pos())
3933 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3934 self.picking.setGeometry(
3935 gpoint.x(), gpoint.y(), 1, self.height())
3936 t = self.time_projection.rev(point.x())
3938 ftrack = self.track_to_screen.rev(point.y())
3939 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3940 self.floating_marker = Marker(nslc_ids, t, t)
3941 self.floating_marker.selected = True
3943 self.picking_timer = qc.QTimer()
3944 self.picking_timer.timeout.connect(
3945 self.animate_picking)
3947 self.picking_timer.setInterval(50)
3948 self.picking_timer.start()
3950 def update_picking(self, x, y, doshift=False):
3951 if self.picking:
3952 mouset = self.time_projection.rev(x)
3953 dt = 0.0
3954 if mouset < self.tmin or mouset > self.tmax:
3955 if mouset < self.tmin:
3956 dt = -(self.tmin - mouset)
3957 else:
3958 dt = mouset - self.tmax
3959 ddt = self.tmax-self.tmin
3960 dt = max(dt, -ddt/10.)
3961 dt = min(dt, ddt/10.)
3963 x0 = x
3964 if self.picking_down is not None:
3965 x0 = self.time_projection(self.picking_down[0])
3967 w = abs(x-x0)
3968 x0 = min(x0, x)
3970 tmin, tmax = (
3971 self.time_projection.rev(x0),
3972 self.time_projection.rev(x0+w))
3974 tmin, tmax = (
3975 max(working_system_time_range[0], tmin),
3976 min(working_system_time_range[1], tmax))
3978 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3980 self.picking.setGeometry(
3981 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3983 ftrack = self.track_to_screen.rev(y)
3984 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3985 self.floating_marker.set(nslc_ids, tmin, tmax)
3987 if dt != 0.0 and doshift:
3988 self.interrupt_following()
3989 self.set_time_range(self.tmin+dt, self.tmax+dt)
3991 self.update()
3993 def update_status(self):
3995 if self.message is None:
3996 point = self.mapFromGlobal(qg.QCursor.pos())
3998 mouse_t = self.time_projection.rev(point.x())
3999 if not is_working_time(mouse_t):
4000 return
4002 if self.floating_marker:
4003 tmi, tma = (
4004 self.floating_marker.tmin,
4005 self.floating_marker.tmax)
4007 tt, ms = gmtime_x(tmi)
4009 if tmi == tma:
4010 message = mystrftime(
4011 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4012 tt=tt, milliseconds=ms)
4013 else:
4014 srange = '%g s' % (tma-tmi)
4015 message = mystrftime(
4016 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4017 tt=tt, milliseconds=ms)
4018 else:
4019 tt, ms = gmtime_x(mouse_t)
4021 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4022 else:
4023 message = self.message
4025 sb = self.window().statusBar()
4026 sb.clearMessage()
4027 sb.showMessage(message)
4029 def set_sortingmode_change_delay_time(self, dt):
4030 self.sortingmode_change_delay_time = dt
4032 def sortingmode_change_delayed(self):
4033 now = time.time()
4034 return (
4035 self.sortingmode_change_delay_time is not None
4036 and now - self.sortingmode_change_time
4037 < self.sortingmode_change_delay_time)
4039 def set_visible_marker_kinds(self, kinds):
4040 self.deselect_all()
4041 self.visible_marker_kinds = tuple(kinds)
4042 self.emit_selected_markers()
4044 def following(self):
4045 return self.follow_timer is not None \
4046 and not self.following_interrupted()
4048 def interrupt_following(self):
4049 self.interactive_range_change_time = time.time()
4051 def following_interrupted(self, now=None):
4052 if now is None:
4053 now = time.time()
4054 return now - self.interactive_range_change_time \
4055 < self.interactive_range_change_delay_time
4057 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4058 if tmax_start is None:
4059 tmax_start = time.time()
4060 self.show_all = False
4061 self.follow_time = tlen
4062 self.follow_timer = qc.QTimer(self)
4063 self.follow_timer.timeout.connect(
4064 self.follow_update)
4065 self.follow_timer.setInterval(interval)
4066 self.follow_timer.start()
4067 self.follow_started = time.time()
4068 self.follow_lapse = lapse
4069 self.follow_tshift = self.follow_started - tmax_start
4070 self.interactive_range_change_time = 0.0
4072 def unfollow(self):
4073 if self.follow_timer is not None:
4074 self.follow_timer.stop()
4075 self.follow_timer = None
4076 self.interactive_range_change_time = 0.0
4078 def follow_update(self):
4079 rnow = time.time()
4080 if self.follow_lapse is None:
4081 now = rnow
4082 else:
4083 now = self.follow_started + (rnow - self.follow_started) \
4084 * self.follow_lapse
4086 if self.following_interrupted(rnow):
4087 return
4088 self.set_time_range(
4089 now-self.follow_time-self.follow_tshift,
4090 now-self.follow_tshift)
4092 self.update()
4094 def myclose(self, return_tag=''):
4095 self.return_tag = return_tag
4096 self.window().close()
4098 def cleanup(self):
4099 self.about_to_close.emit()
4100 self.timer.stop()
4101 if self.follow_timer is not None:
4102 self.follow_timer.stop()
4104 for snuffling in list(self.snufflings):
4105 self.remove_snuffling(snuffling)
4107 def set_error_message(self, key, value):
4108 if value is None:
4109 if key in self.error_messages:
4110 del self.error_messages[key]
4111 else:
4112 self.error_messages[key] = value
4114 def inputline_changed(self, text):
4115 pass
4117 def inputline_finished(self, text):
4118 line = str(text)
4120 toks = line.split()
4121 clearit, hideit, error = False, True, None
4122 if len(toks) >= 1:
4123 command = toks[0].lower()
4125 try:
4126 quick_filter_commands = {
4127 'n': '%s.*.*.*',
4128 's': '*.%s.*.*',
4129 'l': '*.*.%s.*',
4130 'c': '*.*.*.%s'}
4132 if command in quick_filter_commands:
4133 if len(toks) >= 2:
4134 patterns = [
4135 quick_filter_commands[toks[0]] % pat
4136 for pat in toks[1:]]
4137 self.set_quick_filter_patterns(patterns, line)
4138 else:
4139 self.set_quick_filter_patterns(None)
4141 self.update()
4143 elif command in ('hide', 'unhide'):
4144 if len(toks) >= 2:
4145 patterns = []
4146 if len(toks) == 2:
4147 patterns = [toks[1]]
4148 elif len(toks) >= 3:
4149 x = {
4150 'n': '%s.*.*.*',
4151 's': '*.%s.*.*',
4152 'l': '*.*.%s.*',
4153 'c': '*.*.*.%s'}
4155 if toks[1] in x:
4156 patterns.extend(
4157 x[toks[1]] % tok for tok in toks[2:])
4159 for pattern in patterns:
4160 if command == 'hide':
4161 self.add_blacklist_pattern(pattern)
4162 else:
4163 self.remove_blacklist_pattern(pattern)
4165 elif command == 'unhide' and len(toks) == 1:
4166 self.clear_blacklist()
4168 clearit = True
4170 self.update()
4172 elif command == 'markers':
4173 if len(toks) == 2:
4174 if toks[1] == 'all':
4175 kinds = self.all_marker_kinds
4176 else:
4177 kinds = []
4178 for x in toks[1]:
4179 try:
4180 kinds.append(int(x))
4181 except Exception:
4182 pass
4184 self.set_visible_marker_kinds(kinds)
4186 elif len(toks) == 1:
4187 self.set_visible_marker_kinds(())
4189 self.update()
4191 elif command == 'scaling':
4192 if len(toks) == 2:
4193 hideit = False
4194 error = 'wrong number of arguments'
4196 if len(toks) >= 3:
4197 vmin, vmax = [
4198 pyrocko.model.float_or_none(x)
4199 for x in toks[-2:]]
4201 def upd(d, k, vmin, vmax):
4202 if k in d:
4203 if vmin is not None:
4204 d[k] = vmin, d[k][1]
4205 if vmax is not None:
4206 d[k] = d[k][0], vmax
4208 if len(toks) == 1:
4209 self.remove_scaling_hooks()
4211 elif len(toks) == 3:
4212 def hook(data_ranges):
4213 for k in data_ranges:
4214 upd(data_ranges, k, vmin, vmax)
4216 self.set_scaling_hook('_', hook)
4218 elif len(toks) == 4:
4219 pattern = toks[1]
4221 def hook(data_ranges):
4222 for k in pyrocko.util.match_nslcs(
4223 pattern, list(data_ranges.keys())):
4225 upd(data_ranges, k, vmin, vmax)
4227 self.set_scaling_hook(pattern, hook)
4229 elif command == 'goto':
4230 toks2 = line.split(None, 1)
4231 if len(toks2) == 2:
4232 arg = toks2[1]
4233 m = re.match(
4234 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4235 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4236 if m:
4237 tlen = None
4238 if not m.group(1):
4239 tlen = 12*32*24*60*60
4240 elif not m.group(2):
4241 tlen = 32*24*60*60
4242 elif not m.group(3):
4243 tlen = 24*60*60
4244 elif not m.group(4):
4245 tlen = 60*60
4246 elif not m.group(5):
4247 tlen = 60
4249 supl = '1970-01-01 00:00:00'
4250 if len(supl) > len(arg):
4251 arg = arg + supl[-(len(supl)-len(arg)):]
4252 t = pyrocko.util.str_to_time(arg)
4253 self.go_to_time(t, tlen=tlen)
4255 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4256 supl = '00:00:00'
4257 if len(supl) > len(arg):
4258 arg = arg + supl[-(len(supl)-len(arg)):]
4259 tmin, tmax = self.get_time_range()
4260 sdate = pyrocko.util.time_to_str(
4261 tmin/2.+tmax/2., format='%Y-%m-%d')
4262 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4263 self.go_to_time(t)
4265 elif arg == 'today':
4266 self.go_to_time(
4267 day_start(
4268 time.time()), tlen=24*60*60)
4270 elif arg == 'yesterday':
4271 self.go_to_time(
4272 day_start(
4273 time.time()-24*60*60), tlen=24*60*60)
4275 else:
4276 self.go_to_event_by_name(arg)
4278 else:
4279 raise PileViewerMainException(
4280 'No such command: %s' % command)
4282 except PileViewerMainException as e:
4283 error = str(e)
4284 hideit = False
4286 return clearit, hideit, error
4288 return PileViewerMain
4291PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4292GLPileViewerMain = MakePileViewerMainClass(qgl.QGLWidget)
4295class LineEditWithAbort(qw.QLineEdit):
4297 aborted = qc.pyqtSignal()
4298 history_down = qc.pyqtSignal()
4299 history_up = qc.pyqtSignal()
4301 def keyPressEvent(self, key_event):
4302 if key_event.key() == qc.Qt.Key_Escape:
4303 self.aborted.emit()
4304 elif key_event.key() == qc.Qt.Key_Down:
4305 self.history_down.emit()
4306 elif key_event.key() == qc.Qt.Key_Up:
4307 self.history_up.emit()
4308 else:
4309 return qw.QLineEdit.keyPressEvent(self, key_event)
4312class PileViewer(qw.QFrame):
4313 '''
4314 PileViewerMain + Controls + Inputline
4315 '''
4317 def __init__(
4318 self, pile,
4319 ntracks_shown_max=20,
4320 marker_editor_sortable=True,
4321 use_opengl=False,
4322 panel_parent=None,
4323 *args):
4325 qw.QFrame.__init__(self, *args)
4327 layout = qw.QGridLayout()
4328 layout.setContentsMargins(0, 0, 0, 0)
4329 layout.setSpacing(0)
4331 self.menu = PileViewerMenuBar(self)
4333 if use_opengl:
4334 self.viewer = GLPileViewerMain(
4335 pile,
4336 ntracks_shown_max=ntracks_shown_max,
4337 panel_parent=panel_parent,
4338 menu=self.menu)
4339 else:
4340 self.viewer = PileViewerMain(
4341 pile,
4342 ntracks_shown_max=ntracks_shown_max,
4343 panel_parent=panel_parent,
4344 menu=self.menu)
4346 self.marker_editor_sortable = marker_editor_sortable
4348 self.setFrameShape(qw.QFrame.StyledPanel)
4349 self.setFrameShadow(qw.QFrame.Sunken)
4351 self.input_area = qw.QFrame(self)
4352 ia_layout = qw.QGridLayout()
4353 ia_layout.setContentsMargins(11, 11, 11, 11)
4354 self.input_area.setLayout(ia_layout)
4356 self.inputline = LineEditWithAbort(self.input_area)
4357 self.inputline.returnPressed.connect(
4358 self.inputline_returnpressed)
4359 self.inputline.editingFinished.connect(
4360 self.inputline_finished)
4361 self.inputline.aborted.connect(
4362 self.inputline_aborted)
4364 self.inputline.history_down.connect(
4365 lambda: self.step_through_history(1))
4366 self.inputline.history_up.connect(
4367 lambda: self.step_through_history(-1))
4369 self.inputline.textEdited.connect(
4370 self.inputline_changed)
4372 self.inputline.setPlaceholderText(
4373 u'Quick commands: e.g. \'c HH?\' to select channels. '
4374 u'Use ↑ or ↓ to navigate.')
4375 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4376 self.input_area.hide()
4377 self.history = None
4379 self.inputline_error_str = None
4381 self.inputline_error = qw.QLabel()
4382 self.inputline_error.hide()
4384 ia_layout.addWidget(self.inputline, 0, 0)
4385 ia_layout.addWidget(self.inputline_error, 1, 0)
4386 layout.addWidget(self.input_area, 0, 0, 1, 2)
4387 layout.addWidget(self.viewer, 1, 0)
4389 pb = Progressbars(self)
4390 layout.addWidget(pb, 2, 0, 1, 2)
4391 self.progressbars = pb
4393 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4394 self.scrollbar = scrollbar
4395 layout.addWidget(scrollbar, 1, 1)
4396 self.scrollbar.valueChanged.connect(
4397 self.scrollbar_changed)
4399 self.block_scrollbar_changes = False
4401 self.viewer.want_input.connect(
4402 self.inputline_show)
4403 self.viewer.tracks_range_changed.connect(
4404 self.tracks_range_changed)
4405 self.viewer.pile_has_changed_signal.connect(
4406 self.adjust_controls)
4407 self.viewer.about_to_close.connect(
4408 self.save_inputline_history)
4410 self.setLayout(layout)
4412 def cleanup(self):
4413 self.viewer.cleanup()
4415 def get_progressbars(self):
4416 return self.progressbars
4418 def inputline_show(self):
4419 if not self.history:
4420 self.load_inputline_history()
4422 self.input_area.show()
4423 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4424 self.inputline.selectAll()
4426 def inputline_set_error(self, string):
4427 self.inputline_error_str = string
4428 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4429 self.inputline.selectAll()
4430 self.inputline_error.setText(string)
4431 self.input_area.show()
4432 self.inputline_error.show()
4434 def inputline_clear_error(self):
4435 if self.inputline_error_str:
4436 self.inputline.setPalette(qw.QApplication.palette())
4437 self.inputline_error_str = None
4438 self.inputline_error.clear()
4439 self.inputline_error.hide()
4441 def inputline_changed(self, line):
4442 self.viewer.inputline_changed(str(line))
4443 self.inputline_clear_error()
4445 def inputline_returnpressed(self):
4446 line = str(self.inputline.text())
4447 clearit, hideit, error = self.viewer.inputline_finished(line)
4449 if error:
4450 self.inputline_set_error(error)
4452 line = line.strip()
4454 if line != '' and not error:
4455 if not (len(self.history) >= 1 and line == self.history[-1]):
4456 self.history.append(line)
4458 if clearit:
4460 self.inputline.blockSignals(True)
4461 qpat, qinp = self.viewer.get_quick_filter_patterns()
4462 if qpat is None:
4463 self.inputline.clear()
4464 else:
4465 self.inputline.setText(qinp)
4466 self.inputline.blockSignals(False)
4468 if hideit and not error:
4469 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4470 self.input_area.hide()
4472 self.hist_ind = len(self.history)
4474 def inputline_aborted(self):
4475 '''
4476 Hide the input line.
4477 '''
4478 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4479 self.hist_ind = len(self.history)
4480 self.input_area.hide()
4482 def save_inputline_history(self):
4483 '''
4484 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4485 '''
4486 if not self.history:
4487 return
4489 conf = pyrocko.config
4490 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4491 with open(fn_hist, 'w') as f:
4492 i = min(100, len(self.history))
4493 for c in self.history[-i:]:
4494 f.write('%s\n' % c)
4496 def load_inputline_history(self):
4497 '''
4498 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4499 '''
4500 conf = pyrocko.config
4501 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4502 if not os.path.exists(fn_hist):
4503 with open(fn_hist, 'w+') as f:
4504 f.write('\n')
4506 with open(fn_hist, 'r') as f:
4507 self.history = [line.strip() for line in f.readlines()]
4509 self.hist_ind = len(self.history)
4511 def step_through_history(self, ud=1):
4512 '''
4513 Step through input line history and set the input line text.
4514 '''
4515 n = len(self.history)
4516 self.hist_ind += ud
4517 self.hist_ind %= (n + 1)
4518 if len(self.history) != 0 and self.hist_ind != n:
4519 self.inputline.setText(self.history[self.hist_ind])
4520 else:
4521 self.inputline.setText('')
4523 def inputline_finished(self):
4524 pass
4526 def tracks_range_changed(self, ntracks, ilo, ihi):
4527 if self.block_scrollbar_changes:
4528 return
4530 self.scrollbar.blockSignals(True)
4531 self.scrollbar.setPageStep(ihi-ilo)
4532 vmax = max(0, ntracks-(ihi-ilo))
4533 self.scrollbar.setRange(0, vmax)
4534 self.scrollbar.setValue(ilo)
4535 self.scrollbar.setHidden(vmax == 0)
4536 self.scrollbar.blockSignals(False)
4538 def scrollbar_changed(self, value):
4539 self.block_scrollbar_changes = True
4540 ilo = value
4541 ihi = ilo + self.scrollbar.pageStep()
4542 self.viewer.set_tracks_range((ilo, ihi))
4543 self.block_scrollbar_changes = False
4544 self.update_contents()
4546 def controls(self):
4547 frame = qw.QFrame(self)
4548 layout = qw.QGridLayout()
4549 frame.setLayout(layout)
4551 minfreq = 0.001
4552 maxfreq = 1000.0
4553 self.lowpass_control = ValControl(high_is_none=True)
4554 self.lowpass_control.setup(
4555 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4556 self.highpass_control = ValControl(low_is_none=True)
4557 self.highpass_control.setup(
4558 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4559 self.gain_control = ValControl()
4560 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4561 self.rot_control = LinValControl()
4562 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4563 self.colorbar_control = ColorbarControl(self)
4565 self.lowpass_control.valchange.connect(
4566 self.viewer.lowpass_change)
4567 self.highpass_control.valchange.connect(
4568 self.viewer.highpass_change)
4569 self.gain_control.valchange.connect(
4570 self.viewer.gain_change)
4571 self.rot_control.valchange.connect(
4572 self.viewer.rot_change)
4573 self.colorbar_control.cmap_changed.connect(
4574 self.viewer.waterfall_cmap_change
4575 )
4576 self.colorbar_control.clip_changed.connect(
4577 self.viewer.waterfall_clip_change
4578 )
4579 self.colorbar_control.show_absolute_toggled.connect(
4580 self.viewer.waterfall_show_absolute_change
4581 )
4582 self.colorbar_control.show_integrate_toggled.connect(
4583 self.viewer.waterfall_set_integrate
4584 )
4586 for icontrol, control in enumerate((
4587 self.highpass_control,
4588 self.lowpass_control,
4589 self.gain_control,
4590 self.rot_control,
4591 self.colorbar_control)):
4593 for iwidget, widget in enumerate(control.widgets()):
4594 layout.addWidget(widget, icontrol, iwidget)
4596 spacer = qw.QSpacerItem(
4597 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4598 layout.addItem(spacer, 4, 0, 1, 3)
4600 self.adjust_controls()
4601 self.viewer.viewmode_change(ViewMode.Wiggle)
4602 return frame
4604 def marker_editor(self):
4605 editor = pyrocko.gui.marker_editor.MarkerEditor(
4606 self, sortable=self.marker_editor_sortable)
4608 editor.set_viewer(self.get_view())
4609 editor.get_marker_model().dataChanged.connect(
4610 self.update_contents)
4611 return editor
4613 def adjust_controls(self):
4614 dtmin, dtmax = self.viewer.content_deltat_range()
4615 maxfreq = 0.5/dtmin
4616 minfreq = (0.5/dtmax)*0.001
4617 self.lowpass_control.set_range(minfreq, maxfreq)
4618 self.highpass_control.set_range(minfreq, maxfreq)
4620 def setup_snufflings(self):
4621 self.viewer.setup_snufflings()
4623 def get_view(self):
4624 return self.viewer
4626 def update_contents(self):
4627 self.viewer.update()
4629 def get_pile(self):
4630 return self.viewer.get_pile()
4632 def show_colorbar_ctrl(self, show):
4633 for w in self.colorbar_control.widgets():
4634 w.setVisible(show)
4636 def show_gain_ctrl(self, show):
4637 for w in self.gain_control.widgets():
4638 w.setVisible(show)