1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import, print_function
7import os
8import time
9import calendar
10import datetime
11import re
12import math
13import logging
14import operator
15import copy
16import enum
17from itertools import groupby
19import numpy as num
20import pyrocko.model
21import pyrocko.pile
22import pyrocko.trace
23import pyrocko.response
24import pyrocko.util
25import pyrocko.plot
26import pyrocko.gui.snuffling
27import pyrocko.gui.snufflings
28import pyrocko.gui.marker_editor
30from pyrocko.util import hpfloat, gmtime_x, mystrftime
32from .marker import associate_phases_to_events, MarkerOneNSLCRequired
34from .util import (ValControl, LinValControl, Marker, EventMarker,
35 PhaseMarker, make_QPolygonF, draw_label, Label,
36 Progressbars, ColorbarControl)
38from .qt_compat import qc, qg, qw, qsvg
40from .pile_viewer_waterfall import TraceWaterfall
42import scipy.stats as sstats
43import platform
45MIN_LABEL_SIZE_PT = 6
48qc.QString = str
50qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
51 qw.QFileDialog.DontUseSheet
53is_macos = platform.uname()[0] == 'Darwin'
55logger = logging.getLogger('pyrocko.gui.pile_viewer')
58def detrend(x, y):
59 slope, offset, _, _, _ = sstats.linregress(x, y)
60 y_detrended = y - slope * x - offset
61 return y_detrended, slope, offset
64def retrend(x, y_detrended, slope, offset):
65 return x * slope + y_detrended + offset
68class Global(object):
69 appOnDemand = None
72class NSLC(object):
73 def __init__(self, n, s, l=None, c=None): # noqa
74 self.network = n
75 self.station = s
76 self.location = l
77 self.channel = c
80class m_float(float):
82 def __str__(self):
83 if abs(self) >= 10000.:
84 return '%g km' % round(self/1000., 0)
85 elif abs(self) >= 1000.:
86 return '%g km' % round(self/1000., 1)
87 else:
88 return '%.5g m' % self
90 def __lt__(self, other):
91 if other is None:
92 return True
93 return float(self) < float(other)
95 def __gt__(self, other):
96 if other is None:
97 return False
98 return float(self) > float(other)
101def m_float_or_none(x):
102 if x is None:
103 return None
104 else:
105 return m_float(x)
108def make_chunks(items):
109 '''
110 Split a list of integers into sublists of consecutive elements.
111 '''
112 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
113 enumerate(items), (lambda x: x[1]-x[0]))]
116class deg_float(float):
118 def __str__(self):
119 return '%4.0f' % self
121 def __lt__(self, other):
122 if other is None:
123 return True
124 return float(self) < float(other)
126 def __gt__(self, other):
127 if other is None:
128 return False
129 return float(self) > float(other)
132def deg_float_or_none(x):
133 if x is None:
134 return None
135 else:
136 return deg_float(x)
139class sector_int(int):
141 def __str__(self):
142 return '[%i]' % self
144 def __lt__(self, other):
145 if other is None:
146 return True
147 return int(self) < int(other)
149 def __gt__(self, other):
150 if other is None:
151 return False
152 return int(self) > int(other)
155def num_to_html(num):
156 snum = '%g' % num
157 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
158 if m:
159 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
161 return snum
164gap_lap_tolerance = 5.
167class ViewMode(enum.Enum):
168 Wiggle = 1
169 Waterfall = 2
172class Timer(object):
173 def __init__(self):
174 self._start = None
175 self._stop = None
177 def start(self):
178 self._start = os.times()
180 def stop(self):
181 self._stop = os.times()
183 def get(self):
184 a = self._start
185 b = self._stop
186 if a is not None and b is not None:
187 return tuple([b[i] - a[i] for i in range(5)])
188 else:
189 return tuple([0.] * 5)
191 def __sub__(self, other):
192 a = self.get()
193 b = other.get()
194 return tuple([a[i] - b[i] for i in range(5)])
197class ObjectStyle(object):
198 def __init__(self, frame_pen, fill_brush):
199 self.frame_pen = frame_pen
200 self.fill_brush = fill_brush
203box_styles = []
204box_alpha = 100
205for color in 'orange skyblue butter chameleon chocolate plum ' \
206 'scarletred'.split():
208 box_styles.append(ObjectStyle(
209 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
210 qg.QBrush(qg.QColor(
211 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
212 ))
214box_styles_coverage = {}
216box_styles_coverage['waveform'] = [
217 ObjectStyle(
218 qg.QPen(
219 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
220 1, qc.Qt.DashLine),
221 qg.QBrush(qg.QColor(
222 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
223 ),
224 ObjectStyle(
225 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
226 qg.QBrush(qg.QColor(
227 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
228 ),
229 ObjectStyle(
230 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
231 qg.QBrush(qg.QColor(
232 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
233 )]
235box_styles_coverage['waveform_promise'] = [
236 ObjectStyle(
237 qg.QPen(
238 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
239 1, qc.Qt.DashLine),
240 qg.QBrush(qg.QColor(
241 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
242 ),
243 ObjectStyle(
244 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
245 qg.QBrush(qg.QColor(
246 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
247 ),
248 ObjectStyle(
249 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
250 qg.QBrush(qg.QColor(
251 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
252 )]
254sday = 60*60*24. # \
255smonth = 60*60*24*30. # | only used as approx. intervals...
256syear = 60*60*24*365. # /
258acceptable_tincs = num.array([
259 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
260 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
263working_system_time_range = \
264 pyrocko.util.working_system_time_range()
266initial_time_range = []
268try:
269 initial_time_range.append(
270 calendar.timegm((1950, 1, 1, 0, 0, 0)))
271except Exception:
272 initial_time_range.append(working_system_time_range[0])
274try:
275 initial_time_range.append(
276 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
277except Exception:
278 initial_time_range.append(working_system_time_range[1])
281def is_working_time(t):
282 return working_system_time_range[0] <= t and \
283 t <= working_system_time_range[1]
286def fancy_time_ax_format(inc):
287 l0_fmt_brief = ''
288 l2_fmt = ''
289 l2_trig = 0
290 if inc < 0.000001:
291 l0_fmt = '.%n'
292 l0_center = False
293 l1_fmt = '%H:%M:%S'
294 l1_trig = 6
295 l2_fmt = '%b %d, %Y'
296 l2_trig = 3
297 elif inc < 0.001:
298 l0_fmt = '.%u'
299 l0_center = False
300 l1_fmt = '%H:%M:%S'
301 l1_trig = 6
302 l2_fmt = '%b %d, %Y'
303 l2_trig = 3
304 elif inc < 1:
305 l0_fmt = '.%r'
306 l0_center = False
307 l1_fmt = '%H:%M:%S'
308 l1_trig = 6
309 l2_fmt = '%b %d, %Y'
310 l2_trig = 3
311 elif inc < 60:
312 l0_fmt = '%H:%M:%S'
313 l0_center = False
314 l1_fmt = '%b %d, %Y'
315 l1_trig = 3
316 elif inc < 3600:
317 l0_fmt = '%H:%M'
318 l0_center = False
319 l1_fmt = '%b %d, %Y'
320 l1_trig = 3
321 elif inc < sday:
322 l0_fmt = '%H:%M'
323 l0_center = False
324 l1_fmt = '%b %d, %Y'
325 l1_trig = 3
326 elif inc < smonth:
327 l0_fmt = '%a %d'
328 l0_fmt_brief = '%d'
329 l0_center = True
330 l1_fmt = '%b, %Y'
331 l1_trig = 2
332 elif inc < syear:
333 l0_fmt = '%b'
334 l0_center = True
335 l1_fmt = '%Y'
336 l1_trig = 1
337 else:
338 l0_fmt = '%Y'
339 l0_center = False
340 l1_fmt = ''
341 l1_trig = 0
343 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
346def day_start(timestamp):
347 tt = time.gmtime(int(timestamp))
348 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
349 return calendar.timegm(tts)
352def month_start(timestamp):
353 tt = time.gmtime(int(timestamp))
354 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
355 return calendar.timegm(tts)
358def year_start(timestamp):
359 tt = time.gmtime(int(timestamp))
360 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
361 return calendar.timegm(tts)
364def time_nice_value(inc0):
365 if inc0 < acceptable_tincs[0]:
366 return pyrocko.plot.nice_value(inc0)
367 elif inc0 > acceptable_tincs[-1]:
368 return pyrocko.plot.nice_value(inc0/syear)*syear
369 else:
370 i = num.argmin(num.abs(acceptable_tincs-inc0))
371 return acceptable_tincs[i]
374class TimeScaler(pyrocko.plot.AutoScaler):
375 def __init__(self):
376 pyrocko.plot.AutoScaler.__init__(self)
377 self.mode = 'min-max'
379 def make_scale(self, data_range):
380 assert self.mode in ('min-max', 'off'), \
381 'mode must be "min-max" or "off" for TimeScaler'
383 data_min = min(data_range)
384 data_max = max(data_range)
385 is_reverse = (data_range[0] > data_range[1])
387 mi, ma = data_min, data_max
388 nmi = mi
389 if self.mode != 'off':
390 nmi = mi - self.space*(ma-mi)
392 nma = ma
393 if self.mode != 'off':
394 nma = ma + self.space*(ma-mi)
396 mi, ma = nmi, nma
398 if mi == ma and self.mode != 'off':
399 mi -= 1.0
400 ma += 1.0
402 mi = max(working_system_time_range[0], mi)
403 ma = min(working_system_time_range[1], ma)
405 # make nice tick increment
406 if self.inc is not None:
407 inc = self.inc
408 else:
409 if self.approx_ticks > 0.:
410 inc = time_nice_value((ma-mi)/self.approx_ticks)
411 else:
412 inc = time_nice_value((ma-mi)*10.)
414 if inc == 0.0:
415 inc = 1.0
417 if is_reverse:
418 return ma, mi, -inc
419 else:
420 return mi, ma, inc
422 def make_ticks(self, data_range):
423 mi, ma, inc = self.make_scale(data_range)
425 is_reverse = False
426 if inc < 0:
427 mi, ma, inc = ma, mi, -inc
428 is_reverse = True
430 ticks = []
432 if inc < sday:
433 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
434 if inc < 0.001:
435 mi_day = hpfloat(mi_day)
437 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
438 if inc < 0.001:
439 base = hpfloat(base)
441 base_day = mi_day
442 i = 0
443 while True:
444 tick = base+i*inc
445 if tick > ma:
446 break
448 tick_day = day_start(tick)
449 if tick_day > base_day:
450 base_day = tick_day
451 base = base_day
452 i = 0
453 else:
454 ticks.append(tick)
455 i += 1
457 elif inc < smonth:
458 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
459 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
460 delta = datetime.timedelta(days=int(round(inc/sday)))
461 if mi_day == mi:
462 dt_base += delta
463 i = 0
464 while True:
465 current = dt_base + i*delta
466 tick = calendar.timegm(current.timetuple())
467 if tick > ma:
468 break
469 ticks.append(tick)
470 i += 1
472 elif inc < syear:
473 mi_month = month_start(max(
474 mi, working_system_time_range[0]+smonth*1.5))
476 y, m = time.gmtime(mi_month)[:2]
477 while True:
478 tick = calendar.timegm((y, m, 1, 0, 0, 0))
479 m += 1
480 if m > 12:
481 y, m = y+1, 1
483 if tick > ma:
484 break
486 if tick >= mi:
487 ticks.append(tick)
489 else:
490 mi_year = year_start(max(
491 mi, working_system_time_range[0]+syear*1.5))
493 incy = int(round(inc/syear))
494 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
496 while True:
497 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
498 y += incy
499 if tick > ma:
500 break
501 if tick >= mi:
502 ticks.append(tick)
504 if is_reverse:
505 ticks.reverse()
507 return ticks, inc
510def need_l1_tick(tt, ms, l1_trig):
511 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
514def tick_to_labels(tick, inc):
515 tt, ms = gmtime_x(tick)
516 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
517 fancy_time_ax_format(inc)
519 l0 = mystrftime(l0_fmt, tt, ms)
520 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
521 l1, l2 = None, None
522 if need_l1_tick(tt, ms, l1_trig):
523 l1 = mystrftime(l1_fmt, tt, ms)
524 if need_l1_tick(tt, ms, l2_trig):
525 l2 = mystrftime(l2_fmt, tt, ms)
527 return l0, l0_brief, l0_center, l1, l2
530def l1_l2_tick(tick, inc):
531 tt, ms = gmtime_x(tick)
532 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
533 fancy_time_ax_format(inc)
535 l1 = mystrftime(l1_fmt, tt, ms)
536 l2 = mystrftime(l2_fmt, tt, ms)
537 return l1, l2
540class TimeAx(TimeScaler):
541 def __init__(self, *args):
542 TimeScaler.__init__(self, *args)
544 def drawit(self, p, xprojection, yprojection):
545 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
546 p.setPen(pen)
547 font = qg.QFont()
548 font.setBold(True)
549 p.setFont(font)
550 fm = p.fontMetrics()
551 ticklen = 10
552 pad = 10
553 tmin, tmax = xprojection.get_in_range()
554 ticks, inc = self.make_ticks((tmin, tmax))
555 l1_hits = 0
556 l2_hits = 0
558 vmin, vmax = yprojection(0), yprojection(ticklen)
559 uumin, uumax = xprojection.get_out_range()
560 first_tick_with_label = None
561 for tick in ticks:
562 umin = xprojection(tick)
564 umin_approx_next = xprojection(tick+inc)
565 umax = xprojection(tick)
567 pinc_approx = umin_approx_next - umin
569 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
570 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
572 if tick == 0.0 and tmax - tmin < 3600*24:
573 # hide year at epoch (we assume that synthetic data is shown)
574 if l2:
575 l2 = None
576 elif l1:
577 l1 = None
579 if l0_center:
580 ushift = (umin_approx_next-umin)/2.
581 else:
582 ushift = 0.
584 for l0x in (l0, l0_brief, ''):
585 label0 = l0x
586 rect0 = fm.boundingRect(label0)
587 if rect0.width() <= pinc_approx*0.9:
588 break
590 if uumin+pad < umin-rect0.width()/2.+ushift and \
591 umin+rect0.width()/2.+ushift < uumax-pad:
593 if first_tick_with_label is None:
594 first_tick_with_label = tick
595 p.drawText(qc.QPointF(
596 umin-rect0.width()/2.+ushift,
597 vmin+rect0.height()+ticklen), label0)
599 if l1:
600 label1 = l1
601 rect1 = fm.boundingRect(label1)
602 if uumin+pad < umin-rect1.width()/2. and \
603 umin+rect1.width()/2. < uumax-pad:
605 p.drawText(qc.QPointF(
606 umin-rect1.width()/2.,
607 vmin+rect0.height()+rect1.height()+ticklen),
608 label1)
610 l1_hits += 1
612 if l2:
613 label2 = l2
614 rect2 = fm.boundingRect(label2)
615 if uumin+pad < umin-rect2.width()/2. and \
616 umin+rect2.width()/2. < uumax-pad:
618 p.drawText(qc.QPointF(
619 umin-rect2.width()/2.,
620 vmin+rect0.height()+rect1.height()+rect2.height() +
621 ticklen), label2)
623 l2_hits += 1
625 if first_tick_with_label is None:
626 first_tick_with_label = tmin
628 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
630 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
631 tmax - tmin < 3600*24:
633 # hide year at epoch (we assume that synthetic data is shown)
634 if l2:
635 l2 = None
636 elif l1:
637 l1 = None
639 if l1_hits == 0 and l1:
640 label1 = l1
641 rect1 = fm.boundingRect(label1)
642 p.drawText(qc.QPointF(
643 uumin+pad,
644 vmin+rect0.height()+rect1.height()+ticklen),
645 label1)
647 l1_hits += 1
649 if l2_hits == 0 and l2:
650 label2 = l2
651 rect2 = fm.boundingRect(label2)
652 p.drawText(qc.QPointF(
653 uumin+pad,
654 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
655 label2)
657 v = yprojection(0)
658 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
661class Projection(object):
662 def __init__(self):
663 self.xr = 0., 1.
664 self.ur = 0., 1.
666 def set_in_range(self, xmin, xmax):
667 if xmax == xmin:
668 xmax = xmin + 1.
670 self.xr = xmin, xmax
672 def get_in_range(self):
673 return self.xr
675 def set_out_range(self, umin, umax):
676 if umax == umin:
677 umax = umin + 1.
679 self.ur = umin, umax
681 def get_out_range(self):
682 return self.ur
684 def __call__(self, x):
685 umin, umax = self.ur
686 xmin, xmax = self.xr
687 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
689 def clipped(self, x, umax_pad):
690 umin, umax = self.ur
691 xmin, xmax = self.xr
692 return min(
693 umax-umax_pad,
694 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
696 def rev(self, u):
697 umin, umax = self.ur
698 xmin, xmax = self.xr
699 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
701 def copy(self):
702 return copy.copy(self)
705def add_radiobuttongroup(menu, menudef, target, default=None):
706 group = qw.QActionGroup(menu)
707 group.setExclusive(True)
708 menuitems = []
710 for name, value, *shortcut in menudef:
711 action = menu.addAction(name)
712 action.setCheckable(True)
713 action.setActionGroup(group)
714 if shortcut:
715 action.setShortcut(shortcut[0])
717 menuitems.append((action, value))
718 if default is not None and (
719 name.lower().replace(' ', '_') == default or
720 value == default):
721 action.setChecked(True)
723 group.triggered.connect(target)
725 if default is None:
726 menuitems[0][0].setChecked(True)
728 return menuitems
731def sort_actions(menu):
732 actions = [act for act in menu.actions() if not act.menu()]
733 for action in actions:
734 menu.removeAction(action)
735 actions.sort(key=lambda x: str(x.text()))
737 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
738 if help_action:
739 actions.insert(0, actions.pop(actions.index(help_action[0])))
740 for action in actions:
741 menu.addAction(action)
744fkey_map = dict(zip(
745 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
746 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
747 range(10)))
750class PileViewerMainException(Exception):
751 pass
754class PileViewerMenuBar(qw.QMenuBar):
755 ...
758class PileViewerMenu(qw.QMenu):
759 ...
762def MakePileViewerMainClass(base):
764 class PileViewerMain(base):
766 want_input = qc.pyqtSignal()
767 about_to_close = qc.pyqtSignal()
768 pile_has_changed_signal = qc.pyqtSignal()
769 tracks_range_changed = qc.pyqtSignal(int, int, int)
771 begin_markers_add = qc.pyqtSignal(int, int)
772 end_markers_add = qc.pyqtSignal()
773 begin_markers_remove = qc.pyqtSignal(int, int)
774 end_markers_remove = qc.pyqtSignal()
776 marker_selection_changed = qc.pyqtSignal(list)
777 active_event_marker_changed = qc.pyqtSignal()
779 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
780 menu=None):
781 base.__init__(self, *args)
783 self.pile = pile
784 self.ax_height = 80
785 self.panel_parent = panel_parent
787 self.click_tolerance = 5
789 self.ntracks_shown_max = ntracks_shown_max
790 self.initial_ntracks_shown_max = ntracks_shown_max
791 self.ntracks = 0
792 self.show_all = True
793 self.shown_tracks_range = None
794 self.track_start = None
795 self.track_trange = None
797 self.lowpass = None
798 self.highpass = None
799 self.gain = 1.0
800 self.rotate = 0.0
801 self.picking_down = None
802 self.picking = None
803 self.floating_marker = None
804 self.markers = pyrocko.pile.Sorted([], 'tmin')
805 self.markers_deltat_max = 0.
806 self.n_selected_markers = 0
807 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
808 self.visible_marker_kinds = self.all_marker_kinds
809 self.active_event_marker = None
810 self.ignore_releases = 0
811 self.message = None
812 self.reloaded = False
813 self.pile_has_changed = False
814 self.config = pyrocko.config.config('snuffler')
816 self.tax = TimeAx()
817 self.setBackgroundRole(qg.QPalette.Base)
818 self.setAutoFillBackground(True)
819 poli = qw.QSizePolicy(
820 qw.QSizePolicy.Expanding,
821 qw.QSizePolicy.Expanding)
823 self.setSizePolicy(poli)
824 self.setMinimumSize(300, 200)
825 self.setFocusPolicy(qc.Qt.ClickFocus)
827 self.menu = menu or PileViewerMenu(self)
829 file_menu = self.menu.addMenu('&File')
830 view_menu = self.menu.addMenu('&View')
831 options_menu = self.menu.addMenu('&Options')
832 scale_menu = self.menu.addMenu('&Scaling')
833 sort_menu = self.menu.addMenu('Sor&ting')
834 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
836 help_menu = self.menu.addMenu('&Help')
838 self.snufflings_menu = self.toggle_panel_menu.addMenu(
839 'Run Snuffling')
840 self.toggle_panel_menu.addSeparator()
841 self.snuffling_help = help_menu.addMenu('Snuffling Help')
842 help_menu.addSeparator()
844 file_menu.addAction(
845 qg.QIcon.fromTheme('document-open'),
846 'Open waveform files...',
847 self.open_waveforms,
848 qg.QKeySequence.Open)
850 file_menu.addAction(
851 qg.QIcon.fromTheme('document-open'),
852 'Open waveform directory...',
853 self.open_waveform_directory)
855 file_menu.addAction(
856 'Open station files...',
857 self.open_stations)
859 file_menu.addAction(
860 'Open StationXML files...',
861 self.open_stations_xml)
863 file_menu.addAction(
864 'Open event file...',
865 self.read_events)
867 file_menu.addSeparator()
868 file_menu.addAction(
869 'Open marker file...',
870 self.read_markers)
872 file_menu.addAction(
873 qg.QIcon.fromTheme('document-save'),
874 'Save markers...',
875 self.write_markers,
876 qg.QKeySequence.Save)
878 file_menu.addAction(
879 qg.QIcon.fromTheme('document-save-as'),
880 'Save selected markers...',
881 self.write_selected_markers,
882 qg.QKeySequence.SaveAs)
884 file_menu.addSeparator()
885 file_menu.addAction(
886 qg.QIcon.fromTheme('document-print'),
887 'Print',
888 self.printit,
889 qg.QKeySequence.Print)
891 file_menu.addAction(
892 qg.QIcon.fromTheme('insert-image'),
893 'Save as SVG or PNG',
894 self.savesvg,
895 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
897 file_menu.addSeparator()
898 close = file_menu.addAction(
899 qg.QIcon.fromTheme('window-close'),
900 'Close',
901 self.myclose)
902 close.setShortcuts(
903 (qg.QKeySequence(qc.Qt.Key_Q),
904 qg.QKeySequence(qc.Qt.Key_X)))
906 # Scale Menu
907 menudef = [
908 ('Individual Scale',
909 lambda tr: tr.nslc_id,
910 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
911 ('Common Scale',
912 lambda tr: None,
913 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
914 ('Common Scale per Station',
915 lambda tr: (tr.network, tr.station),
916 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
917 ('Common Scale per Station Location',
918 lambda tr: (tr.network, tr.station, tr.location)),
919 ('Common Scale per Component',
920 lambda tr: (tr.channel)),
921 ]
923 self.menuitems_scaling = add_radiobuttongroup(
924 scale_menu, menudef, self.scalingmode_change,
925 default=self.config.trace_scale)
926 scale_menu.addSeparator()
928 self.scaling_key = self.menuitems_scaling[0][1]
929 self.scaling_hooks = {}
930 self.scalingmode_change()
932 menudef = [
933 ('Scaling based on Minimum and Maximum',
934 ('minmax', 'minmax')),
935 ('Scaling based on Minimum and Maximum (Robust)',
936 ('minmax', 'robust')),
937 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
938 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
939 ]
941 self.menuitems_scaling_base = add_radiobuttongroup(
942 scale_menu, menudef, self.scaling_base_change)
944 self.scaling_base = self.menuitems_scaling_base[0][1]
945 scale_menu.addSeparator()
947 self.menuitem_fixscalerange = scale_menu.addAction(
948 'Fix Scale Ranges')
949 self.menuitem_fixscalerange.setCheckable(True)
951 # Sort Menu
952 def sector_dist(sta):
953 if sta.dist_m is None:
954 return None, None
955 else:
956 return (
957 sector_int(round((sta.azimuth+15.)/30.)),
958 m_float(sta.dist_m))
960 menudef = [
961 ('Sort by Names',
962 lambda tr: (),
963 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
964 ('Sort by Distance',
965 lambda tr: self.station_attrib(
966 tr,
967 lambda sta: (m_float_or_none(sta.dist_m),),
968 lambda tr: (None,)),
969 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
970 ('Sort by Azimuth',
971 lambda tr: self.station_attrib(
972 tr,
973 lambda sta: (deg_float_or_none(sta.azimuth),),
974 lambda tr: (None,))),
975 ('Sort by Distance in 12 Azimuthal Blocks',
976 lambda tr: self.station_attrib(
977 tr,
978 sector_dist,
979 lambda tr: (None, None))),
980 ('Sort by Backazimuth',
981 lambda tr: self.station_attrib(
982 tr,
983 lambda sta: (deg_float_or_none(sta.backazimuth),),
984 lambda tr: (None,))),
985 ]
986 self.menuitems_ssorting = add_radiobuttongroup(
987 sort_menu, menudef, self.s_sortingmode_change)
988 sort_menu.addSeparator()
990 self._ssort = lambda tr: ()
992 self.menu.addSeparator()
994 menudef = [
995 ('Subsort by Network, Station, Location, Channel',
996 ((0, 1, 2, 3), # gathering
997 lambda tr: tr.location)), # coloring
998 ('Subsort by Network, Station, Channel, Location',
999 ((0, 1, 3, 2),
1000 lambda tr: tr.channel)),
1001 ('Subsort by Station, Network, Channel, Location',
1002 ((1, 0, 3, 2),
1003 lambda tr: tr.channel)),
1004 ('Subsort by Location, Network, Station, Channel',
1005 ((2, 0, 1, 3),
1006 lambda tr: tr.channel)),
1007 ('Subsort by Channel, Network, Station, Location',
1008 ((3, 0, 1, 2),
1009 lambda tr: (tr.network, tr.station, tr.location))),
1010 ('Subsort by Network, Station, Channel (Grouped by Location)',
1011 ((0, 1, 3),
1012 lambda tr: tr.location)),
1013 ('Subsort by Station, Network, Channel (Grouped by Location)',
1014 ((1, 0, 3),
1015 lambda tr: tr.location)),
1016 ]
1018 self.menuitems_sorting = add_radiobuttongroup(
1019 sort_menu, menudef, self.sortingmode_change)
1021 menudef = [(x.key, x.value) for x in
1022 self.config.visible_length_setting]
1024 # View menu
1025 self.menuitems_visible_length = add_radiobuttongroup(
1026 view_menu, menudef,
1027 self.visible_length_change)
1028 view_menu.addSeparator()
1030 view_modes = [
1031 ('Wiggle Plot', ViewMode.Wiggle),
1032 ('Waterfall', ViewMode.Waterfall)
1033 ]
1035 self.menuitems_viewmode = add_radiobuttongroup(
1036 view_menu, view_modes,
1037 self.viewmode_change, default=ViewMode.Wiggle)
1038 view_menu.addSeparator()
1040 self.menuitem_cliptraces = view_menu.addAction(
1041 'Clip Traces')
1042 self.menuitem_cliptraces.setCheckable(True)
1043 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1045 self.menuitem_showboxes = view_menu.addAction(
1046 'Show Boxes')
1047 self.menuitem_showboxes.setCheckable(True)
1048 self.menuitem_showboxes.setChecked(
1049 self.config.show_boxes)
1051 self.menuitem_colortraces = view_menu.addAction(
1052 'Color Traces')
1053 self.menuitem_colortraces.setCheckable(True)
1054 self.menuitem_antialias = view_menu.addAction(
1055 'Antialiasing')
1056 self.menuitem_antialias.setCheckable(True)
1058 view_menu.addSeparator()
1059 self.menuitem_showscalerange = view_menu.addAction(
1060 'Show Scale Ranges')
1061 self.menuitem_showscalerange.setCheckable(True)
1062 self.menuitem_showscalerange.setChecked(
1063 self.config.show_scale_ranges)
1065 self.menuitem_showscaleaxis = view_menu.addAction(
1066 'Show Scale Axes')
1067 self.menuitem_showscaleaxis.setCheckable(True)
1068 self.menuitem_showscaleaxis.setChecked(
1069 self.config.show_scale_axes)
1071 self.menuitem_showzeroline = view_menu.addAction(
1072 'Show Zero Lines')
1073 self.menuitem_showzeroline.setCheckable(True)
1075 view_menu.addSeparator()
1076 view_menu.addAction(
1077 qg.QIcon.fromTheme('view-fullscreen'),
1078 'Fullscreen',
1079 self.toggle_fullscreen,
1080 qg.QKeySequence(qc.Qt.Key_F11))
1082 # Options Menu
1083 self.menuitem_demean = options_menu.addAction('Demean')
1084 self.menuitem_demean.setCheckable(True)
1085 self.menuitem_demean.setChecked(self.config.demean)
1086 self.menuitem_demean.setShortcut(
1087 qg.QKeySequence(qc.Qt.Key_Underscore))
1089 self.menuitem_distances_3d = options_menu.addAction(
1090 '3D distances',
1091 self.distances_3d_changed)
1092 self.menuitem_distances_3d.setCheckable(True)
1094 self.menuitem_allowdownsampling = options_menu.addAction(
1095 'Allow Downsampling')
1096 self.menuitem_allowdownsampling.setCheckable(True)
1097 self.menuitem_allowdownsampling.setChecked(True)
1099 self.menuitem_degap = options_menu.addAction(
1100 'Allow Degapping')
1101 self.menuitem_degap.setCheckable(True)
1102 self.menuitem_degap.setChecked(True)
1104 options_menu.addSeparator()
1106 self.menuitem_fft_filtering = options_menu.addAction(
1107 'FFT Filtering')
1108 self.menuitem_fft_filtering.setCheckable(True)
1110 self.menuitem_lphp = options_menu.addAction(
1111 'Bandpass is Low- + Highpass')
1112 self.menuitem_lphp.setCheckable(True)
1113 self.menuitem_lphp.setChecked(True)
1115 options_menu.addSeparator()
1116 self.menuitem_watch = options_menu.addAction(
1117 'Watch Files')
1118 self.menuitem_watch.setCheckable(True)
1120 self.menuitem_liberal_fetch = options_menu.addAction(
1121 'Liberal Fetch Optimization')
1122 self.menuitem_liberal_fetch.setCheckable(True)
1124 self.visible_length = menudef[0][1]
1126 self.snufflings_menu.addAction(
1127 'Reload Snufflings',
1128 self.setup_snufflings)
1130 # Disable ShadowPileTest
1131 if False:
1132 test_action = self.menu.addAction(
1133 'Test',
1134 self.toggletest)
1135 test_action.setCheckable(True)
1137 help_menu.addAction(
1138 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1139 'Snuffler Controls',
1140 self.help,
1141 qg.QKeySequence(qc.Qt.Key_Question))
1143 help_menu.addAction(
1144 'About',
1145 self.about)
1147 self.time_projection = Projection()
1148 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1149 self.time_projection.set_out_range(0., self.width())
1151 self.gather = None
1153 self.trace_filter = None
1154 self.quick_filter = None
1155 self.quick_filter_patterns = None, None
1156 self.blacklist = []
1158 self.track_to_screen = Projection()
1159 self.track_to_nslc_ids = {}
1161 self.cached_vec = None
1162 self.cached_processed_traces = None
1164 self.timer = qc.QTimer(self)
1165 self.timer.timeout.connect(self.periodical)
1166 self.timer.setInterval(1000)
1167 self.timer.start()
1168 self.pile.add_listener(self)
1169 self.trace_styles = {}
1170 if self.get_squirrel() is None:
1171 self.determine_box_styles()
1173 self.setMouseTracking(True)
1175 user_home_dir = os.path.expanduser('~')
1176 self.snuffling_modules = {}
1177 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1178 self.default_snufflings = None
1179 self.snufflings = []
1181 self.stations = {}
1183 self.timer_draw = Timer()
1184 self.timer_cutout = Timer()
1185 self.time_spent_painting = 0.0
1186 self.time_last_painted = time.time()
1188 self.interactive_range_change_time = 0.0
1189 self.interactive_range_change_delay_time = 10.0
1190 self.follow_timer = None
1192 self.sortingmode_change_time = 0.0
1193 self.sortingmode_change_delay_time = None
1195 self.old_data_ranges = {}
1197 self.error_messages = {}
1198 self.return_tag = None
1199 self.wheel_pos = 60
1201 self.setAcceptDrops(True)
1202 self._paths_to_load = []
1204 self.tf_cache = {}
1206 self.waterfall = TraceWaterfall()
1207 self.waterfall_cmap = 'viridis'
1208 self.waterfall_clip_min = 0.
1209 self.waterfall_clip_max = 1.
1210 self.waterfall_show_absolute = False
1211 self.waterfall_integrate = False
1212 self.view_mode = ViewMode.Wiggle
1214 self.automatic_updates = True
1216 self.closing = False
1217 self.in_paint_event = False
1219 def fail(self, reason):
1220 box = qw.QMessageBox(self)
1221 box.setText(reason)
1222 box.exec_()
1224 def set_trace_filter(self, filter_func):
1225 self.trace_filter = filter_func
1226 self.sortingmode_change()
1228 def update_trace_filter(self):
1229 if self.blacklist:
1231 def blacklist_func(tr):
1232 return not pyrocko.util.match_nslc(
1233 self.blacklist, tr.nslc_id)
1235 else:
1236 blacklist_func = None
1238 if self.quick_filter is None and blacklist_func is None:
1239 self.set_trace_filter(None)
1240 elif self.quick_filter is None:
1241 self.set_trace_filter(blacklist_func)
1242 elif blacklist_func is None:
1243 self.set_trace_filter(self.quick_filter)
1244 else:
1245 self.set_trace_filter(
1246 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1248 def set_quick_filter(self, filter_func):
1249 self.quick_filter = filter_func
1250 self.update_trace_filter()
1252 def set_quick_filter_patterns(self, patterns, inputline=None):
1253 if patterns is not None:
1254 self.set_quick_filter(
1255 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1256 else:
1257 self.set_quick_filter(None)
1259 self.quick_filter_patterns = patterns, inputline
1261 def get_quick_filter_patterns(self):
1262 return self.quick_filter_patterns
1264 def add_blacklist_pattern(self, pattern):
1265 if pattern == 'empty':
1266 keys = set(self.pile.nslc_ids)
1267 trs = self.pile.all(
1268 tmin=self.tmin,
1269 tmax=self.tmax,
1270 load_data=False,
1271 degap=False)
1273 for tr in trs:
1274 if tr.nslc_id in keys:
1275 keys.remove(tr.nslc_id)
1277 for key in keys:
1278 xpattern = '.'.join(key)
1279 if xpattern not in self.blacklist:
1280 self.blacklist.append(xpattern)
1282 else:
1283 if pattern in self.blacklist:
1284 self.blacklist.remove(pattern)
1286 self.blacklist.append(pattern)
1288 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1289 self.update_trace_filter()
1291 def remove_blacklist_pattern(self, pattern):
1292 if pattern in self.blacklist:
1293 self.blacklist.remove(pattern)
1294 else:
1295 raise PileViewerMainException(
1296 'Pattern not found in blacklist.')
1298 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1299 self.update_trace_filter()
1301 def clear_blacklist(self):
1302 self.blacklist = []
1303 self.update_trace_filter()
1305 def ssort(self, tr):
1306 return self._ssort(tr)
1308 def station_key(self, x):
1309 return x.network, x.station
1311 def station_keys(self, x):
1312 return [
1313 (x.network, x.station, x.location),
1314 (x.network, x.station)]
1316 def station_attrib(self, tr, getter, default_getter):
1317 for sk in self.station_keys(tr):
1318 if sk in self.stations:
1319 station = self.stations[sk]
1320 return getter(station)
1322 return default_getter(tr)
1324 def get_station(self, sk):
1325 return self.stations[sk]
1327 def has_station(self, station):
1328 for sk in self.station_keys(station):
1329 if sk in self.stations:
1330 return True
1332 return False
1334 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1335 return self.station_attrib(
1336 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1338 def set_stations(self, stations):
1339 self.stations = {}
1340 self.add_stations(stations)
1342 def add_stations(self, stations):
1343 for station in stations:
1344 for sk in self.station_keys(station):
1345 self.stations[sk] = station
1347 ev = self.get_active_event()
1348 if ev:
1349 self.set_origin(ev)
1351 def add_event(self, event):
1352 marker = EventMarker(event)
1353 self.add_marker(marker)
1355 def add_events(self, events):
1356 markers = [EventMarker(e) for e in events]
1357 self.add_markers(markers)
1359 def set_event_marker_as_origin(self, ignore=None):
1360 selected = self.selected_markers()
1361 if not selected:
1362 self.fail('An event marker must be selected.')
1363 return
1365 m = selected[0]
1366 if not isinstance(m, EventMarker):
1367 self.fail('Selected marker is not an event.')
1368 return
1370 self.set_active_event_marker(m)
1372 def deactivate_event_marker(self):
1373 if self.active_event_marker:
1374 self.active_event_marker.active = False
1376 self.active_event_marker_changed.emit()
1377 self.active_event_marker = None
1379 def set_active_event_marker(self, event_marker):
1380 if self.active_event_marker:
1381 self.active_event_marker.active = False
1383 self.active_event_marker = event_marker
1384 event_marker.active = True
1385 event = event_marker.get_event()
1386 self.set_origin(event)
1387 self.active_event_marker_changed.emit()
1389 def set_active_event(self, event):
1390 for marker in self.markers:
1391 if isinstance(marker, EventMarker):
1392 if marker.get_event() is event:
1393 self.set_active_event_marker(marker)
1395 def get_active_event_marker(self):
1396 return self.active_event_marker
1398 def get_active_event(self):
1399 m = self.get_active_event_marker()
1400 if m is not None:
1401 return m.get_event()
1402 else:
1403 return None
1405 def get_active_markers(self):
1406 emarker = self.get_active_event_marker()
1407 if emarker is None:
1408 return None, []
1410 else:
1411 ev = emarker.get_event()
1412 pmarkers = [
1413 m for m in self.markers
1414 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1416 return emarker, pmarkers
1418 def set_origin(self, location):
1419 for station in self.stations.values():
1420 station.set_event_relative_data(
1421 location,
1422 distance_3d=self.menuitem_distances_3d.isChecked())
1424 self.sortingmode_change()
1426 def distances_3d_changed(self):
1427 ignore = self.menuitem_distances_3d.isChecked()
1428 self.set_event_marker_as_origin(ignore)
1430 def iter_snuffling_modules(self):
1431 pjoin = os.path.join
1432 for path in self.snuffling_paths:
1434 if not os.path.isdir(path):
1435 os.mkdir(path)
1437 for entry in os.listdir(path):
1438 directory = path
1439 fn = entry
1440 d = pjoin(path, entry)
1441 if os.path.isdir(d):
1442 directory = d
1443 if os.path.isfile(
1444 os.path.join(directory, 'snuffling.py')):
1445 fn = 'snuffling.py'
1447 if not fn.endswith('.py'):
1448 continue
1450 name = fn[:-3]
1452 if (directory, name) not in self.snuffling_modules:
1453 self.snuffling_modules[directory, name] = \
1454 pyrocko.gui.snuffling.SnufflingModule(
1455 directory, name, self)
1457 yield self.snuffling_modules[directory, name]
1459 def setup_snufflings(self):
1460 # user snufflings
1461 for mod in self.iter_snuffling_modules():
1462 try:
1463 mod.load_if_needed()
1464 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1465 logger.warning('Snuffling module "%s" is broken' % e)
1467 # load the default snufflings on first run
1468 if self.default_snufflings is None:
1469 self.default_snufflings = pyrocko.gui\
1470 .snufflings.__snufflings__()
1471 for snuffling in self.default_snufflings:
1472 self.add_snuffling(snuffling)
1474 def set_panel_parent(self, panel_parent):
1475 self.panel_parent = panel_parent
1477 def get_panel_parent(self):
1478 return self.panel_parent
1480 def add_snuffling(self, snuffling, reloaded=False):
1481 logger.debug('Adding snuffling %s' % snuffling.get_name())
1482 snuffling.init_gui(
1483 self, self.get_panel_parent(), self, reloaded=reloaded)
1484 self.snufflings.append(snuffling)
1485 self.update()
1487 def remove_snuffling(self, snuffling):
1488 snuffling.delete_gui()
1489 self.update()
1490 self.snufflings.remove(snuffling)
1491 snuffling.pre_destroy()
1493 def add_snuffling_menuitem(self, item):
1494 self.snufflings_menu.addAction(item)
1495 item.setParent(self.snufflings_menu)
1496 sort_actions(self.snufflings_menu)
1498 def remove_snuffling_menuitem(self, item):
1499 self.snufflings_menu.removeAction(item)
1501 def add_snuffling_help_menuitem(self, item):
1502 self.snuffling_help.addAction(item)
1503 item.setParent(self.snuffling_help)
1504 sort_actions(self.snuffling_help)
1506 def remove_snuffling_help_menuitem(self, item):
1507 self.snuffling_help.removeAction(item)
1509 def add_panel_toggler(self, item):
1510 self.toggle_panel_menu.addAction(item)
1511 item.setParent(self.toggle_panel_menu)
1512 sort_actions(self.toggle_panel_menu)
1514 def remove_panel_toggler(self, item):
1515 self.toggle_panel_menu.removeAction(item)
1517 def load(self, paths, regex=None, format='detect',
1518 cache_dir=None, force_cache=False):
1520 if cache_dir is None:
1521 cache_dir = pyrocko.config.config().cache_dir
1522 if isinstance(paths, str):
1523 paths = [paths]
1525 fns = pyrocko.util.select_files(
1526 paths, selector=None, include=regex, show_progress=False)
1528 if not fns:
1529 return
1531 cache = pyrocko.pile.get_cache(cache_dir)
1533 t = [time.time()]
1535 def update_bar(label, value):
1536 pbs = self.parent().get_progressbars()
1537 if label.lower() == 'looking at files':
1538 label = 'Looking at %i files' % len(fns)
1539 else:
1540 label = 'Scanning %i files' % len(fns)
1542 return pbs.set_status(label, value)
1544 def update_progress(label, i, n):
1545 abort = False
1547 qw.qApp.processEvents()
1548 if n != 0:
1549 perc = i*100/n
1550 else:
1551 perc = 100
1552 abort |= update_bar(label, perc)
1553 abort |= self.window().is_closing()
1555 tnow = time.time()
1556 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1557 self.update()
1558 t[0] = tnow
1560 return abort
1562 self.automatic_updates = False
1564 self.pile.load_files(
1565 sorted(fns),
1566 filename_attributes=regex,
1567 cache=cache,
1568 fileformat=format,
1569 show_progress=False,
1570 update_progress=update_progress)
1572 self.automatic_updates = True
1573 self.update()
1575 def load_queued(self):
1576 if not self._paths_to_load:
1577 return
1578 paths = self._paths_to_load
1579 self._paths_to_load = []
1580 self.load(paths)
1582 def load_soon(self, paths):
1583 self._paths_to_load.extend(paths)
1584 qc.QTimer.singleShot(200, self.load_queued)
1586 def open_waveforms(self):
1587 caption = 'Select one or more files to open'
1589 fns, _ = qw.QFileDialog.getOpenFileNames(
1590 self, caption, options=qfiledialog_options)
1592 if fns:
1593 self.load(list(str(fn) for fn in fns))
1595 def open_waveform_directory(self):
1596 caption = 'Select directory to scan for waveform files'
1598 dn = qw.QFileDialog.getExistingDirectory(
1599 self, caption, options=qfiledialog_options)
1601 if dn:
1602 self.load([str(dn)])
1604 def open_stations(self, fns=None):
1605 caption = 'Select one or more Pyrocko station files to open'
1607 if not fns:
1608 fns, _ = qw.QFileDialog.getOpenFileNames(
1609 self, caption, options=qfiledialog_options)
1611 try:
1612 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1613 for stat in stations:
1614 self.add_stations(stat)
1616 except Exception as e:
1617 self.fail('Failed to read station file: %s' % str(e))
1619 def open_stations_xml(self, fns=None):
1620 from pyrocko.io import stationxml
1622 caption = 'Select one or more StationXML files'
1623 if not fns:
1624 fns, _ = qw.QFileDialog.getOpenFileNames(
1625 self, caption, options=qfiledialog_options,
1626 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1627 ';;All files (*)')
1629 try:
1630 stations = [
1631 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1632 for x in fns]
1634 for stat in stations:
1635 self.add_stations(stat)
1637 except Exception as e:
1638 self.fail('Failed to read StationXML file: %s' % str(e))
1640 def add_traces(self, traces):
1641 if traces:
1642 mtf = pyrocko.pile.MemTracesFile(None, traces)
1643 self.pile.add_file(mtf)
1644 ticket = (self.pile, mtf)
1645 return ticket
1646 else:
1647 return (None, None)
1649 def release_data(self, tickets):
1650 for ticket in tickets:
1651 pile, mtf = ticket
1652 if pile is not None:
1653 pile.remove_file(mtf)
1655 def periodical(self):
1656 if self.menuitem_watch.isChecked():
1657 if self.pile.reload_modified():
1658 self.update()
1660 def get_pile(self):
1661 return self.pile
1663 def pile_changed(self, what):
1664 self.pile_has_changed = True
1665 self.pile_has_changed_signal.emit()
1666 if self.automatic_updates:
1667 self.update()
1669 def set_gathering(self, gather=None, color=None):
1671 if gather is None:
1672 def gather_func(tr):
1673 return tr.nslc_id
1675 gather = (0, 1, 2, 3)
1677 else:
1678 def gather_func(tr):
1679 return (
1680 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1682 if color is None:
1683 def color(tr):
1684 return tr.location
1686 self.gather = gather_func
1687 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1689 self.color_gather = color
1690 self.color_keys = self.pile.gather_keys(color)
1691 previous_ntracks = self.ntracks
1692 self.set_ntracks(len(keys))
1694 if self.shown_tracks_range is None or \
1695 previous_ntracks == 0 or \
1696 self.show_all:
1698 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1699 key_at_top = None
1700 n = high-low
1702 else:
1703 low, high = self.shown_tracks_range
1704 key_at_top = self.track_keys[low]
1705 n = high-low
1707 self.track_keys = sorted(keys)
1709 track_patterns = []
1710 for k in self.track_keys:
1711 pat = ['*', '*', '*', '*']
1712 for i, j in enumerate(gather):
1713 pat[j] = k[-len(gather)+i]
1715 track_patterns.append(pat)
1717 self.track_patterns = track_patterns
1719 if key_at_top is not None:
1720 try:
1721 ind = self.track_keys.index(key_at_top)
1722 low = ind
1723 high = low+n
1724 except Exception:
1725 pass
1727 self.set_tracks_range((low, high))
1729 self.key_to_row = dict(
1730 [(key, i) for (i, key) in enumerate(self.track_keys)])
1732 def inrange(x, r):
1733 return r[0] <= x and x < r[1]
1735 def trace_selector(trace):
1736 gt = self.gather(trace)
1737 return (
1738 gt in self.key_to_row and
1739 inrange(self.key_to_row[gt], self.shown_tracks_range))
1741 self.trace_selector = lambda x: \
1742 (self.trace_filter is None or self.trace_filter(x)) \
1743 and trace_selector(x)
1745 if self.tmin == working_system_time_range[0] and \
1746 self.tmax == working_system_time_range[1] or \
1747 self.show_all:
1749 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1750 if tmin is not None and tmax is not None:
1751 tlen = (tmax - tmin)
1752 tpad = tlen * 5./self.width()
1753 self.set_time_range(tmin-tpad, tmax+tpad)
1755 def set_time_range(self, tmin, tmax):
1756 if tmin is None:
1757 tmin = initial_time_range[0]
1759 if tmax is None:
1760 tmax = initial_time_range[1]
1762 if tmin > tmax:
1763 tmin, tmax = tmax, tmin
1765 if tmin == tmax:
1766 tmin -= 1.
1767 tmax += 1.
1769 tmin = max(working_system_time_range[0], tmin)
1770 tmax = min(working_system_time_range[1], tmax)
1772 min_deltat = self.content_deltat_range()[0]
1773 if (tmax - tmin < min_deltat):
1774 m = (tmin + tmax) / 2.
1775 tmin = m - min_deltat/2.
1776 tmax = m + min_deltat/2.
1778 self.time_projection.set_in_range(tmin, tmax)
1779 self.tmin, self.tmax = tmin, tmax
1781 def get_time_range(self):
1782 return self.tmin, self.tmax
1784 def ypart(self, y):
1785 if y < self.ax_height:
1786 return -1
1787 elif y > self.height()-self.ax_height:
1788 return 1
1789 else:
1790 return 0
1792 def time_fractional_digits(self):
1793 min_deltat = self.content_deltat_range()[0]
1794 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1796 def write_markers(self, fn=None):
1797 caption = "Choose a file name to write markers"
1798 if not fn:
1799 fn, _ = qw.QFileDialog.getSaveFileName(
1800 self, caption, options=qfiledialog_options)
1801 if fn:
1802 try:
1803 Marker.save_markers(
1804 self.markers, fn,
1805 fdigits=self.time_fractional_digits())
1807 except Exception as e:
1808 self.fail('Failed to write marker file: %s' % str(e))
1810 def write_selected_markers(self, fn=None):
1811 caption = "Choose a file name to write selected markers"
1812 if not fn:
1813 fn, _ = qw.QFileDialog.getSaveFileName(
1814 self, caption, options=qfiledialog_options)
1815 if fn:
1816 try:
1817 Marker.save_markers(
1818 self.iter_selected_markers(),
1819 fn,
1820 fdigits=self.time_fractional_digits())
1822 except Exception as e:
1823 self.fail('Failed to write marker file: %s' % str(e))
1825 def read_events(self, fn=None):
1826 '''
1827 Open QFileDialog to open, read and add
1828 :py:class:`pyrocko.model.Event` instances and their marker
1829 representation to the pile viewer.
1830 '''
1831 caption = "Selet one or more files to open"
1832 if not fn:
1833 fn, _ = qw.QFileDialog.getOpenFileName(
1834 self, caption, options=qfiledialog_options)
1835 if fn:
1836 try:
1837 self.add_events(pyrocko.model.load_events(fn))
1838 self.associate_phases_to_events()
1840 except Exception as e:
1841 self.fail('Failed to read event file: %s' % str(e))
1843 def read_markers(self, fn=None):
1844 '''
1845 Open QFileDialog to open, read and add markers to the pile viewer.
1846 '''
1847 caption = "Selet one or more marker files to open"
1848 if not fn:
1849 fn, _ = qw.QFileDialog.getOpenFileName(
1850 self, caption, options=qfiledialog_options)
1851 if fn:
1852 try:
1853 self.add_markers(Marker.load_markers(fn))
1854 self.associate_phases_to_events()
1856 except Exception as e:
1857 self.fail('Failed to read marker file: %s' % str(e))
1859 def associate_phases_to_events(self):
1860 associate_phases_to_events(self.markers)
1862 def add_marker(self, marker):
1863 # need index to inform QAbstactTableModel about upcoming change,
1864 # but have to restore current state in order to not cause problems
1865 self.markers.insert(marker)
1866 i = self.markers.remove(marker)
1868 self.begin_markers_add.emit(i, i)
1869 self.markers.insert(marker)
1870 self.end_markers_add.emit()
1871 self.markers_deltat_max = max(
1872 self.markers_deltat_max, marker.tmax - marker.tmin)
1874 def add_markers(self, markers):
1875 if not self.markers:
1876 self.begin_markers_add.emit(0, len(markers) - 1)
1877 self.markers.insert_many(markers)
1878 self.end_markers_add.emit()
1879 self.update_markers_deltat_max()
1880 else:
1881 for marker in markers:
1882 self.add_marker(marker)
1884 def update_markers_deltat_max(self):
1885 if self.markers:
1886 self.markers_deltat_max = max(
1887 marker.tmax - marker.tmin for marker in self.markers)
1889 def remove_marker(self, marker):
1890 '''
1891 Remove a ``marker`` from the :py:class:`PileViewer`.
1893 :param marker: :py:class:`Marker` (or subclass) instance
1894 '''
1896 if marker is self.active_event_marker:
1897 self.deactivate_event_marker()
1899 try:
1900 i = self.markers.index(marker)
1901 self.begin_markers_remove.emit(i, i)
1902 self.markers.remove_at(i)
1903 self.end_markers_remove.emit()
1904 except ValueError:
1905 pass
1907 def remove_markers(self, markers):
1908 '''
1909 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1911 :param markers: list of :py:class:`Marker` (or subclass)
1912 instances
1913 '''
1915 if markers is self.markers:
1916 markers = list(markers)
1918 for marker in markers:
1919 self.remove_marker(marker)
1921 self.update_markers_deltat_max()
1923 def remove_selected_markers(self):
1924 def delete_segment(istart, iend):
1925 self.begin_markers_remove.emit(istart, iend-1)
1926 for _ in range(iend - istart):
1927 self.markers.remove_at(istart)
1929 self.end_markers_remove.emit()
1931 istart = None
1932 ipos = 0
1933 markers = self.markers
1934 nmarkers = len(self.markers)
1935 while ipos < nmarkers:
1936 marker = markers[ipos]
1937 if marker.is_selected():
1938 if marker is self.active_event_marker:
1939 self.deactivate_event_marker()
1941 if istart is None:
1942 istart = ipos
1943 else:
1944 if istart is not None:
1945 delete_segment(istart, ipos)
1946 nmarkers -= ipos - istart
1947 ipos = istart - 1
1948 istart = None
1950 ipos += 1
1952 if istart is not None:
1953 delete_segment(istart, ipos)
1955 self.update_markers_deltat_max()
1957 def selected_markers(self):
1958 return [marker for marker in self.markers if marker.is_selected()]
1960 def iter_selected_markers(self):
1961 for marker in self.markers:
1962 if marker.is_selected():
1963 yield marker
1965 def get_markers(self):
1966 return self.markers
1968 def mousePressEvent(self, mouse_ev):
1969 self.show_all = False
1970 point = self.mapFromGlobal(mouse_ev.globalPos())
1972 if mouse_ev.button() == qc.Qt.LeftButton:
1973 marker = self.marker_under_cursor(point.x(), point.y())
1974 if self.picking:
1975 if self.picking_down is None:
1976 self.picking_down = (
1977 self.time_projection.rev(mouse_ev.x()),
1978 mouse_ev.y())
1980 elif marker is not None:
1981 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1982 self.deselect_all()
1983 marker.selected = True
1984 self.emit_selected_markers()
1985 self.update()
1986 else:
1987 self.track_start = mouse_ev.x(), mouse_ev.y()
1988 self.track_trange = self.tmin, self.tmax
1990 if mouse_ev.button() == qc.Qt.RightButton \
1991 and isinstance(self.menu, qw.QMenu):
1992 self.menu.exec_(qg.QCursor.pos())
1993 self.update_status()
1995 def mouseReleaseEvent(self, mouse_ev):
1996 if self.ignore_releases:
1997 self.ignore_releases -= 1
1998 return
2000 if self.picking:
2001 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2002 self.emit_selected_markers()
2004 if self.track_start:
2005 self.update()
2007 self.track_start = None
2008 self.track_trange = None
2009 self.update_status()
2011 def mouseDoubleClickEvent(self, mouse_ev):
2012 self.show_all = False
2013 self.start_picking(None)
2014 self.ignore_releases = 1
2016 def mouseMoveEvent(self, mouse_ev):
2017 point = self.mapFromGlobal(mouse_ev.globalPos())
2019 if self.picking:
2020 self.update_picking(point.x(), point.y())
2022 elif self.track_start is not None:
2023 x0, y0 = self.track_start
2024 dx = (point.x() - x0)/float(self.width())
2025 dy = (point.y() - y0)/float(self.height())
2026 if self.ypart(y0) == 1:
2027 dy = 0
2029 tmin0, tmax0 = self.track_trange
2031 scale = math.exp(-dy*5.)
2032 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2033 frac = x0/float(self.width())
2034 dt = dx*(tmax0-tmin0)*scale
2036 self.interrupt_following()
2037 self.set_time_range(
2038 tmin0 - dt - dtr*frac,
2039 tmax0 - dt + dtr*(1.-frac))
2041 self.update()
2042 else:
2043 self.hoovering(point.x(), point.y())
2045 self.update_status()
2047 def nslc_ids_under_cursor(self, x, y):
2048 ftrack = self.track_to_screen.rev(y)
2049 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2050 return nslc_ids
2052 def marker_under_cursor(self, x, y):
2053 mouset = self.time_projection.rev(x)
2054 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2055 relevant_nslc_ids = None
2056 for marker in self.markers:
2057 if marker.kind not in self.visible_marker_kinds:
2058 continue
2060 if (abs(mouset-marker.tmin) < deltat or
2061 abs(mouset-marker.tmax) < deltat):
2063 if relevant_nslc_ids is None:
2064 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2066 marker_nslc_ids = marker.get_nslc_ids()
2067 if not marker_nslc_ids:
2068 return marker
2070 for nslc_id in marker_nslc_ids:
2071 if nslc_id in relevant_nslc_ids:
2072 return marker
2074 def hoovering(self, x, y):
2075 mouset = self.time_projection.rev(x)
2076 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2077 needupdate = False
2078 haveone = False
2079 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2080 for marker in self.markers:
2081 if marker.kind not in self.visible_marker_kinds:
2082 continue
2084 state = abs(mouset-marker.tmin) < deltat or \
2085 abs(mouset-marker.tmax) < deltat and not haveone
2087 if state:
2088 xstate = False
2090 marker_nslc_ids = marker.get_nslc_ids()
2091 if not marker_nslc_ids:
2092 xstate = True
2094 for nslc in relevant_nslc_ids:
2095 if marker.match_nslc(nslc):
2096 xstate = True
2098 state = xstate
2100 if state:
2101 haveone = True
2102 oldstate = marker.is_alerted()
2103 if oldstate != state:
2104 needupdate = True
2105 marker.set_alerted(state)
2106 if state:
2107 self.message = marker.hoover_message()
2109 if not haveone:
2110 self.message = None
2112 if needupdate:
2113 self.update()
2115 def event(self, event):
2116 if event.type() == qc.QEvent.KeyPress:
2117 self.keyPressEvent(event)
2118 return True
2119 else:
2120 return base.event(self, event)
2122 def keyPressEvent(self, key_event):
2123 self.show_all = False
2124 dt = self.tmax - self.tmin
2125 tmid = (self.tmin + self.tmax) / 2.
2127 key = key_event.key()
2128 try:
2129 keytext = str(key_event.text())
2130 except UnicodeEncodeError:
2131 return
2133 if key == qc.Qt.Key_Space:
2134 self.interrupt_following()
2135 self.set_time_range(self.tmin+dt, self.tmax+dt)
2137 elif key == qc.Qt.Key_Up:
2138 for m in self.selected_markers():
2139 if isinstance(m, PhaseMarker):
2140 if key_event.modifiers() & qc.Qt.ShiftModifier:
2141 p = 0
2142 else:
2143 p = 1 if m.get_polarity() != 1 else None
2144 m.set_polarity(p)
2146 elif key == qc.Qt.Key_Down:
2147 for m in self.selected_markers():
2148 if isinstance(m, PhaseMarker):
2149 if key_event.modifiers() & qc.Qt.ShiftModifier:
2150 p = 0
2151 else:
2152 p = -1 if m.get_polarity() != -1 else None
2153 m.set_polarity(p)
2155 elif key == qc.Qt.Key_B:
2156 dt = self.tmax - self.tmin
2157 self.interrupt_following()
2158 self.set_time_range(self.tmin-dt, self.tmax-dt)
2160 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2161 self.interrupt_following()
2163 tgo = None
2165 class TraceDummy(object):
2166 def __init__(self, marker):
2167 self._marker = marker
2169 @property
2170 def nslc_id(self):
2171 return self._marker.one_nslc()
2173 def marker_to_itrack(marker):
2174 try:
2175 return self.key_to_row.get(
2176 self.gather(TraceDummy(marker)), -1)
2178 except MarkerOneNSLCRequired:
2179 return -1
2181 emarker, pmarkers = self.get_active_markers()
2182 pmarkers = [
2183 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2184 pmarkers.sort(key=lambda m: (
2185 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2187 if key == qc.Qt.Key_Backtab:
2188 pmarkers.reverse()
2190 smarkers = self.selected_markers()
2191 iselected = []
2192 for sm in smarkers:
2193 try:
2194 iselected.append(pmarkers.index(sm))
2195 except ValueError:
2196 pass
2198 if iselected:
2199 icurrent = max(iselected) + 1
2200 else:
2201 icurrent = 0
2203 if icurrent < len(pmarkers):
2204 self.deselect_all()
2205 cmarker = pmarkers[icurrent]
2206 cmarker.selected = True
2207 tgo = cmarker.tmin
2208 if not self.tmin < tgo < self.tmax:
2209 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2211 itrack = marker_to_itrack(cmarker)
2212 if itrack != -1:
2213 if itrack < self.shown_tracks_range[0]:
2214 self.scroll_tracks(
2215 - (self.shown_tracks_range[0] - itrack))
2216 elif self.shown_tracks_range[1] <= itrack:
2217 self.scroll_tracks(
2218 itrack - self.shown_tracks_range[1]+1)
2220 if itrack not in self.track_to_nslc_ids:
2221 self.go_to_selection()
2223 elif keytext in ('p', 'n', 'P', 'N'):
2224 smarkers = self.selected_markers()
2225 tgo = None
2226 dir = str(keytext)
2227 if smarkers:
2228 tmid = smarkers[0].tmin
2229 for smarker in smarkers:
2230 if dir == 'n':
2231 tmid = max(smarker.tmin, tmid)
2232 else:
2233 tmid = min(smarker.tmin, tmid)
2235 tgo = tmid
2237 if dir.lower() == 'n':
2238 for marker in sorted(
2239 self.markers,
2240 key=operator.attrgetter('tmin')):
2242 t = marker.tmin
2243 if t > tmid and \
2244 marker.kind in self.visible_marker_kinds and \
2245 (dir == 'n' or
2246 isinstance(marker, EventMarker)):
2248 self.deselect_all()
2249 marker.selected = True
2250 tgo = t
2251 break
2252 else:
2253 for marker in sorted(
2254 self.markers,
2255 key=operator.attrgetter('tmin'),
2256 reverse=True):
2258 t = marker.tmin
2259 if t < tmid and \
2260 marker.kind in self.visible_marker_kinds and \
2261 (dir == 'p' or
2262 isinstance(marker, EventMarker)):
2263 self.deselect_all()
2264 marker.selected = True
2265 tgo = t
2266 break
2268 if tgo is not None:
2269 self.interrupt_following()
2270 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2272 elif keytext == 'r':
2273 if self.pile.reload_modified():
2274 self.reloaded = True
2276 elif keytext == 'R':
2277 self.setup_snufflings()
2279 elif key == qc.Qt.Key_Backspace:
2280 self.remove_selected_markers()
2282 elif keytext == 'a':
2283 for marker in self.markers:
2284 if ((self.tmin <= marker.tmin <= self.tmax or
2285 self.tmin <= marker.tmax <= self.tmax) and
2286 marker.kind in self.visible_marker_kinds):
2287 marker.selected = True
2288 else:
2289 marker.selected = False
2291 elif keytext == 'A':
2292 for marker in self.markers:
2293 if marker.kind in self.visible_marker_kinds:
2294 marker.selected = True
2296 elif keytext == 'd':
2297 self.deselect_all()
2299 elif keytext == 'E':
2300 self.deactivate_event_marker()
2302 elif keytext == 'e':
2303 markers = self.selected_markers()
2304 event_markers_in_spe = [
2305 marker for marker in markers
2306 if not isinstance(marker, PhaseMarker)]
2308 phase_markers = [
2309 marker for marker in markers
2310 if isinstance(marker, PhaseMarker)]
2312 if len(event_markers_in_spe) == 1:
2313 event_marker = event_markers_in_spe[0]
2314 if not isinstance(event_marker, EventMarker):
2315 nslcs = list(event_marker.nslc_ids)
2316 lat, lon = 0.0, 0.0
2317 old = self.get_active_event()
2318 if len(nslcs) == 1:
2319 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2320 elif old is not None:
2321 lat, lon = old.lat, old.lon
2323 event_marker.convert_to_event_marker(lat, lon)
2325 self.set_active_event_marker(event_marker)
2326 event = event_marker.get_event()
2327 for marker in phase_markers:
2328 marker.set_event(event)
2330 else:
2331 for marker in event_markers_in_spe:
2332 marker.convert_to_event_marker()
2334 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2335 for marker in self.selected_markers():
2336 marker.set_kind(int(keytext))
2337 self.emit_selected_markers()
2339 elif key in fkey_map:
2340 self.handle_fkeys(key)
2342 elif key == qc.Qt.Key_Escape:
2343 if self.picking:
2344 self.stop_picking(0, 0, abort=True)
2346 elif key == qc.Qt.Key_PageDown:
2347 self.scroll_tracks(
2348 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2350 elif key == qc.Qt.Key_PageUp:
2351 self.scroll_tracks(
2352 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2354 elif key == qc.Qt.Key_Plus:
2355 self.zoom_tracks(0., 1.)
2357 elif key == qc.Qt.Key_Minus:
2358 self.zoom_tracks(0., -1.)
2360 elif key == qc.Qt.Key_Equal:
2361 ntracks_shown = self.shown_tracks_range[1] - \
2362 self.shown_tracks_range[0]
2363 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2364 self.zoom_tracks(0., dtracks)
2366 elif key == qc.Qt.Key_Colon:
2367 self.want_input.emit()
2369 elif keytext == 'f':
2370 self.toggle_fullscreen()
2372 elif keytext == 'g':
2373 self.go_to_selection()
2375 elif keytext == 'G':
2376 self.go_to_selection(tight=True)
2378 elif keytext == 'm':
2379 self.toggle_marker_editor()
2381 elif keytext == 'c':
2382 self.toggle_main_controls()
2384 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2385 dir = 1
2386 amount = 1
2387 if key_event.key() == qc.Qt.Key_Left:
2388 dir = -1
2389 if key_event.modifiers() & qc.Qt.ShiftModifier:
2390 amount = 10
2391 self.nudge_selected_markers(dir*amount)
2392 else:
2393 super().keyPressEvent(key_event)
2395 if keytext != '' and keytext in 'degaApPnN':
2396 self.emit_selected_markers()
2398 self.update()
2399 self.update_status()
2401 def handle_fkeys(self, key):
2402 self.set_phase_kind(
2403 self.selected_markers(),
2404 fkey_map[key] + 1)
2405 self.emit_selected_markers()
2407 def emit_selected_markers(self):
2408 ibounds = []
2409 last_selected = False
2410 for imarker, marker in enumerate(self.markers):
2411 this_selected = marker.is_selected()
2412 if this_selected != last_selected:
2413 ibounds.append(imarker)
2415 last_selected = this_selected
2417 if last_selected:
2418 ibounds.append(len(self.markers))
2420 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2421 self.n_selected_markers = sum(
2422 chunk[1] - chunk[0] for chunk in chunks)
2423 self.marker_selection_changed.emit(chunks)
2425 def toggle_marker_editor(self):
2426 self.panel_parent.toggle_marker_editor()
2428 def toggle_main_controls(self):
2429 self.panel_parent.toggle_main_controls()
2431 def nudge_selected_markers(self, npixels):
2432 a, b = self.time_projection.ur
2433 c, d = self.time_projection.xr
2434 for marker in self.selected_markers():
2435 if not isinstance(marker, EventMarker):
2436 marker.tmin += npixels * (d-c)/b
2437 marker.tmax += npixels * (d-c)/b
2439 def toggle_fullscreen(self):
2440 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2441 self.window().windowState() & qc.Qt.WindowMaximized:
2442 self.window().showNormal()
2443 else:
2444 if is_macos:
2445 self.window().showMaximized()
2446 else:
2447 self.window().showFullScreen()
2449 def about(self):
2450 fn = pyrocko.util.data_file('snuffler.png')
2451 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2452 txt = f.read()
2453 label = qw.QLabel(txt % {'logo': fn})
2454 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2455 self.show_doc('About', [label], target='tab')
2457 def help(self):
2458 class MyScrollArea(qw.QScrollArea):
2460 def sizeHint(self):
2461 s = qc.QSize()
2462 s.setWidth(self.widget().sizeHint().width())
2463 s.setHeight(self.widget().sizeHint().height())
2464 return s
2466 with open(pyrocko.util.data_file(
2467 'snuffler_help.html')) as f:
2468 hcheat = qw.QLabel(f.read())
2470 with open(pyrocko.util.data_file(
2471 'snuffler_help_epilog.html')) as f:
2472 hepilog = qw.QLabel(f.read())
2474 for h in [hcheat, hepilog]:
2475 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2476 h.setWordWrap(True)
2478 self.show_doc('Help', [hcheat, hepilog], target='panel')
2480 def show_doc(self, name, labels, target='panel'):
2481 scroller = qw.QScrollArea()
2482 frame = qw.QFrame(scroller)
2483 frame.setLineWidth(0)
2484 layout = qw.QVBoxLayout()
2485 layout.setContentsMargins(0, 0, 0, 0)
2486 layout.setSpacing(0)
2487 frame.setLayout(layout)
2488 scroller.setWidget(frame)
2489 scroller.setWidgetResizable(True)
2490 frame.setBackgroundRole(qg.QPalette.Base)
2491 for h in labels:
2492 h.setParent(frame)
2493 h.setMargin(3)
2494 h.setTextInteractionFlags(
2495 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2496 h.setBackgroundRole(qg.QPalette.Base)
2497 layout.addWidget(h)
2498 h.linkActivated.connect(
2499 self.open_link)
2501 if self.panel_parent is not None:
2502 if target == 'panel':
2503 self.panel_parent.add_panel(
2504 name, scroller, True, volatile=False)
2505 else:
2506 self.panel_parent.add_tab(name, scroller)
2508 def open_link(self, link):
2509 qg.QDesktopServices.openUrl(qc.QUrl(link))
2511 def wheelEvent(self, wheel_event):
2512 self.wheel_pos += wheel_event.angleDelta().y()
2514 n = self.wheel_pos // 120
2515 self.wheel_pos = self.wheel_pos % 120
2516 if n == 0:
2517 return
2519 amount = max(
2520 1.,
2521 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2522 wdelta = amount * n
2524 trmin, trmax = self.track_to_screen.get_in_range()
2525 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2526 / (trmax-trmin)
2528 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2529 self.zoom_tracks(anchor, wdelta)
2530 else:
2531 self.scroll_tracks(-wdelta)
2533 def dragEnterEvent(self, event):
2534 if event.mimeData().hasUrls():
2535 if any(url.toLocalFile() for url in event.mimeData().urls()):
2536 event.setDropAction(qc.Qt.LinkAction)
2537 event.accept()
2539 def dropEvent(self, event):
2540 if event.mimeData().hasUrls():
2541 paths = list(
2542 str(url.toLocalFile()) for url in event.mimeData().urls())
2543 event.acceptProposedAction()
2544 self.load(paths)
2546 def get_phase_name(self, kind):
2547 return self.config.get_phase_name(kind)
2549 def set_phase_kind(self, markers, kind):
2550 phasename = self.get_phase_name(kind)
2552 for marker in markers:
2553 if isinstance(marker, PhaseMarker):
2554 if kind == 10:
2555 marker.convert_to_marker()
2556 else:
2557 marker.set_phasename(phasename)
2558 marker.set_event(self.get_active_event())
2560 elif isinstance(marker, EventMarker):
2561 pass
2563 else:
2564 if kind != 10:
2565 event = self.get_active_event()
2566 marker.convert_to_phase_marker(
2567 event, phasename, None, False)
2569 def set_ntracks(self, ntracks):
2570 if self.ntracks != ntracks:
2571 self.ntracks = ntracks
2572 if self.shown_tracks_range is not None:
2573 l, h = self.shown_tracks_range
2574 else:
2575 l, h = 0, self.ntracks
2577 self.tracks_range_changed.emit(self.ntracks, l, h)
2579 def set_tracks_range(self, range, start=None):
2581 low, high = range
2582 low = min(self.ntracks-1, low)
2583 high = min(self.ntracks, high)
2584 low = max(0, low)
2585 high = max(1, high)
2587 if start is None:
2588 start = float(low)
2590 if self.shown_tracks_range != (low, high):
2591 self.shown_tracks_range = low, high
2592 self.shown_tracks_start = start
2594 self.tracks_range_changed.emit(self.ntracks, low, high)
2596 def scroll_tracks(self, shift):
2597 shown = self.shown_tracks_range
2598 shiftmin = -shown[0]
2599 shiftmax = self.ntracks-shown[1]
2600 shift = max(shiftmin, shift)
2601 shift = min(shiftmax, shift)
2602 shown = shown[0] + shift, shown[1] + shift
2604 self.set_tracks_range((int(shown[0]), int(shown[1])))
2606 self.update()
2608 def zoom_tracks(self, anchor, delta):
2609 ntracks_shown = self.shown_tracks_range[1] \
2610 - self.shown_tracks_range[0]
2612 if (ntracks_shown == 1 and delta <= 0) or \
2613 (ntracks_shown == self.ntracks and delta >= 0):
2614 return
2616 ntracks_shown += int(round(delta))
2617 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2619 u = self.shown_tracks_start
2620 nu = max(0., u-anchor*delta)
2621 nv = nu + ntracks_shown
2622 if nv > self.ntracks:
2623 nu -= nv - self.ntracks
2624 nv -= nv - self.ntracks
2626 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2628 self.ntracks_shown_max = self.shown_tracks_range[1] \
2629 - self.shown_tracks_range[0]
2631 self.update()
2633 def content_time_range(self):
2634 pile = self.get_pile()
2635 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2636 if tmin is None:
2637 tmin = initial_time_range[0]
2638 if tmax is None:
2639 tmax = initial_time_range[1]
2641 return tmin, tmax
2643 def content_deltat_range(self):
2644 pile = self.get_pile()
2646 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2648 if deltatmin is None:
2649 deltatmin = 0.001
2651 if deltatmax is None:
2652 deltatmax = 1000.0
2654 return deltatmin, deltatmax
2656 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2657 if tmax < tmin:
2658 tmin, tmax = tmax, tmin
2660 deltatmin = self.content_deltat_range()[0]
2661 dt = deltatmin * self.visible_length * 0.95
2663 if dt == 0.0:
2664 dt = 1.0
2666 if tight:
2667 if tmax != tmin:
2668 dtm = tmax - tmin
2669 tmin -= dtm*0.1
2670 tmax += dtm*0.1
2671 return tmin, tmax
2672 else:
2673 tcenter = (tmin + tmax) / 2.
2674 tmin = tcenter - 0.5*dt
2675 tmax = tcenter + 0.5*dt
2676 return tmin, tmax
2678 if tmax-tmin < dt:
2679 vmin, vmax = self.get_time_range()
2680 dt = min(vmax - vmin, dt)
2682 tcenter = (tmin+tmax)/2.
2683 etmin, etmax = tmin, tmax
2684 tmin = min(etmin, tcenter - 0.5*dt)
2685 tmax = max(etmax, tcenter + 0.5*dt)
2686 dtm = tmax-tmin
2687 if etmin == tmin:
2688 tmin -= dtm*0.1
2689 if etmax == tmax:
2690 tmax += dtm*0.1
2692 else:
2693 dtm = tmax-tmin
2694 tmin -= dtm*0.1
2695 tmax += dtm*0.1
2697 return tmin, tmax
2699 def go_to_selection(self, tight=False):
2700 markers = self.selected_markers()
2701 if markers:
2702 tmax, tmin = self.content_time_range()
2703 for marker in markers:
2704 tmin = min(tmin, marker.tmin)
2705 tmax = max(tmax, marker.tmax)
2707 else:
2708 if tight:
2709 vmin, vmax = self.get_time_range()
2710 tmin = tmax = (vmin + vmax) / 2.
2711 else:
2712 tmin, tmax = self.content_time_range()
2714 tmin, tmax = self.make_good_looking_time_range(
2715 tmin, tmax, tight=tight)
2717 self.interrupt_following()
2718 self.set_time_range(tmin, tmax)
2719 self.update()
2721 def go_to_time(self, t, tlen=None):
2722 tmax = t
2723 if tlen is not None:
2724 tmax = t+tlen
2725 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2726 self.interrupt_following()
2727 self.set_time_range(tmin, tmax)
2728 self.update()
2730 def go_to_event_by_name(self, name):
2731 for marker in self.markers:
2732 if isinstance(marker, EventMarker):
2733 event = marker.get_event()
2734 if event.name and event.name.lower() == name.lower():
2735 tmin, tmax = self.make_good_looking_time_range(
2736 event.time, event.time)
2738 self.interrupt_following()
2739 self.set_time_range(tmin, tmax)
2741 def printit(self):
2742 from .qt_compat import qprint
2743 printer = qprint.QPrinter()
2744 printer.setOrientation(qprint.QPrinter.Landscape)
2746 dialog = qprint.QPrintDialog(printer, self)
2747 dialog.setWindowTitle('Print')
2749 if dialog.exec_() != qw.QDialog.Accepted:
2750 return
2752 painter = qg.QPainter()
2753 painter.begin(printer)
2754 page = printer.pageRect()
2755 self.drawit(
2756 painter, printmode=False, w=page.width(), h=page.height())
2758 painter.end()
2760 def savesvg(self, fn=None):
2762 if not fn:
2763 fn, _ = qw.QFileDialog.getSaveFileName(
2764 self,
2765 'Save as SVG|PNG',
2766 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2767 'SVG|PNG (*.svg *.png)',
2768 options=qfiledialog_options)
2770 if fn == '':
2771 return
2773 fn = str(fn)
2775 if fn.lower().endswith('.svg'):
2776 try:
2777 w, h = 842, 595
2778 margin = 0.025
2779 m = max(w, h)*margin
2781 generator = qsvg.QSvgGenerator()
2782 generator.setFileName(fn)
2783 generator.setSize(qc.QSize(w, h))
2784 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2786 painter = qg.QPainter()
2787 painter.begin(generator)
2788 self.drawit(painter, printmode=False, w=w, h=h)
2789 painter.end()
2791 except Exception as e:
2792 self.fail('Failed to write SVG file: %s' % str(e))
2794 elif fn.lower().endswith('.png'):
2795 pixmap = self.grab()
2797 try:
2798 pixmap.save(fn)
2800 except Exception as e:
2801 self.fail('Failed to write PNG file: %s' % str(e))
2803 else:
2804 self.fail(
2805 'Unsupported file type: filename must end with ".svg" or '
2806 '".png".')
2808 def paintEvent(self, paint_ev):
2809 '''
2810 Called by QT whenever widget needs to be painted.
2811 '''
2812 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2813 # was called twice (by different threads?), causing segfaults.
2814 if self.in_paint_event:
2815 logger.warning('Blocking reentrant call to paintEvent().')
2816 return
2818 self.in_paint_event = True
2820 painter = qg.QPainter(self)
2822 if self.menuitem_antialias.isChecked():
2823 painter.setRenderHint(qg.QPainter.Antialiasing)
2825 self.drawit(painter)
2827 logger.debug(
2828 'Time spent drawing: '
2829 ' user:%.3f sys:%.3f children_user:%.3f'
2830 ' childred_sys:%.3f elapsed:%.3f' %
2831 (self.timer_draw - self.timer_cutout))
2833 logger.debug(
2834 'Time spent processing:'
2835 ' user:%.3f sys:%.3f children_user:%.3f'
2836 ' childred_sys:%.3f elapsed:%.3f' %
2837 self.timer_cutout.get())
2839 self.time_spent_painting = self.timer_draw.get()[-1]
2840 self.time_last_painted = time.time()
2841 self.in_paint_event = False
2843 def determine_box_styles(self):
2845 traces = list(self.pile.iter_traces())
2846 traces.sort(key=operator.attrgetter('full_id'))
2847 istyle = 0
2848 trace_styles = {}
2849 for itr, tr in enumerate(traces):
2850 if itr > 0:
2851 other = traces[itr-1]
2852 if not (
2853 other.nslc_id == tr.nslc_id
2854 and other.deltat == tr.deltat
2855 and abs(other.tmax - tr.tmin)
2856 < gap_lap_tolerance*tr.deltat):
2858 istyle += 1
2860 trace_styles[tr.full_id, tr.deltat] = istyle
2862 self.trace_styles = trace_styles
2864 def draw_trace_boxes(self, p, time_projection, track_projections):
2866 for v_projection in track_projections.values():
2867 v_projection.set_in_range(0., 1.)
2869 def selector(x):
2870 return x.overlaps(*time_projection.get_in_range())
2872 if self.trace_filter is not None:
2873 def tselector(x):
2874 return selector(x) and self.trace_filter(x)
2876 else:
2877 tselector = selector
2879 traces = list(self.pile.iter_traces(
2880 group_selector=selector, trace_selector=tselector))
2882 traces.sort(key=operator.attrgetter('full_id'))
2884 def drawbox(itrack, istyle, traces):
2885 v_projection = track_projections[itrack]
2886 dvmin = v_projection(0.)
2887 dvmax = v_projection(1.)
2888 dtmin = time_projection.clipped(traces[0].tmin, 0)
2889 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2891 style = box_styles[istyle % len(box_styles)]
2892 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2893 p.fillRect(rect, style.fill_brush)
2894 p.setPen(style.frame_pen)
2895 p.drawRect(rect)
2897 traces_by_style = {}
2898 for itr, tr in enumerate(traces):
2899 gt = self.gather(tr)
2900 if gt not in self.key_to_row:
2901 continue
2903 itrack = self.key_to_row[gt]
2904 if itrack not in track_projections:
2905 continue
2907 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2909 if len(traces) < 500:
2910 drawbox(itrack, istyle, [tr])
2911 else:
2912 if (itrack, istyle) not in traces_by_style:
2913 traces_by_style[itrack, istyle] = []
2914 traces_by_style[itrack, istyle].append(tr)
2916 for (itrack, istyle), traces in traces_by_style.items():
2917 drawbox(itrack, istyle, traces)
2919 def draw_visible_markers(
2920 self, p, vcenter_projection, primary_pen):
2922 try:
2923 markers = self.markers.with_key_in_limited(
2924 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2926 except pyrocko.pile.TooMany:
2927 tmin = self.markers[0].tmin
2928 tmax = self.markers[-1].tmax
2929 umin_view, umax_view = self.time_projection.get_out_range()
2930 umin = max(umin_view, self.time_projection(tmin))
2931 umax = min(umax_view, self.time_projection(tmax))
2932 v0, _ = vcenter_projection.get_out_range()
2933 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2935 p.save()
2937 pen = qg.QPen(primary_pen)
2938 pen.setWidth(2)
2939 pen.setStyle(qc.Qt.DotLine)
2940 # pat = [5., 3.]
2941 # pen.setDashPattern(pat)
2942 p.setPen(pen)
2944 if self.n_selected_markers == len(self.markers):
2945 s_selected = ' (all selected)'
2946 elif self.n_selected_markers > 0:
2947 s_selected = ' (%i selected)' % self.n_selected_markers
2948 else:
2949 s_selected = ''
2951 draw_label(
2952 p, umin+10., v0-10.,
2953 '%i Markers' % len(self.markers) + s_selected,
2954 label_bg, 'LB')
2956 line = qc.QLineF(umin, v0, umax, v0)
2957 p.drawLine(line)
2958 p.restore()
2960 return
2962 for marker in markers:
2963 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2964 and marker.kind in self.visible_marker_kinds:
2966 marker.draw(
2967 p, self.time_projection, vcenter_projection,
2968 with_label=True)
2970 def get_squirrel(self):
2971 try:
2972 return self.pile._squirrel
2973 except AttributeError:
2974 return None
2976 def draw_coverage(self, p, time_projection, track_projections):
2977 sq = self.get_squirrel()
2978 if sq is None:
2979 return
2981 def drawbox(itrack, tmin, tmax, style):
2982 v_projection = track_projections[itrack]
2983 dvmin = v_projection(0.)
2984 dvmax = v_projection(1.)
2985 dtmin = time_projection.clipped(tmin, 0)
2986 dtmax = time_projection.clipped(tmax, 1)
2988 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2989 p.fillRect(rect, style.fill_brush)
2990 p.setPen(style.frame_pen)
2991 p.drawRect(rect)
2993 pattern_list = []
2994 pattern_to_itrack = {}
2995 for key in self.track_keys:
2996 itrack = self.key_to_row[key]
2997 if itrack not in track_projections:
2998 continue
3000 pattern = self.track_patterns[itrack]
3001 pattern_to_itrack[tuple(pattern)] = itrack
3002 pattern_list.append(tuple(pattern))
3004 vmin, vmax = self.get_time_range()
3006 for kind in ['waveform', 'waveform_promise']:
3007 for coverage in sq.get_coverage(
3008 kind, vmin, vmax, pattern_list, limit=500):
3009 itrack = pattern_to_itrack[coverage.pattern.nslc]
3011 if coverage.changes is None:
3012 drawbox(
3013 itrack, coverage.tmin, coverage.tmax,
3014 box_styles_coverage[kind][0])
3015 else:
3016 t = None
3017 pcount = 0
3018 for tb, count in coverage.changes:
3019 if t is not None and tb > t:
3020 if pcount > 0:
3021 drawbox(
3022 itrack, t, tb,
3023 box_styles_coverage[kind][
3024 min(len(box_styles_coverage)-1,
3025 pcount)])
3027 t = tb
3028 pcount = count
3030 def drawit(self, p, printmode=False, w=None, h=None):
3031 '''
3032 This performs the actual drawing.
3033 '''
3035 self.timer_draw.start()
3036 show_boxes = self.menuitem_showboxes.isChecked()
3037 sq = self.get_squirrel()
3039 if self.gather is None:
3040 self.set_gathering()
3042 if self.pile_has_changed:
3044 if not self.sortingmode_change_delayed():
3045 self.sortingmode_change()
3047 if show_boxes and sq is None:
3048 self.determine_box_styles()
3050 self.pile_has_changed = False
3052 if h is None:
3053 h = float(self.height())
3054 if w is None:
3055 w = float(self.width())
3057 if printmode:
3058 primary_color = (0, 0, 0)
3059 else:
3060 primary_color = pyrocko.plot.tango_colors['aluminium5']
3062 primary_pen = qg.QPen(qg.QColor(*primary_color))
3064 ax_h = float(self.ax_height)
3066 vbottom_ax_projection = Projection()
3067 vtop_ax_projection = Projection()
3068 vcenter_projection = Projection()
3070 self.time_projection.set_out_range(0., w)
3071 vbottom_ax_projection.set_out_range(h-ax_h, h)
3072 vtop_ax_projection.set_out_range(0., ax_h)
3073 vcenter_projection.set_out_range(ax_h, h-ax_h)
3074 vcenter_projection.set_in_range(0., 1.)
3075 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3077 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3078 track_projections = {}
3079 for i in range(*self.shown_tracks_range):
3080 proj = Projection()
3081 proj.set_out_range(
3082 self.track_to_screen(i+0.05),
3083 self.track_to_screen(i+1.-0.05))
3085 track_projections[i] = proj
3087 if self.tmin > self.tmax:
3088 return
3090 self.time_projection.set_in_range(self.tmin, self.tmax)
3091 vbottom_ax_projection.set_in_range(0, ax_h)
3093 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3095 yscaler = pyrocko.plot.AutoScaler()
3097 p.setPen(primary_pen)
3099 font = qg.QFont()
3100 font.setBold(True)
3102 axannotfont = qg.QFont()
3103 axannotfont.setBold(True)
3104 axannotfont.setPointSize(8)
3106 processed_traces = self.prepare_cutout2(
3107 self.tmin, self.tmax,
3108 trace_selector=self.trace_selector,
3109 degap=self.menuitem_degap.isChecked(),
3110 demean=self.menuitem_demean.isChecked())
3112 if not printmode and show_boxes:
3113 if (self.view_mode is ViewMode.Wiggle) \
3114 or (self.view_mode is ViewMode.Waterfall
3115 and not processed_traces):
3117 if sq is None:
3118 self.draw_trace_boxes(
3119 p, self.time_projection, track_projections)
3121 else:
3122 self.draw_coverage(
3123 p, self.time_projection, track_projections)
3125 p.setFont(font)
3126 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3128 color_lookup = dict(
3129 [(k, i) for (i, k) in enumerate(self.color_keys)])
3131 self.track_to_nslc_ids = {}
3132 nticks = 0
3133 annot_labels = []
3135 if self.view_mode is ViewMode.Waterfall and processed_traces:
3136 waterfall = self.waterfall
3137 waterfall.set_time_range(self.tmin, self.tmax)
3138 waterfall.set_traces(processed_traces)
3139 waterfall.set_cmap(self.waterfall_cmap)
3140 waterfall.set_integrate(self.waterfall_integrate)
3141 waterfall.set_clip(
3142 self.waterfall_clip_min, self.waterfall_clip_max)
3143 waterfall.show_absolute_values(
3144 self.waterfall_show_absolute)
3146 rect = qc.QRectF(
3147 0, self.ax_height,
3148 self.width(), self.height() - self.ax_height*2
3149 )
3150 waterfall.draw_waterfall(p, rect=rect)
3152 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3153 show_scales = self.menuitem_showscalerange.isChecked() \
3154 or self.menuitem_showscaleaxis.isChecked()
3156 fm = qg.QFontMetrics(axannotfont, p.device())
3157 trackheight = self.track_to_screen(1.-0.05) \
3158 - self.track_to_screen(0.05)
3160 nlinesavail = trackheight/float(fm.lineSpacing())
3162 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3163 if self.menuitem_showscaleaxis.isChecked() \
3164 else 15
3166 yscaler = pyrocko.plot.AutoScaler(
3167 no_exp_interval=(-3, 2), approx_ticks=nticks,
3168 snap=show_scales
3169 and not self.menuitem_showscaleaxis.isChecked())
3171 data_ranges = pyrocko.trace.minmax(
3172 processed_traces,
3173 key=self.scaling_key,
3174 mode=self.scaling_base[0],
3175 outer_mode=self.scaling_base[1])
3177 if not self.menuitem_fixscalerange.isChecked():
3178 self.old_data_ranges = data_ranges
3179 else:
3180 data_ranges.update(self.old_data_ranges)
3182 self.apply_scaling_hooks(data_ranges)
3184 trace_to_itrack = {}
3185 track_scaling_keys = {}
3186 track_scaling_colors = {}
3187 for trace in processed_traces:
3188 gt = self.gather(trace)
3189 if gt not in self.key_to_row:
3190 continue
3192 itrack = self.key_to_row[gt]
3193 if itrack not in track_projections:
3194 continue
3196 trace_to_itrack[trace] = itrack
3198 if itrack not in self.track_to_nslc_ids:
3199 self.track_to_nslc_ids[itrack] = set()
3201 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3203 if itrack not in track_scaling_keys:
3204 track_scaling_keys[itrack] = set()
3206 scaling_key = self.scaling_key(trace)
3207 track_scaling_keys[itrack].add(scaling_key)
3209 color = pyrocko.plot.color(
3210 color_lookup[self.color_gather(trace)])
3212 k = itrack, scaling_key
3213 if k not in track_scaling_colors \
3214 and self.menuitem_colortraces.isChecked():
3215 track_scaling_colors[k] = color
3216 else:
3217 track_scaling_colors[k] = primary_color
3219 # y axes, zero lines
3220 trace_projections = {}
3221 for itrack in list(track_projections.keys()):
3222 if itrack not in track_scaling_keys:
3223 continue
3224 uoff = 0
3225 for scaling_key in track_scaling_keys[itrack]:
3226 data_range = data_ranges[scaling_key]
3227 dymin, dymax = data_range
3228 ymin, ymax, yinc = yscaler.make_scale(
3229 (dymin/self.gain, dymax/self.gain))
3230 iexp = yscaler.make_exp(yinc)
3231 factor = 10**iexp
3232 trace_projection = track_projections[itrack].copy()
3233 trace_projection.set_in_range(ymax, ymin)
3234 trace_projections[itrack, scaling_key] = \
3235 trace_projection
3236 umin, umax = self.time_projection.get_out_range()
3237 vmin, vmax = trace_projection.get_out_range()
3238 umax_zeroline = umax
3239 uoffnext = uoff
3241 if show_scales:
3242 pen = qg.QPen(primary_pen)
3243 k = itrack, scaling_key
3244 if k in track_scaling_colors:
3245 c = qg.QColor(*track_scaling_colors[
3246 itrack, scaling_key])
3248 pen.setColor(c)
3250 p.setPen(pen)
3251 if nlinesavail > 3:
3252 if self.menuitem_showscaleaxis.isChecked():
3253 ymin_annot = math.ceil(ymin/yinc)*yinc
3254 ny_annot = int(
3255 math.floor(ymax/yinc)
3256 - math.ceil(ymin/yinc)) + 1
3258 for iy_annot in range(ny_annot):
3259 y = ymin_annot + iy_annot*yinc
3260 v = trace_projection(y)
3261 line = qc.QLineF(
3262 umax-10-uoff, v, umax-uoff, v)
3264 p.drawLine(line)
3265 if iy_annot == ny_annot - 1 \
3266 and iexp != 0:
3267 sexp = ' × ' \
3268 '10<sup>%i</sup>' % iexp
3269 else:
3270 sexp = ''
3272 snum = num_to_html(y/factor)
3273 lab = Label(
3274 p,
3275 umax-20-uoff,
3276 v, '%s%s' % (snum, sexp),
3277 label_bg=None,
3278 anchor='MR',
3279 font=axannotfont,
3280 color=c)
3282 uoffnext = max(
3283 lab.rect.width()+30., uoffnext)
3285 annot_labels.append(lab)
3286 if y == 0.:
3287 umax_zeroline = \
3288 umax - 20 \
3289 - lab.rect.width() - 10 \
3290 - uoff
3291 else:
3292 if not show_boxes:
3293 qpoints = make_QPolygonF(
3294 [umax-20-uoff,
3295 umax-10-uoff,
3296 umax-10-uoff,
3297 umax-20-uoff],
3298 [vmax, vmax, vmin, vmin])
3299 p.drawPolyline(qpoints)
3301 snum = num_to_html(ymin)
3302 labmin = Label(
3303 p, umax-15-uoff, vmax, snum,
3304 label_bg=None,
3305 anchor='BR',
3306 font=axannotfont,
3307 color=c)
3309 annot_labels.append(labmin)
3310 snum = num_to_html(ymax)
3311 labmax = Label(
3312 p, umax-15-uoff, vmin, snum,
3313 label_bg=None,
3314 anchor='TR',
3315 font=axannotfont,
3316 color=c)
3318 annot_labels.append(labmax)
3320 for lab in (labmin, labmax):
3321 uoffnext = max(
3322 lab.rect.width()+10., uoffnext)
3324 if self.menuitem_showzeroline.isChecked():
3325 v = trace_projection(0.)
3326 if vmin <= v <= vmax:
3327 line = qc.QLineF(umin, v, umax_zeroline, v)
3328 p.drawLine(line)
3330 uoff = uoffnext
3332 p.setFont(font)
3333 p.setPen(primary_pen)
3334 for trace in processed_traces:
3335 if self.view_mode is not ViewMode.Wiggle:
3336 break
3338 if trace not in trace_to_itrack:
3339 continue
3341 itrack = trace_to_itrack[trace]
3342 scaling_key = self.scaling_key(trace)
3343 trace_projection = trace_projections[
3344 itrack, scaling_key]
3346 vdata = trace_projection(trace.get_ydata())
3348 udata_min = float(self.time_projection(trace.tmin))
3349 udata_max = float(self.time_projection(
3350 trace.tmin+trace.deltat*(vdata.size-1)))
3351 udata = num.linspace(udata_min, udata_max, vdata.size)
3353 qpoints = make_QPolygonF(udata, vdata)
3355 umin, umax = self.time_projection.get_out_range()
3356 vmin, vmax = trace_projection.get_out_range()
3358 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3360 if self.menuitem_cliptraces.isChecked():
3361 p.setClipRect(trackrect)
3363 if self.menuitem_colortraces.isChecked():
3364 color = pyrocko.plot.color(
3365 color_lookup[self.color_gather(trace)])
3366 pen = qg.QPen(qg.QColor(*color), 1)
3367 p.setPen(pen)
3369 p.drawPolyline(qpoints)
3371 if self.floating_marker:
3372 self.floating_marker.draw_trace(
3373 self, p, trace,
3374 self.time_projection, trace_projection, 1.0)
3376 for marker in self.markers.with_key_in(
3377 self.tmin - self.markers_deltat_max,
3378 self.tmax):
3380 if marker.tmin < self.tmax \
3381 and self.tmin < marker.tmax \
3382 and marker.kind \
3383 in self.visible_marker_kinds:
3384 marker.draw_trace(
3385 self, p, trace, self.time_projection,
3386 trace_projection, 1.0)
3388 p.setPen(primary_pen)
3390 if self.menuitem_cliptraces.isChecked():
3391 p.setClipRect(0, 0, int(w), int(h))
3393 if self.floating_marker:
3394 self.floating_marker.draw(
3395 p, self.time_projection, vcenter_projection)
3397 self.draw_visible_markers(
3398 p, vcenter_projection, primary_pen)
3400 p.setPen(primary_pen)
3401 while font.pointSize() > 2:
3402 fm = qg.QFontMetrics(font, p.device())
3403 trackheight = self.track_to_screen(1.-0.05) \
3404 - self.track_to_screen(0.05)
3405 nlinesavail = trackheight/float(fm.lineSpacing())
3406 if nlinesavail > 1:
3407 break
3409 font.setPointSize(font.pointSize()-1)
3411 p.setFont(font)
3412 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3414 for key in self.track_keys:
3415 itrack = self.key_to_row[key]
3416 if itrack in track_projections:
3417 plabel = ' '.join(
3418 [str(x) for x in key if x is not None])
3419 lx = 10
3420 ly = self.track_to_screen(itrack+0.5)
3422 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3423 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3424 continue
3426 contains_cursor = \
3427 self.track_to_screen(itrack) \
3428 < mouse_pos.y() \
3429 < self.track_to_screen(itrack+1)
3431 if not contains_cursor:
3432 continue
3434 font_large = p.font()
3435 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3436 p.setFont(font_large)
3437 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3438 p.setFont(font)
3440 for lab in annot_labels:
3441 lab.draw()
3443 self.timer_draw.stop()
3445 def see_data_params(self):
3447 min_deltat = self.content_deltat_range()[0]
3449 # determine padding and downampling requirements
3450 if self.lowpass is not None:
3451 deltat_target = 1./self.lowpass * 0.25
3452 ndecimate = min(
3453 50,
3454 max(1, int(round(deltat_target / min_deltat))))
3455 tpad = 1./self.lowpass * 2.
3456 else:
3457 ndecimate = 1
3458 tpad = min_deltat*5.
3460 if self.highpass is not None:
3461 tpad = max(1./self.highpass * 2., tpad)
3463 nsee_points_per_trace = 5000*10
3464 tsee = ndecimate*nsee_points_per_trace*min_deltat
3466 return ndecimate, tpad, tsee
3468 def clean_update(self):
3469 self.cached_processed_traces = None
3470 self.update()
3472 def get_adequate_tpad(self):
3473 tpad = 0.
3474 for f in [self.highpass, self.lowpass]:
3475 if f is not None:
3476 tpad = max(tpad, 1.0/f)
3478 for snuffling in self.snufflings:
3479 if snuffling._post_process_hook_enabled \
3480 or snuffling._pre_process_hook_enabled:
3482 tpad = max(tpad, snuffling.get_tpad())
3484 return tpad
3486 def prepare_cutout2(
3487 self, tmin, tmax, trace_selector=None, degap=True,
3488 demean=True, nmax=6000):
3490 if self.pile.is_empty():
3491 return []
3493 nmax = self.visible_length
3495 self.timer_cutout.start()
3497 tsee = tmax-tmin
3498 min_deltat_wo_decimate = tsee/nmax
3499 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3501 min_deltat_allow = min_deltat_wo_decimate
3502 if self.lowpass is not None:
3503 target_deltat_lp = 0.25/self.lowpass
3504 if target_deltat_lp > min_deltat_wo_decimate:
3505 min_deltat_allow = min_deltat_w_decimate
3507 min_deltat_allow = math.exp(
3508 int(math.floor(math.log(min_deltat_allow))))
3510 tmin_ = tmin
3511 tmax_ = tmax
3513 # fetch more than needed?
3514 if self.menuitem_liberal_fetch.isChecked():
3515 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3516 tmin = math.floor(tmin/tlen) * tlen
3517 tmax = math.ceil(tmax/tlen) * tlen
3519 fft_filtering = self.menuitem_fft_filtering.isChecked()
3520 lphp = self.menuitem_lphp.isChecked()
3521 ads = self.menuitem_allowdownsampling.isChecked()
3523 tpad = self.get_adequate_tpad()
3524 tpad = max(tpad, tsee)
3526 # state vector to decide if cached traces can be used
3527 vec = (
3528 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3529 self.highpass, fft_filtering, lphp,
3530 min_deltat_allow, self.rotate, self.shown_tracks_range,
3531 ads, self.pile.get_update_count())
3533 if (self.cached_vec
3534 and self.cached_vec[0] <= vec[0]
3535 and vec[1] <= self.cached_vec[1]
3536 and vec[2:] == self.cached_vec[2:]
3537 and not (self.reloaded or self.menuitem_watch.isChecked())
3538 and self.cached_processed_traces is not None):
3540 logger.debug('Using cached traces')
3541 processed_traces = self.cached_processed_traces
3543 else:
3544 processed_traces = []
3545 if self.pile.deltatmax >= min_deltat_allow:
3547 if isinstance(self.pile, pyrocko.pile.Pile):
3548 def group_selector(gr):
3549 return gr.deltatmax >= min_deltat_allow
3551 kwargs = dict(group_selector=group_selector)
3552 else:
3553 kwargs = {}
3555 if trace_selector is not None:
3556 def trace_selectorx(tr):
3557 return tr.deltat >= min_deltat_allow \
3558 and trace_selector(tr)
3559 else:
3560 def trace_selectorx(tr):
3561 return tr.deltat >= min_deltat_allow
3563 for traces in self.pile.chopper(
3564 tmin=tmin, tmax=tmax, tpad=tpad,
3565 want_incomplete=True,
3566 degap=degap,
3567 maxgap=gap_lap_tolerance,
3568 maxlap=gap_lap_tolerance,
3569 keep_current_files_open=True,
3570 trace_selector=trace_selectorx,
3571 accessor_id=id(self),
3572 snap=(math.floor, math.ceil),
3573 include_last=True, **kwargs):
3575 if demean:
3576 for tr in traces:
3577 if (tr.meta and tr.meta.get('tabu', False)):
3578 continue
3579 y = tr.get_ydata()
3580 tr.set_ydata(y - num.mean(y))
3582 traces = self.pre_process_hooks(traces)
3584 for trace in traces:
3586 if not (trace.meta
3587 and trace.meta.get('tabu', False)):
3589 if fft_filtering:
3590 but = pyrocko.response.ButterworthResponse
3591 multres = pyrocko.response.MultiplyResponse
3592 if self.lowpass is not None \
3593 or self.highpass is not None:
3595 it = num.arange(
3596 trace.data_len(), dtype=float)
3597 detr_data, m, b = detrend(
3598 it, trace.get_ydata())
3600 trace.set_ydata(detr_data)
3602 freqs, fdata = trace.spectrum(
3603 pad_to_pow2=True, tfade=None)
3605 nfreqs = fdata.size
3607 key = (trace.deltat, nfreqs)
3609 if key not in self.tf_cache:
3610 resps = []
3611 if self.lowpass is not None:
3612 resps.append(but(
3613 order=4,
3614 corner=self.lowpass,
3615 type='low'))
3617 if self.highpass is not None:
3618 resps.append(but(
3619 order=4,
3620 corner=self.highpass,
3621 type='high'))
3623 resp = multres(resps)
3624 self.tf_cache[key] = \
3625 resp.evaluate(freqs)
3627 filtered_data = num.fft.irfft(
3628 fdata*self.tf_cache[key]
3629 )[:trace.data_len()]
3631 retrended_data = retrend(
3632 it, filtered_data, m, b)
3634 trace.set_ydata(retrended_data)
3636 else:
3638 if ads and self.lowpass is not None:
3639 while trace.deltat \
3640 < min_deltat_wo_decimate:
3642 trace.downsample(2, demean=False)
3644 fmax = 0.5/trace.deltat
3645 if not lphp and (
3646 self.lowpass is not None
3647 and self.highpass is not None
3648 and self.lowpass < fmax
3649 and self.highpass < fmax
3650 and self.highpass < self.lowpass):
3652 trace.bandpass(
3653 2, self.highpass, self.lowpass)
3654 else:
3655 if self.lowpass is not None:
3656 if self.lowpass < 0.5/trace.deltat:
3657 trace.lowpass(
3658 4, self.lowpass,
3659 demean=False)
3661 if self.highpass is not None:
3662 if self.lowpass is None \
3663 or self.highpass \
3664 < self.lowpass:
3666 if self.highpass < \
3667 0.5/trace.deltat:
3668 trace.highpass(
3669 4, self.highpass,
3670 demean=False)
3672 processed_traces.append(trace)
3674 if self.rotate != 0.0:
3675 phi = self.rotate/180.*math.pi
3676 cphi = math.cos(phi)
3677 sphi = math.sin(phi)
3678 for a in processed_traces:
3679 for b in processed_traces:
3680 if (a.network == b.network
3681 and a.station == b.station
3682 and a.location == b.location
3683 and ((a.channel.lower().endswith('n')
3684 and b.channel.lower().endswith('e'))
3685 or (a.channel.endswith('1')
3686 and b.channel.endswith('2')))
3687 and abs(a.deltat-b.deltat) < a.deltat*0.001
3688 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3689 len(a.get_ydata()) == len(b.get_ydata())):
3691 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3692 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3693 a.set_ydata(aydata)
3694 b.set_ydata(bydata)
3696 processed_traces = self.post_process_hooks(processed_traces)
3698 self.cached_processed_traces = processed_traces
3699 self.cached_vec = vec
3701 chopped_traces = []
3702 for trace in processed_traces:
3703 chop_tmin = tmin_ - trace.deltat*4
3704 chop_tmax = tmax_ + trace.deltat*4
3706 try:
3707 ctrace = trace.chop(
3708 chop_tmin, chop_tmax,
3709 inplace=False)
3711 except pyrocko.trace.NoData:
3712 continue
3714 if ctrace.data_len() < 2:
3715 continue
3717 chopped_traces.append(ctrace)
3719 self.timer_cutout.stop()
3720 return chopped_traces
3722 def pre_process_hooks(self, traces):
3723 for snuffling in self.snufflings:
3724 if snuffling._pre_process_hook_enabled:
3725 traces = snuffling.pre_process_hook(traces)
3727 return traces
3729 def post_process_hooks(self, traces):
3730 for snuffling in self.snufflings:
3731 if snuffling._post_process_hook_enabled:
3732 traces = snuffling.post_process_hook(traces)
3734 return traces
3736 def visible_length_change(self, ignore=None):
3737 for menuitem, vlen in self.menuitems_visible_length:
3738 if menuitem.isChecked():
3739 self.visible_length = vlen
3741 def scaling_base_change(self, ignore=None):
3742 for menuitem, scaling_base in self.menuitems_scaling_base:
3743 if menuitem.isChecked():
3744 self.scaling_base = scaling_base
3746 def scalingmode_change(self, ignore=None):
3747 for menuitem, scaling_key in self.menuitems_scaling:
3748 if menuitem.isChecked():
3749 self.scaling_key = scaling_key
3750 self.update()
3752 def apply_scaling_hooks(self, data_ranges):
3753 for k in sorted(self.scaling_hooks.keys()):
3754 hook = self.scaling_hooks[k]
3755 hook(data_ranges)
3757 def viewmode_change(self, ignore=True):
3758 for item, mode in self.menuitems_viewmode:
3759 if item.isChecked():
3760 self.view_mode = mode
3761 break
3762 else:
3763 raise AttributeError('unknown view mode')
3765 items_waterfall_disabled = (
3766 self.menuitem_showscaleaxis,
3767 self.menuitem_showscalerange,
3768 self.menuitem_showzeroline,
3769 self.menuitem_colortraces,
3770 self.menuitem_cliptraces,
3771 *(itm[0] for itm in self.menuitems_visible_length)
3772 )
3774 if self.view_mode is ViewMode.Waterfall:
3775 self.parent().show_colorbar_ctrl(True)
3776 self.parent().show_gain_ctrl(False)
3778 for item in items_waterfall_disabled:
3779 item.setDisabled(True)
3781 self.visible_length = 180.
3782 else:
3783 self.parent().show_colorbar_ctrl(False)
3784 self.parent().show_gain_ctrl(True)
3786 for item in items_waterfall_disabled:
3787 item.setDisabled(False)
3789 self.visible_length_change()
3790 self.update()
3792 def set_scaling_hook(self, k, hook):
3793 self.scaling_hooks[k] = hook
3795 def remove_scaling_hook(self, k):
3796 del self.scaling_hooks[k]
3798 def remove_scaling_hooks(self):
3799 self.scaling_hooks = {}
3801 def s_sortingmode_change(self, ignore=None):
3802 for menuitem, valfunc in self.menuitems_ssorting:
3803 if menuitem.isChecked():
3804 self._ssort = valfunc
3806 self.sortingmode_change()
3808 def sortingmode_change(self, ignore=None):
3809 for menuitem, (gather, color) in self.menuitems_sorting:
3810 if menuitem.isChecked():
3811 self.set_gathering(gather, color)
3813 self.sortingmode_change_time = time.time()
3815 def lowpass_change(self, value, ignore=None):
3816 self.lowpass = value
3817 self.passband_check()
3818 self.tf_cache = {}
3819 self.update()
3821 def highpass_change(self, value, ignore=None):
3822 self.highpass = value
3823 self.passband_check()
3824 self.tf_cache = {}
3825 self.update()
3827 def passband_check(self):
3828 if self.highpass and self.lowpass \
3829 and self.highpass >= self.lowpass:
3831 self.message = 'Corner frequency of highpass larger than ' \
3832 'corner frequency of lowpass! I will now ' \
3833 'deactivate the highpass.'
3835 self.update_status()
3836 else:
3837 oldmess = self.message
3838 self.message = None
3839 if oldmess is not None:
3840 self.update_status()
3842 def gain_change(self, value, ignore):
3843 self.gain = value
3844 self.update()
3846 def rot_change(self, value, ignore):
3847 self.rotate = value
3848 self.update()
3850 def waterfall_cmap_change(self, cmap):
3851 self.waterfall_cmap = cmap
3852 self.update()
3854 def waterfall_clip_change(self, clip_min, clip_max):
3855 self.waterfall_clip_min = clip_min
3856 self.waterfall_clip_max = clip_max
3857 self.update()
3859 def waterfall_show_absolute_change(self, toggle):
3860 self.waterfall_show_absolute = toggle
3861 self.update()
3863 def waterfall_set_integrate(self, toggle):
3864 self.waterfall_integrate = toggle
3865 self.update()
3867 def set_selected_markers(self, markers):
3868 '''
3869 Set a list of markers selected
3871 :param markers: list of markers
3872 '''
3873 self.deselect_all()
3874 for m in markers:
3875 m.selected = True
3877 self.update()
3879 def deselect_all(self):
3880 for marker in self.markers:
3881 marker.selected = False
3883 def animate_picking(self):
3884 point = self.mapFromGlobal(qg.QCursor.pos())
3885 self.update_picking(point.x(), point.y(), doshift=True)
3887 def get_nslc_ids_for_track(self, ftrack):
3888 itrack = int(ftrack)
3889 return self.track_to_nslc_ids.get(itrack, [])
3891 def stop_picking(self, x, y, abort=False):
3892 if self.picking:
3893 self.update_picking(x, y, doshift=False)
3894 self.picking = None
3895 self.picking_down = None
3896 self.picking_timer.stop()
3897 self.picking_timer = None
3898 if not abort:
3899 self.add_marker(self.floating_marker)
3900 self.floating_marker.selected = True
3901 self.emit_selected_markers()
3903 self.floating_marker = None
3905 def start_picking(self, ignore):
3907 if not self.picking:
3908 self.deselect_all()
3909 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3910 point = self.mapFromGlobal(qg.QCursor.pos())
3912 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3913 self.picking.setGeometry(
3914 gpoint.x(), gpoint.y(), 1, self.height())
3915 t = self.time_projection.rev(point.x())
3917 ftrack = self.track_to_screen.rev(point.y())
3918 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3919 self.floating_marker = Marker(nslc_ids, t, t)
3920 self.floating_marker.selected = True
3922 self.picking_timer = qc.QTimer()
3923 self.picking_timer.timeout.connect(
3924 self.animate_picking)
3926 self.picking_timer.setInterval(50)
3927 self.picking_timer.start()
3929 def update_picking(self, x, y, doshift=False):
3930 if self.picking:
3931 mouset = self.time_projection.rev(x)
3932 dt = 0.0
3933 if mouset < self.tmin or mouset > self.tmax:
3934 if mouset < self.tmin:
3935 dt = -(self.tmin - mouset)
3936 else:
3937 dt = mouset - self.tmax
3938 ddt = self.tmax-self.tmin
3939 dt = max(dt, -ddt/10.)
3940 dt = min(dt, ddt/10.)
3942 x0 = x
3943 if self.picking_down is not None:
3944 x0 = self.time_projection(self.picking_down[0])
3946 w = abs(x-x0)
3947 x0 = min(x0, x)
3949 tmin, tmax = (
3950 self.time_projection.rev(x0),
3951 self.time_projection.rev(x0+w))
3953 tmin, tmax = (
3954 max(working_system_time_range[0], tmin),
3955 min(working_system_time_range[1], tmax))
3957 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3959 self.picking.setGeometry(
3960 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3962 ftrack = self.track_to_screen.rev(y)
3963 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3964 self.floating_marker.set(nslc_ids, tmin, tmax)
3966 if dt != 0.0 and doshift:
3967 self.interrupt_following()
3968 self.set_time_range(self.tmin+dt, self.tmax+dt)
3970 self.update()
3972 def update_status(self):
3974 if self.message is None:
3975 point = self.mapFromGlobal(qg.QCursor.pos())
3977 mouse_t = self.time_projection.rev(point.x())
3978 if not is_working_time(mouse_t):
3979 return
3981 if self.floating_marker:
3982 tmi, tma = (
3983 self.floating_marker.tmin,
3984 self.floating_marker.tmax)
3986 tt, ms = gmtime_x(tmi)
3988 if tmi == tma:
3989 message = mystrftime(
3990 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
3991 tt=tt, milliseconds=ms)
3992 else:
3993 srange = '%g s' % (tma-tmi)
3994 message = mystrftime(
3995 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
3996 tt=tt, milliseconds=ms)
3997 else:
3998 tt, ms = gmtime_x(mouse_t)
4000 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4001 else:
4002 message = self.message
4004 sb = self.window().statusBar()
4005 sb.clearMessage()
4006 sb.showMessage(message)
4008 def set_sortingmode_change_delay_time(self, dt):
4009 self.sortingmode_change_delay_time = dt
4011 def sortingmode_change_delayed(self):
4012 now = time.time()
4013 return (
4014 self.sortingmode_change_delay_time is not None
4015 and now - self.sortingmode_change_time
4016 < self.sortingmode_change_delay_time)
4018 def set_visible_marker_kinds(self, kinds):
4019 self.deselect_all()
4020 self.visible_marker_kinds = tuple(kinds)
4021 self.emit_selected_markers()
4023 def following(self):
4024 return self.follow_timer is not None \
4025 and not self.following_interrupted()
4027 def interrupt_following(self):
4028 self.interactive_range_change_time = time.time()
4030 def following_interrupted(self, now=None):
4031 if now is None:
4032 now = time.time()
4033 return now - self.interactive_range_change_time \
4034 < self.interactive_range_change_delay_time
4036 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4037 if tmax_start is None:
4038 tmax_start = time.time()
4039 self.show_all = False
4040 self.follow_time = tlen
4041 self.follow_timer = qc.QTimer(self)
4042 self.follow_timer.timeout.connect(
4043 self.follow_update)
4044 self.follow_timer.setInterval(interval)
4045 self.follow_timer.start()
4046 self.follow_started = time.time()
4047 self.follow_lapse = lapse
4048 self.follow_tshift = self.follow_started - tmax_start
4049 self.interactive_range_change_time = 0.0
4051 def unfollow(self):
4052 if self.follow_timer is not None:
4053 self.follow_timer.stop()
4054 self.follow_timer = None
4055 self.interactive_range_change_time = 0.0
4057 def follow_update(self):
4058 rnow = time.time()
4059 if self.follow_lapse is None:
4060 now = rnow
4061 else:
4062 now = self.follow_started + (rnow - self.follow_started) \
4063 * self.follow_lapse
4065 if self.following_interrupted(rnow):
4066 return
4067 self.set_time_range(
4068 now-self.follow_time-self.follow_tshift,
4069 now-self.follow_tshift)
4071 self.update()
4073 def myclose(self, return_tag=''):
4074 self.return_tag = return_tag
4075 self.window().close()
4077 def cleanup(self):
4078 self.about_to_close.emit()
4079 self.timer.stop()
4080 if self.follow_timer is not None:
4081 self.follow_timer.stop()
4083 for snuffling in list(self.snufflings):
4084 self.remove_snuffling(snuffling)
4086 def set_error_message(self, key, value):
4087 if value is None:
4088 if key in self.error_messages:
4089 del self.error_messages[key]
4090 else:
4091 self.error_messages[key] = value
4093 def inputline_changed(self, text):
4094 pass
4096 def inputline_finished(self, text):
4097 line = str(text)
4099 toks = line.split()
4100 clearit, hideit, error = False, True, None
4101 if len(toks) >= 1:
4102 command = toks[0].lower()
4104 try:
4105 quick_filter_commands = {
4106 'n': '%s.*.*.*',
4107 's': '*.%s.*.*',
4108 'l': '*.*.%s.*',
4109 'c': '*.*.*.%s'}
4111 if command in quick_filter_commands:
4112 if len(toks) >= 2:
4113 patterns = [
4114 quick_filter_commands[toks[0]] % pat
4115 for pat in toks[1:]]
4116 self.set_quick_filter_patterns(patterns, line)
4117 else:
4118 self.set_quick_filter_patterns(None)
4120 self.update()
4122 elif command in ('hide', 'unhide'):
4123 if len(toks) >= 2:
4124 patterns = []
4125 if len(toks) == 2:
4126 patterns = [toks[1]]
4127 elif len(toks) >= 3:
4128 x = {
4129 'n': '%s.*.*.*',
4130 's': '*.%s.*.*',
4131 'l': '*.*.%s.*',
4132 'c': '*.*.*.%s'}
4134 if toks[1] in x:
4135 patterns.extend(
4136 x[toks[1]] % tok for tok in toks[2:])
4138 for pattern in patterns:
4139 if command == 'hide':
4140 self.add_blacklist_pattern(pattern)
4141 else:
4142 self.remove_blacklist_pattern(pattern)
4144 elif command == 'unhide' and len(toks) == 1:
4145 self.clear_blacklist()
4147 clearit = True
4149 self.update()
4151 elif command == 'markers':
4152 if len(toks) == 2:
4153 if toks[1] == 'all':
4154 kinds = self.all_marker_kinds
4155 else:
4156 kinds = []
4157 for x in toks[1]:
4158 try:
4159 kinds.append(int(x))
4160 except Exception:
4161 pass
4163 self.set_visible_marker_kinds(kinds)
4165 elif len(toks) == 1:
4166 self.set_visible_marker_kinds(())
4168 self.update()
4170 elif command == 'scaling':
4171 if len(toks) == 2:
4172 hideit = False
4173 error = 'wrong number of arguments'
4175 if len(toks) >= 3:
4176 vmin, vmax = [
4177 pyrocko.model.float_or_none(x)
4178 for x in toks[-2:]]
4180 def upd(d, k, vmin, vmax):
4181 if k in d:
4182 if vmin is not None:
4183 d[k] = vmin, d[k][1]
4184 if vmax is not None:
4185 d[k] = d[k][0], vmax
4187 if len(toks) == 1:
4188 self.remove_scaling_hooks()
4190 elif len(toks) == 3:
4191 def hook(data_ranges):
4192 for k in data_ranges:
4193 upd(data_ranges, k, vmin, vmax)
4195 self.set_scaling_hook('_', hook)
4197 elif len(toks) == 4:
4198 pattern = toks[1]
4200 def hook(data_ranges):
4201 for k in pyrocko.util.match_nslcs(
4202 pattern, list(data_ranges.keys())):
4204 upd(data_ranges, k, vmin, vmax)
4206 self.set_scaling_hook(pattern, hook)
4208 elif command == 'goto':
4209 toks2 = line.split(None, 1)
4210 if len(toks2) == 2:
4211 arg = toks2[1]
4212 m = re.match(
4213 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4214 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4215 if m:
4216 tlen = None
4217 if not m.group(1):
4218 tlen = 12*32*24*60*60
4219 elif not m.group(2):
4220 tlen = 32*24*60*60
4221 elif not m.group(3):
4222 tlen = 24*60*60
4223 elif not m.group(4):
4224 tlen = 60*60
4225 elif not m.group(5):
4226 tlen = 60
4228 supl = '1970-01-01 00:00:00'
4229 if len(supl) > len(arg):
4230 arg = arg + supl[-(len(supl)-len(arg)):]
4231 t = pyrocko.util.str_to_time(arg)
4232 self.go_to_time(t, tlen=tlen)
4234 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4235 supl = '00:00:00'
4236 if len(supl) > len(arg):
4237 arg = arg + supl[-(len(supl)-len(arg)):]
4238 tmin, tmax = self.get_time_range()
4239 sdate = pyrocko.util.time_to_str(
4240 tmin/2.+tmax/2., format='%Y-%m-%d')
4241 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4242 self.go_to_time(t)
4244 elif arg == 'today':
4245 self.go_to_time(
4246 day_start(
4247 time.time()), tlen=24*60*60)
4249 elif arg == 'yesterday':
4250 self.go_to_time(
4251 day_start(
4252 time.time()-24*60*60), tlen=24*60*60)
4254 else:
4255 self.go_to_event_by_name(arg)
4257 else:
4258 raise PileViewerMainException(
4259 'No such command: %s' % command)
4261 except PileViewerMainException as e:
4262 error = str(e)
4263 hideit = False
4265 return clearit, hideit, error
4267 return PileViewerMain
4270PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4271GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4274class LineEditWithAbort(qw.QLineEdit):
4276 aborted = qc.pyqtSignal()
4277 history_down = qc.pyqtSignal()
4278 history_up = qc.pyqtSignal()
4280 def keyPressEvent(self, key_event):
4281 if key_event.key() == qc.Qt.Key_Escape:
4282 self.aborted.emit()
4283 elif key_event.key() == qc.Qt.Key_Down:
4284 self.history_down.emit()
4285 elif key_event.key() == qc.Qt.Key_Up:
4286 self.history_up.emit()
4287 else:
4288 return qw.QLineEdit.keyPressEvent(self, key_event)
4291class PileViewer(qw.QFrame):
4292 '''
4293 PileViewerMain + Controls + Inputline
4294 '''
4296 def __init__(
4297 self, pile,
4298 ntracks_shown_max=20,
4299 marker_editor_sortable=True,
4300 use_opengl=None,
4301 panel_parent=None,
4302 *args):
4304 qw.QFrame.__init__(self, *args)
4306 layout = qw.QGridLayout()
4307 layout.setContentsMargins(0, 0, 0, 0)
4308 layout.setSpacing(0)
4310 self.menu = PileViewerMenuBar(self)
4312 if use_opengl is None:
4313 use_opengl = is_macos
4315 if use_opengl:
4316 self.viewer = GLPileViewerMain(
4317 pile,
4318 ntracks_shown_max=ntracks_shown_max,
4319 panel_parent=panel_parent,
4320 menu=self.menu)
4321 else:
4322 self.viewer = PileViewerMain(
4323 pile,
4324 ntracks_shown_max=ntracks_shown_max,
4325 panel_parent=panel_parent,
4326 menu=self.menu)
4328 self.marker_editor_sortable = marker_editor_sortable
4330 self.setFrameShape(qw.QFrame.StyledPanel)
4331 self.setFrameShadow(qw.QFrame.Sunken)
4333 self.input_area = qw.QFrame(self)
4334 ia_layout = qw.QGridLayout()
4335 ia_layout.setContentsMargins(11, 11, 11, 11)
4336 self.input_area.setLayout(ia_layout)
4338 self.inputline = LineEditWithAbort(self.input_area)
4339 self.inputline.returnPressed.connect(
4340 self.inputline_returnpressed)
4341 self.inputline.editingFinished.connect(
4342 self.inputline_finished)
4343 self.inputline.aborted.connect(
4344 self.inputline_aborted)
4346 self.inputline.history_down.connect(
4347 lambda: self.step_through_history(1))
4348 self.inputline.history_up.connect(
4349 lambda: self.step_through_history(-1))
4351 self.inputline.textEdited.connect(
4352 self.inputline_changed)
4354 self.inputline.setPlaceholderText(
4355 u'Quick commands: e.g. \'c HH?\' to select channels. '
4356 u'Use ↑ or ↓ to navigate.')
4357 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4358 self.input_area.hide()
4359 self.history = None
4361 self.inputline_error_str = None
4363 self.inputline_error = qw.QLabel()
4364 self.inputline_error.hide()
4366 ia_layout.addWidget(self.inputline, 0, 0)
4367 ia_layout.addWidget(self.inputline_error, 1, 0)
4368 layout.addWidget(self.input_area, 0, 0, 1, 2)
4369 layout.addWidget(self.viewer, 1, 0)
4371 pb = Progressbars(self)
4372 layout.addWidget(pb, 2, 0, 1, 2)
4373 self.progressbars = pb
4375 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4376 self.scrollbar = scrollbar
4377 layout.addWidget(scrollbar, 1, 1)
4378 self.scrollbar.valueChanged.connect(
4379 self.scrollbar_changed)
4381 self.block_scrollbar_changes = False
4383 self.viewer.want_input.connect(
4384 self.inputline_show)
4385 self.viewer.tracks_range_changed.connect(
4386 self.tracks_range_changed)
4387 self.viewer.pile_has_changed_signal.connect(
4388 self.adjust_controls)
4389 self.viewer.about_to_close.connect(
4390 self.save_inputline_history)
4392 self.setLayout(layout)
4394 def cleanup(self):
4395 self.viewer.cleanup()
4397 def get_progressbars(self):
4398 return self.progressbars
4400 def inputline_show(self):
4401 if not self.history:
4402 self.load_inputline_history()
4404 self.input_area.show()
4405 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4406 self.inputline.selectAll()
4408 def inputline_set_error(self, string):
4409 self.inputline_error_str = string
4410 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4411 self.inputline.selectAll()
4412 self.inputline_error.setText(string)
4413 self.input_area.show()
4414 self.inputline_error.show()
4416 def inputline_clear_error(self):
4417 if self.inputline_error_str:
4418 self.inputline.setPalette(qw.QApplication.palette())
4419 self.inputline_error_str = None
4420 self.inputline_error.clear()
4421 self.inputline_error.hide()
4423 def inputline_changed(self, line):
4424 self.viewer.inputline_changed(str(line))
4425 self.inputline_clear_error()
4427 def inputline_returnpressed(self):
4428 line = str(self.inputline.text())
4429 clearit, hideit, error = self.viewer.inputline_finished(line)
4431 if error:
4432 self.inputline_set_error(error)
4434 line = line.strip()
4436 if line != '' and not error:
4437 if not (len(self.history) >= 1 and line == self.history[-1]):
4438 self.history.append(line)
4440 if clearit:
4442 self.inputline.blockSignals(True)
4443 qpat, qinp = self.viewer.get_quick_filter_patterns()
4444 if qpat is None:
4445 self.inputline.clear()
4446 else:
4447 self.inputline.setText(qinp)
4448 self.inputline.blockSignals(False)
4450 if hideit and not error:
4451 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4452 self.input_area.hide()
4454 self.hist_ind = len(self.history)
4456 def inputline_aborted(self):
4457 '''
4458 Hide the input line.
4459 '''
4460 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4461 self.hist_ind = len(self.history)
4462 self.input_area.hide()
4464 def save_inputline_history(self):
4465 '''
4466 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4467 '''
4468 if not self.history:
4469 return
4471 conf = pyrocko.config
4472 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4473 with open(fn_hist, 'w') as f:
4474 i = min(100, len(self.history))
4475 for c in self.history[-i:]:
4476 f.write('%s\n' % c)
4478 def load_inputline_history(self):
4479 '''
4480 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4481 '''
4482 conf = pyrocko.config
4483 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4484 if not os.path.exists(fn_hist):
4485 with open(fn_hist, 'w+') as f:
4486 f.write('\n')
4488 with open(fn_hist, 'r') as f:
4489 self.history = [line.strip() for line in f.readlines()]
4491 self.hist_ind = len(self.history)
4493 def step_through_history(self, ud=1):
4494 '''
4495 Step through input line history and set the input line text.
4496 '''
4497 n = len(self.history)
4498 self.hist_ind += ud
4499 self.hist_ind %= (n + 1)
4500 if len(self.history) != 0 and self.hist_ind != n:
4501 self.inputline.setText(self.history[self.hist_ind])
4502 else:
4503 self.inputline.setText('')
4505 def inputline_finished(self):
4506 pass
4508 def tracks_range_changed(self, ntracks, ilo, ihi):
4509 if self.block_scrollbar_changes:
4510 return
4512 self.scrollbar.blockSignals(True)
4513 self.scrollbar.setPageStep(ihi-ilo)
4514 vmax = max(0, ntracks-(ihi-ilo))
4515 self.scrollbar.setRange(0, vmax)
4516 self.scrollbar.setValue(ilo)
4517 self.scrollbar.setHidden(vmax == 0)
4518 self.scrollbar.blockSignals(False)
4520 def scrollbar_changed(self, value):
4521 self.block_scrollbar_changes = True
4522 ilo = value
4523 ihi = ilo + self.scrollbar.pageStep()
4524 self.viewer.set_tracks_range((ilo, ihi))
4525 self.block_scrollbar_changes = False
4526 self.update_contents()
4528 def controls(self):
4529 frame = qw.QFrame(self)
4530 layout = qw.QGridLayout()
4531 frame.setLayout(layout)
4533 minfreq = 0.001
4534 maxfreq = 1000.0
4535 self.lowpass_control = ValControl(high_is_none=True)
4536 self.lowpass_control.setup(
4537 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4538 self.highpass_control = ValControl(low_is_none=True)
4539 self.highpass_control.setup(
4540 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4541 self.gain_control = ValControl()
4542 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4543 self.rot_control = LinValControl()
4544 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4545 self.colorbar_control = ColorbarControl(self)
4547 self.lowpass_control.valchange.connect(
4548 self.viewer.lowpass_change)
4549 self.highpass_control.valchange.connect(
4550 self.viewer.highpass_change)
4551 self.gain_control.valchange.connect(
4552 self.viewer.gain_change)
4553 self.rot_control.valchange.connect(
4554 self.viewer.rot_change)
4555 self.colorbar_control.cmap_changed.connect(
4556 self.viewer.waterfall_cmap_change
4557 )
4558 self.colorbar_control.clip_changed.connect(
4559 self.viewer.waterfall_clip_change
4560 )
4561 self.colorbar_control.show_absolute_toggled.connect(
4562 self.viewer.waterfall_show_absolute_change
4563 )
4564 self.colorbar_control.show_integrate_toggled.connect(
4565 self.viewer.waterfall_set_integrate
4566 )
4568 for icontrol, control in enumerate((
4569 self.highpass_control,
4570 self.lowpass_control,
4571 self.gain_control,
4572 self.rot_control,
4573 self.colorbar_control)):
4575 for iwidget, widget in enumerate(control.widgets()):
4576 layout.addWidget(widget, icontrol, iwidget)
4578 spacer = qw.QSpacerItem(
4579 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4580 layout.addItem(spacer, 4, 0, 1, 3)
4582 self.adjust_controls()
4583 self.viewer.viewmode_change(ViewMode.Wiggle)
4584 return frame
4586 def marker_editor(self):
4587 editor = pyrocko.gui.marker_editor.MarkerEditor(
4588 self, sortable=self.marker_editor_sortable)
4590 editor.set_viewer(self.get_view())
4591 editor.get_marker_model().dataChanged.connect(
4592 self.update_contents)
4593 return editor
4595 def adjust_controls(self):
4596 dtmin, dtmax = self.viewer.content_deltat_range()
4597 maxfreq = 0.5/dtmin
4598 minfreq = (0.5/dtmax)*0.001
4599 self.lowpass_control.set_range(minfreq, maxfreq)
4600 self.highpass_control.set_range(minfreq, maxfreq)
4602 def setup_snufflings(self):
4603 self.viewer.setup_snufflings()
4605 def get_view(self):
4606 return self.viewer
4608 def update_contents(self):
4609 self.viewer.update()
4611 def get_pile(self):
4612 return self.viewer.get_pile()
4614 def show_colorbar_ctrl(self, show):
4615 for w in self.colorbar_control.widgets():
4616 w.setVisible(show)
4618 def show_gain_ctrl(self, show):
4619 for w in self.gain_control.widgets():
4620 w.setVisible(show)