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
562 data = []
563 for tick in ticks:
564 umin = xprojection(tick)
566 umin_approx_next = xprojection(tick+inc)
567 umax = xprojection(tick)
569 pinc_approx = umin_approx_next - umin
571 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
572 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
574 if tick == 0.0 and tmax - tmin < 3600*24:
575 # hide year at epoch
576 # (we assume that synthetic data is shown)
577 if l2:
578 l2 = None
579 elif l1:
580 l1 = None
582 if l0_center:
583 ushift = (umin_approx_next-umin)/2.
584 else:
585 ushift = 0.
587 abbr_level = 0
588 for l0x in (l0, l0_brief, ''):
589 label0 = l0x
590 rect0 = fm.boundingRect(label0)
591 if rect0.width() <= pinc_approx*0.9:
592 break
594 abbr_level += 1
596 data.append((
597 l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
598 pinc_approx))
600 for (l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
601 pinc_approx) in data:
603 label0 = (l0, l0_brief, '')[abbr_level]
604 rect0 = fm.boundingRect(label0)
606 if uumin+pad < umin-rect0.width()/2.+ushift and \
607 umin+rect0.width()/2.+ushift < uumax-pad:
609 if first_tick_with_label is None:
610 first_tick_with_label = tick
611 p.drawText(qc.QPointF(
612 umin-rect0.width()/2.+ushift,
613 vmin+rect0.height()+ticklen), label0)
615 if l1:
616 label1 = l1
617 rect1 = fm.boundingRect(label1)
618 if uumin+pad < umin-rect1.width()/2. and \
619 umin+rect1.width()/2. < uumax-pad:
621 p.drawText(qc.QPointF(
622 umin-rect1.width()/2.,
623 vmin+rect0.height()+rect1.height()+ticklen),
624 label1)
626 l1_hits += 1
628 if l2:
629 label2 = l2
630 rect2 = fm.boundingRect(label2)
631 if uumin+pad < umin-rect2.width()/2. and \
632 umin+rect2.width()/2. < uumax-pad:
634 p.drawText(qc.QPointF(
635 umin-rect2.width()/2.,
636 vmin+rect0.height()+rect1.height()+rect2.height() +
637 ticklen), label2)
639 l2_hits += 1
641 if first_tick_with_label is None:
642 first_tick_with_label = tmin
644 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
646 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
647 tmax - tmin < 3600*24:
649 # hide year at epoch (we assume that synthetic data is shown)
650 if l2:
651 l2 = None
652 elif l1:
653 l1 = None
655 if l1_hits == 0 and l1:
656 label1 = l1
657 rect1 = fm.boundingRect(label1)
658 p.drawText(qc.QPointF(
659 uumin+pad,
660 vmin+rect0.height()+rect1.height()+ticklen),
661 label1)
663 l1_hits += 1
665 if l2_hits == 0 and l2:
666 label2 = l2
667 rect2 = fm.boundingRect(label2)
668 p.drawText(qc.QPointF(
669 uumin+pad,
670 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
671 label2)
673 v = yprojection(0)
674 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
677class Projection(object):
678 def __init__(self):
679 self.xr = 0., 1.
680 self.ur = 0., 1.
682 def set_in_range(self, xmin, xmax):
683 if xmax == xmin:
684 xmax = xmin + 1.
686 self.xr = xmin, xmax
688 def get_in_range(self):
689 return self.xr
691 def set_out_range(self, umin, umax):
692 if umax == umin:
693 umax = umin + 1.
695 self.ur = umin, umax
697 def get_out_range(self):
698 return self.ur
700 def __call__(self, x):
701 umin, umax = self.ur
702 xmin, xmax = self.xr
703 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
705 def clipped(self, x, umax_pad):
706 umin, umax = self.ur
707 xmin, xmax = self.xr
708 return min(
709 umax-umax_pad,
710 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
712 def rev(self, u):
713 umin, umax = self.ur
714 xmin, xmax = self.xr
715 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
717 def copy(self):
718 return copy.copy(self)
721def add_radiobuttongroup(menu, menudef, target, default=None):
722 group = qw.QActionGroup(menu)
723 group.setExclusive(True)
724 menuitems = []
726 for name, value, *shortcut in menudef:
727 action = menu.addAction(name)
728 action.setCheckable(True)
729 action.setActionGroup(group)
730 if shortcut:
731 action.setShortcut(shortcut[0])
733 menuitems.append((action, value))
734 if default is not None and (
735 name.lower().replace(' ', '_') == default or
736 value == default):
737 action.setChecked(True)
739 group.triggered.connect(target)
741 if default is None:
742 menuitems[0][0].setChecked(True)
744 return menuitems
747def sort_actions(menu):
748 actions = [act for act in menu.actions() if not act.menu()]
749 for action in actions:
750 menu.removeAction(action)
751 actions.sort(key=lambda x: str(x.text()))
753 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
754 if help_action:
755 actions.insert(0, actions.pop(actions.index(help_action[0])))
756 for action in actions:
757 menu.addAction(action)
760fkey_map = dict(zip(
761 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
762 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
763 range(10)))
766class PileViewerMainException(Exception):
767 pass
770class PileViewerMenuBar(qw.QMenuBar):
771 ...
774class PileViewerMenu(qw.QMenu):
775 ...
778def MakePileViewerMainClass(base):
780 class PileViewerMain(base):
782 want_input = qc.pyqtSignal()
783 about_to_close = qc.pyqtSignal()
784 pile_has_changed_signal = qc.pyqtSignal()
785 tracks_range_changed = qc.pyqtSignal(int, int, int)
787 begin_markers_add = qc.pyqtSignal(int, int)
788 end_markers_add = qc.pyqtSignal()
789 begin_markers_remove = qc.pyqtSignal(int, int)
790 end_markers_remove = qc.pyqtSignal()
792 marker_selection_changed = qc.pyqtSignal(list)
793 active_event_marker_changed = qc.pyqtSignal()
795 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
796 menu=None):
797 base.__init__(self, *args)
799 self.pile = pile
800 self.ax_height = 80
801 self.panel_parent = panel_parent
803 self.click_tolerance = 5
805 self.ntracks_shown_max = ntracks_shown_max
806 self.initial_ntracks_shown_max = ntracks_shown_max
807 self.ntracks = 0
808 self.show_all = True
809 self.shown_tracks_range = None
810 self.track_start = None
811 self.track_trange = None
813 self.lowpass = None
814 self.highpass = None
815 self.gain = 1.0
816 self.rotate = 0.0
817 self.picking_down = None
818 self.picking = None
819 self.floating_marker = None
820 self.markers = pyrocko.pile.Sorted([], 'tmin')
821 self.markers_deltat_max = 0.
822 self.n_selected_markers = 0
823 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
824 self.visible_marker_kinds = self.all_marker_kinds
825 self.active_event_marker = None
826 self.ignore_releases = 0
827 self.message = None
828 self.reloaded = False
829 self.pile_has_changed = False
830 self.config = pyrocko.config.config('snuffler')
832 self.tax = TimeAx()
833 self.setBackgroundRole(qg.QPalette.Base)
834 self.setAutoFillBackground(True)
835 poli = qw.QSizePolicy(
836 qw.QSizePolicy.Expanding,
837 qw.QSizePolicy.Expanding)
839 self.setSizePolicy(poli)
840 self.setMinimumSize(300, 200)
841 self.setFocusPolicy(qc.Qt.ClickFocus)
843 self.menu = menu or PileViewerMenu(self)
845 file_menu = self.menu.addMenu('&File')
846 view_menu = self.menu.addMenu('&View')
847 options_menu = self.menu.addMenu('&Options')
848 scale_menu = self.menu.addMenu('&Scaling')
849 sort_menu = self.menu.addMenu('Sor&ting')
850 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
852 help_menu = self.menu.addMenu('&Help')
854 self.snufflings_menu = self.toggle_panel_menu.addMenu(
855 'Run Snuffling')
856 self.toggle_panel_menu.addSeparator()
857 self.snuffling_help = help_menu.addMenu('Snuffling Help')
858 help_menu.addSeparator()
860 file_menu.addAction(
861 qg.QIcon.fromTheme('document-open'),
862 'Open waveform files...',
863 self.open_waveforms,
864 qg.QKeySequence.Open)
866 file_menu.addAction(
867 qg.QIcon.fromTheme('document-open'),
868 'Open waveform directory...',
869 self.open_waveform_directory)
871 file_menu.addAction(
872 'Open station files...',
873 self.open_stations)
875 file_menu.addAction(
876 'Open StationXML files...',
877 self.open_stations_xml)
879 file_menu.addAction(
880 'Open event file...',
881 self.read_events)
883 file_menu.addSeparator()
884 file_menu.addAction(
885 'Open marker file...',
886 self.read_markers)
888 file_menu.addAction(
889 qg.QIcon.fromTheme('document-save'),
890 'Save markers...',
891 self.write_markers,
892 qg.QKeySequence.Save)
894 file_menu.addAction(
895 qg.QIcon.fromTheme('document-save-as'),
896 'Save selected markers...',
897 self.write_selected_markers,
898 qg.QKeySequence.SaveAs)
900 file_menu.addSeparator()
901 file_menu.addAction(
902 qg.QIcon.fromTheme('document-print'),
903 'Print',
904 self.printit,
905 qg.QKeySequence.Print)
907 file_menu.addAction(
908 qg.QIcon.fromTheme('insert-image'),
909 'Save as SVG or PNG',
910 self.savesvg,
911 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
913 file_menu.addSeparator()
914 close = file_menu.addAction(
915 qg.QIcon.fromTheme('window-close'),
916 'Close',
917 self.myclose)
918 close.setShortcuts(
919 (qg.QKeySequence(qc.Qt.Key_Q),
920 qg.QKeySequence(qc.Qt.Key_X)))
922 # Scale Menu
923 menudef = [
924 ('Individual Scale',
925 lambda tr: tr.nslc_id,
926 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
927 ('Common Scale',
928 lambda tr: None,
929 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
930 ('Common Scale per Station',
931 lambda tr: (tr.network, tr.station),
932 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
933 ('Common Scale per Station Location',
934 lambda tr: (tr.network, tr.station, tr.location)),
935 ('Common Scale per Component',
936 lambda tr: (tr.channel)),
937 ]
939 self.menuitems_scaling = add_radiobuttongroup(
940 scale_menu, menudef, self.scalingmode_change,
941 default=self.config.trace_scale)
942 scale_menu.addSeparator()
944 self.scaling_key = self.menuitems_scaling[0][1]
945 self.scaling_hooks = {}
946 self.scalingmode_change()
948 menudef = [
949 ('Scaling based on Minimum and Maximum',
950 ('minmax', 'minmax')),
951 ('Scaling based on Minimum and Maximum (Robust)',
952 ('minmax', 'robust')),
953 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
954 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
955 ]
957 self.menuitems_scaling_base = add_radiobuttongroup(
958 scale_menu, menudef, self.scaling_base_change)
960 self.scaling_base = self.menuitems_scaling_base[0][1]
961 scale_menu.addSeparator()
963 self.menuitem_fixscalerange = scale_menu.addAction(
964 'Fix Scale Ranges')
965 self.menuitem_fixscalerange.setCheckable(True)
967 # Sort Menu
968 def sector_dist(sta):
969 if sta.dist_m is None:
970 return None, None
971 else:
972 return (
973 sector_int(round((sta.azimuth+15.)/30.)),
974 m_float(sta.dist_m))
976 menudef = [
977 ('Sort by Names',
978 lambda tr: (),
979 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
980 ('Sort by Distance',
981 lambda tr: self.station_attrib(
982 tr,
983 lambda sta: (m_float_or_none(sta.dist_m),),
984 lambda tr: (None,)),
985 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
986 ('Sort by Azimuth',
987 lambda tr: self.station_attrib(
988 tr,
989 lambda sta: (deg_float_or_none(sta.azimuth),),
990 lambda tr: (None,))),
991 ('Sort by Distance in 12 Azimuthal Blocks',
992 lambda tr: self.station_attrib(
993 tr,
994 sector_dist,
995 lambda tr: (None, None))),
996 ('Sort by Backazimuth',
997 lambda tr: self.station_attrib(
998 tr,
999 lambda sta: (deg_float_or_none(sta.backazimuth),),
1000 lambda tr: (None,))),
1001 ]
1002 self.menuitems_ssorting = add_radiobuttongroup(
1003 sort_menu, menudef, self.s_sortingmode_change)
1004 sort_menu.addSeparator()
1006 self._ssort = lambda tr: ()
1008 self.menu.addSeparator()
1010 menudef = [
1011 ('Subsort by Network, Station, Location, Channel',
1012 ((0, 1, 2, 3), # gathering
1013 lambda tr: tr.location)), # coloring
1014 ('Subsort by Network, Station, Channel, Location',
1015 ((0, 1, 3, 2),
1016 lambda tr: tr.channel)),
1017 ('Subsort by Station, Network, Channel, Location',
1018 ((1, 0, 3, 2),
1019 lambda tr: tr.channel)),
1020 ('Subsort by Location, Network, Station, Channel',
1021 ((2, 0, 1, 3),
1022 lambda tr: tr.channel)),
1023 ('Subsort by Channel, Network, Station, Location',
1024 ((3, 0, 1, 2),
1025 lambda tr: (tr.network, tr.station, tr.location))),
1026 ('Subsort by Network, Station, Channel (Grouped by Location)',
1027 ((0, 1, 3),
1028 lambda tr: tr.location)),
1029 ('Subsort by Station, Network, Channel (Grouped by Location)',
1030 ((1, 0, 3),
1031 lambda tr: tr.location)),
1032 ]
1034 self.menuitems_sorting = add_radiobuttongroup(
1035 sort_menu, menudef, self.sortingmode_change)
1037 menudef = [(x.key, x.value) for x in
1038 self.config.visible_length_setting]
1040 # View menu
1041 self.menuitems_visible_length = add_radiobuttongroup(
1042 view_menu, menudef,
1043 self.visible_length_change)
1044 view_menu.addSeparator()
1046 view_modes = [
1047 ('Wiggle Plot', ViewMode.Wiggle),
1048 ('Waterfall', ViewMode.Waterfall)
1049 ]
1051 self.menuitems_viewmode = add_radiobuttongroup(
1052 view_menu, view_modes,
1053 self.viewmode_change, default=ViewMode.Wiggle)
1054 view_menu.addSeparator()
1056 self.menuitem_cliptraces = view_menu.addAction(
1057 'Clip Traces')
1058 self.menuitem_cliptraces.setCheckable(True)
1059 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1061 self.menuitem_showboxes = view_menu.addAction(
1062 'Show Boxes')
1063 self.menuitem_showboxes.setCheckable(True)
1064 self.menuitem_showboxes.setChecked(
1065 self.config.show_boxes)
1067 self.menuitem_colortraces = view_menu.addAction(
1068 'Color Traces')
1069 self.menuitem_colortraces.setCheckable(True)
1070 self.menuitem_antialias = view_menu.addAction(
1071 'Antialiasing')
1072 self.menuitem_antialias.setCheckable(True)
1074 view_menu.addSeparator()
1075 self.menuitem_showscalerange = view_menu.addAction(
1076 'Show Scale Ranges')
1077 self.menuitem_showscalerange.setCheckable(True)
1078 self.menuitem_showscalerange.setChecked(
1079 self.config.show_scale_ranges)
1081 self.menuitem_showscaleaxis = view_menu.addAction(
1082 'Show Scale Axes')
1083 self.menuitem_showscaleaxis.setCheckable(True)
1084 self.menuitem_showscaleaxis.setChecked(
1085 self.config.show_scale_axes)
1087 self.menuitem_showzeroline = view_menu.addAction(
1088 'Show Zero Lines')
1089 self.menuitem_showzeroline.setCheckable(True)
1091 view_menu.addSeparator()
1092 view_menu.addAction(
1093 qg.QIcon.fromTheme('view-fullscreen'),
1094 'Fullscreen',
1095 self.toggle_fullscreen,
1096 qg.QKeySequence(qc.Qt.Key_F11))
1098 # Options Menu
1099 self.menuitem_demean = options_menu.addAction('Demean')
1100 self.menuitem_demean.setCheckable(True)
1101 self.menuitem_demean.setChecked(self.config.demean)
1102 self.menuitem_demean.setShortcut(
1103 qg.QKeySequence(qc.Qt.Key_Underscore))
1105 self.menuitem_distances_3d = options_menu.addAction(
1106 '3D distances',
1107 self.distances_3d_changed)
1108 self.menuitem_distances_3d.setCheckable(True)
1110 self.menuitem_allowdownsampling = options_menu.addAction(
1111 'Allow Downsampling')
1112 self.menuitem_allowdownsampling.setCheckable(True)
1113 self.menuitem_allowdownsampling.setChecked(True)
1115 self.menuitem_degap = options_menu.addAction(
1116 'Allow Degapping')
1117 self.menuitem_degap.setCheckable(True)
1118 self.menuitem_degap.setChecked(True)
1120 options_menu.addSeparator()
1122 self.menuitem_fft_filtering = options_menu.addAction(
1123 'FFT Filtering')
1124 self.menuitem_fft_filtering.setCheckable(True)
1126 self.menuitem_lphp = options_menu.addAction(
1127 'Bandpass is Low- + Highpass')
1128 self.menuitem_lphp.setCheckable(True)
1129 self.menuitem_lphp.setChecked(True)
1131 options_menu.addSeparator()
1132 self.menuitem_watch = options_menu.addAction(
1133 'Watch Files')
1134 self.menuitem_watch.setCheckable(True)
1136 self.menuitem_liberal_fetch = options_menu.addAction(
1137 'Liberal Fetch Optimization')
1138 self.menuitem_liberal_fetch.setCheckable(True)
1140 self.visible_length = menudef[0][1]
1142 self.snufflings_menu.addAction(
1143 'Reload Snufflings',
1144 self.setup_snufflings)
1146 # Disable ShadowPileTest
1147 if False:
1148 test_action = self.menu.addAction(
1149 'Test',
1150 self.toggletest)
1151 test_action.setCheckable(True)
1153 help_menu.addAction(
1154 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1155 'Snuffler Controls',
1156 self.help,
1157 qg.QKeySequence(qc.Qt.Key_Question))
1159 help_menu.addAction(
1160 'About',
1161 self.about)
1163 self.time_projection = Projection()
1164 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1165 self.time_projection.set_out_range(0., self.width())
1167 self.gather = None
1169 self.trace_filter = None
1170 self.quick_filter = None
1171 self.quick_filter_patterns = None, None
1172 self.blacklist = []
1174 self.track_to_screen = Projection()
1175 self.track_to_nslc_ids = {}
1177 self.cached_vec = None
1178 self.cached_processed_traces = None
1180 self.timer = qc.QTimer(self)
1181 self.timer.timeout.connect(self.periodical)
1182 self.timer.setInterval(1000)
1183 self.timer.start()
1184 self.pile.add_listener(self)
1185 self.trace_styles = {}
1186 if self.get_squirrel() is None:
1187 self.determine_box_styles()
1189 self.setMouseTracking(True)
1191 user_home_dir = os.path.expanduser('~')
1192 self.snuffling_modules = {}
1193 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1194 self.default_snufflings = None
1195 self.snufflings = []
1197 self.stations = {}
1199 self.timer_draw = Timer()
1200 self.timer_cutout = Timer()
1201 self.time_spent_painting = 0.0
1202 self.time_last_painted = time.time()
1204 self.interactive_range_change_time = 0.0
1205 self.interactive_range_change_delay_time = 10.0
1206 self.follow_timer = None
1208 self.sortingmode_change_time = 0.0
1209 self.sortingmode_change_delay_time = None
1211 self.old_data_ranges = {}
1213 self.error_messages = {}
1214 self.return_tag = None
1215 self.wheel_pos = 60
1217 self.setAcceptDrops(True)
1218 self._paths_to_load = []
1220 self.tf_cache = {}
1222 self.waterfall = TraceWaterfall()
1223 self.waterfall_cmap = 'viridis'
1224 self.waterfall_clip_min = 0.
1225 self.waterfall_clip_max = 1.
1226 self.waterfall_show_absolute = False
1227 self.waterfall_integrate = False
1228 self.view_mode = ViewMode.Wiggle
1230 self.automatic_updates = True
1232 self.closing = False
1233 self.in_paint_event = False
1235 def fail(self, reason):
1236 box = qw.QMessageBox(self)
1237 box.setText(reason)
1238 box.exec_()
1240 def set_trace_filter(self, filter_func):
1241 self.trace_filter = filter_func
1242 self.sortingmode_change()
1244 def update_trace_filter(self):
1245 if self.blacklist:
1247 def blacklist_func(tr):
1248 return not pyrocko.util.match_nslc(
1249 self.blacklist, tr.nslc_id)
1251 else:
1252 blacklist_func = None
1254 if self.quick_filter is None and blacklist_func is None:
1255 self.set_trace_filter(None)
1256 elif self.quick_filter is None:
1257 self.set_trace_filter(blacklist_func)
1258 elif blacklist_func is None:
1259 self.set_trace_filter(self.quick_filter)
1260 else:
1261 self.set_trace_filter(
1262 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1264 def set_quick_filter(self, filter_func):
1265 self.quick_filter = filter_func
1266 self.update_trace_filter()
1268 def set_quick_filter_patterns(self, patterns, inputline=None):
1269 if patterns is not None:
1270 self.set_quick_filter(
1271 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1272 else:
1273 self.set_quick_filter(None)
1275 self.quick_filter_patterns = patterns, inputline
1277 def get_quick_filter_patterns(self):
1278 return self.quick_filter_patterns
1280 def add_blacklist_pattern(self, pattern):
1281 if pattern == 'empty':
1282 keys = set(self.pile.nslc_ids)
1283 trs = self.pile.all(
1284 tmin=self.tmin,
1285 tmax=self.tmax,
1286 load_data=False,
1287 degap=False)
1289 for tr in trs:
1290 if tr.nslc_id in keys:
1291 keys.remove(tr.nslc_id)
1293 for key in keys:
1294 xpattern = '.'.join(key)
1295 if xpattern not in self.blacklist:
1296 self.blacklist.append(xpattern)
1298 else:
1299 if pattern in self.blacklist:
1300 self.blacklist.remove(pattern)
1302 self.blacklist.append(pattern)
1304 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1305 self.update_trace_filter()
1307 def remove_blacklist_pattern(self, pattern):
1308 if pattern in self.blacklist:
1309 self.blacklist.remove(pattern)
1310 else:
1311 raise PileViewerMainException(
1312 'Pattern not found in blacklist.')
1314 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1315 self.update_trace_filter()
1317 def clear_blacklist(self):
1318 self.blacklist = []
1319 self.update_trace_filter()
1321 def ssort(self, tr):
1322 return self._ssort(tr)
1324 def station_key(self, x):
1325 return x.network, x.station
1327 def station_keys(self, x):
1328 return [
1329 (x.network, x.station, x.location),
1330 (x.network, x.station)]
1332 def station_attrib(self, tr, getter, default_getter):
1333 for sk in self.station_keys(tr):
1334 if sk in self.stations:
1335 station = self.stations[sk]
1336 return getter(station)
1338 return default_getter(tr)
1340 def get_station(self, sk):
1341 return self.stations[sk]
1343 def has_station(self, station):
1344 for sk in self.station_keys(station):
1345 if sk in self.stations:
1346 return True
1348 return False
1350 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1351 return self.station_attrib(
1352 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1354 def set_stations(self, stations):
1355 self.stations = {}
1356 self.add_stations(stations)
1358 def add_stations(self, stations):
1359 for station in stations:
1360 for sk in self.station_keys(station):
1361 self.stations[sk] = station
1363 ev = self.get_active_event()
1364 if ev:
1365 self.set_origin(ev)
1367 def add_event(self, event):
1368 marker = EventMarker(event)
1369 self.add_marker(marker)
1371 def add_events(self, events):
1372 markers = [EventMarker(e) for e in events]
1373 self.add_markers(markers)
1375 def set_event_marker_as_origin(self, ignore=None):
1376 selected = self.selected_markers()
1377 if not selected:
1378 self.fail('An event marker must be selected.')
1379 return
1381 m = selected[0]
1382 if not isinstance(m, EventMarker):
1383 self.fail('Selected marker is not an event.')
1384 return
1386 self.set_active_event_marker(m)
1388 def deactivate_event_marker(self):
1389 if self.active_event_marker:
1390 self.active_event_marker.active = False
1392 self.active_event_marker_changed.emit()
1393 self.active_event_marker = None
1395 def set_active_event_marker(self, event_marker):
1396 if self.active_event_marker:
1397 self.active_event_marker.active = False
1399 self.active_event_marker = event_marker
1400 event_marker.active = True
1401 event = event_marker.get_event()
1402 self.set_origin(event)
1403 self.active_event_marker_changed.emit()
1405 def set_active_event(self, event):
1406 for marker in self.markers:
1407 if isinstance(marker, EventMarker):
1408 if marker.get_event() is event:
1409 self.set_active_event_marker(marker)
1411 def get_active_event_marker(self):
1412 return self.active_event_marker
1414 def get_active_event(self):
1415 m = self.get_active_event_marker()
1416 if m is not None:
1417 return m.get_event()
1418 else:
1419 return None
1421 def get_active_markers(self):
1422 emarker = self.get_active_event_marker()
1423 if emarker is None:
1424 return None, []
1426 else:
1427 ev = emarker.get_event()
1428 pmarkers = [
1429 m for m in self.markers
1430 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1432 return emarker, pmarkers
1434 def set_origin(self, location):
1435 for station in self.stations.values():
1436 station.set_event_relative_data(
1437 location,
1438 distance_3d=self.menuitem_distances_3d.isChecked())
1440 self.sortingmode_change()
1442 def distances_3d_changed(self):
1443 ignore = self.menuitem_distances_3d.isChecked()
1444 self.set_event_marker_as_origin(ignore)
1446 def iter_snuffling_modules(self):
1447 pjoin = os.path.join
1448 for path in self.snuffling_paths:
1450 if not os.path.isdir(path):
1451 os.mkdir(path)
1453 for entry in os.listdir(path):
1454 directory = path
1455 fn = entry
1456 d = pjoin(path, entry)
1457 if os.path.isdir(d):
1458 directory = d
1459 if os.path.isfile(
1460 os.path.join(directory, 'snuffling.py')):
1461 fn = 'snuffling.py'
1463 if not fn.endswith('.py'):
1464 continue
1466 name = fn[:-3]
1468 if (directory, name) not in self.snuffling_modules:
1469 self.snuffling_modules[directory, name] = \
1470 pyrocko.gui.snuffling.SnufflingModule(
1471 directory, name, self)
1473 yield self.snuffling_modules[directory, name]
1475 def setup_snufflings(self):
1476 # user snufflings
1477 for mod in self.iter_snuffling_modules():
1478 try:
1479 mod.load_if_needed()
1480 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1481 logger.warning('Snuffling module "%s" is broken' % e)
1483 # load the default snufflings on first run
1484 if self.default_snufflings is None:
1485 self.default_snufflings = pyrocko.gui\
1486 .snufflings.__snufflings__()
1487 for snuffling in self.default_snufflings:
1488 self.add_snuffling(snuffling)
1490 def set_panel_parent(self, panel_parent):
1491 self.panel_parent = panel_parent
1493 def get_panel_parent(self):
1494 return self.panel_parent
1496 def add_snuffling(self, snuffling, reloaded=False):
1497 logger.debug('Adding snuffling %s' % snuffling.get_name())
1498 snuffling.init_gui(
1499 self, self.get_panel_parent(), self, reloaded=reloaded)
1500 self.snufflings.append(snuffling)
1501 self.update()
1503 def remove_snuffling(self, snuffling):
1504 snuffling.delete_gui()
1505 self.update()
1506 self.snufflings.remove(snuffling)
1507 snuffling.pre_destroy()
1509 def add_snuffling_menuitem(self, item):
1510 self.snufflings_menu.addAction(item)
1511 item.setParent(self.snufflings_menu)
1512 sort_actions(self.snufflings_menu)
1514 def remove_snuffling_menuitem(self, item):
1515 self.snufflings_menu.removeAction(item)
1517 def add_snuffling_help_menuitem(self, item):
1518 self.snuffling_help.addAction(item)
1519 item.setParent(self.snuffling_help)
1520 sort_actions(self.snuffling_help)
1522 def remove_snuffling_help_menuitem(self, item):
1523 self.snuffling_help.removeAction(item)
1525 def add_panel_toggler(self, item):
1526 self.toggle_panel_menu.addAction(item)
1527 item.setParent(self.toggle_panel_menu)
1528 sort_actions(self.toggle_panel_menu)
1530 def remove_panel_toggler(self, item):
1531 self.toggle_panel_menu.removeAction(item)
1533 def load(self, paths, regex=None, format='detect',
1534 cache_dir=None, force_cache=False):
1536 if cache_dir is None:
1537 cache_dir = pyrocko.config.config().cache_dir
1538 if isinstance(paths, str):
1539 paths = [paths]
1541 fns = pyrocko.util.select_files(
1542 paths, selector=None, include=regex, show_progress=False)
1544 if not fns:
1545 return
1547 cache = pyrocko.pile.get_cache(cache_dir)
1549 t = [time.time()]
1551 def update_bar(label, value):
1552 pbs = self.parent().get_progressbars()
1553 if label.lower() == 'looking at files':
1554 label = 'Looking at %i files' % len(fns)
1555 else:
1556 label = 'Scanning %i files' % len(fns)
1558 return pbs.set_status(label, value)
1560 def update_progress(label, i, n):
1561 abort = False
1563 qw.qApp.processEvents()
1564 if n != 0:
1565 perc = i*100/n
1566 else:
1567 perc = 100
1568 abort |= update_bar(label, perc)
1569 abort |= self.window().is_closing()
1571 tnow = time.time()
1572 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1573 self.update()
1574 t[0] = tnow
1576 return abort
1578 self.automatic_updates = False
1580 self.pile.load_files(
1581 sorted(fns),
1582 filename_attributes=regex,
1583 cache=cache,
1584 fileformat=format,
1585 show_progress=False,
1586 update_progress=update_progress)
1588 self.automatic_updates = True
1589 self.update()
1591 def load_queued(self):
1592 if not self._paths_to_load:
1593 return
1594 paths = self._paths_to_load
1595 self._paths_to_load = []
1596 self.load(paths)
1598 def load_soon(self, paths):
1599 self._paths_to_load.extend(paths)
1600 qc.QTimer.singleShot(200, self.load_queued)
1602 def open_waveforms(self):
1603 caption = 'Select one or more files to open'
1605 fns, _ = qw.QFileDialog.getOpenFileNames(
1606 self, caption, options=qfiledialog_options)
1608 if fns:
1609 self.load(list(str(fn) for fn in fns))
1611 def open_waveform_directory(self):
1612 caption = 'Select directory to scan for waveform files'
1614 dn = qw.QFileDialog.getExistingDirectory(
1615 self, caption, options=qfiledialog_options)
1617 if dn:
1618 self.load([str(dn)])
1620 def open_stations(self, fns=None):
1621 caption = 'Select one or more Pyrocko station files to open'
1623 if not fns:
1624 fns, _ = qw.QFileDialog.getOpenFileNames(
1625 self, caption, options=qfiledialog_options)
1627 try:
1628 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1629 for stat in stations:
1630 self.add_stations(stat)
1632 except Exception as e:
1633 self.fail('Failed to read station file: %s' % str(e))
1635 def open_stations_xml(self, fns=None):
1636 from pyrocko.io import stationxml
1638 caption = 'Select one or more StationXML files'
1639 if not fns:
1640 fns, _ = qw.QFileDialog.getOpenFileNames(
1641 self, caption, options=qfiledialog_options,
1642 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1643 ';;All files (*)')
1645 try:
1646 stations = [
1647 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1648 for x in fns]
1650 for stat in stations:
1651 self.add_stations(stat)
1653 except Exception as e:
1654 self.fail('Failed to read StationXML file: %s' % str(e))
1656 def add_traces(self, traces):
1657 if traces:
1658 mtf = pyrocko.pile.MemTracesFile(None, traces)
1659 self.pile.add_file(mtf)
1660 ticket = (self.pile, mtf)
1661 return ticket
1662 else:
1663 return (None, None)
1665 def release_data(self, tickets):
1666 for ticket in tickets:
1667 pile, mtf = ticket
1668 if pile is not None:
1669 pile.remove_file(mtf)
1671 def periodical(self):
1672 if self.menuitem_watch.isChecked():
1673 if self.pile.reload_modified():
1674 self.update()
1676 def get_pile(self):
1677 return self.pile
1679 def pile_changed(self, what):
1680 self.pile_has_changed = True
1681 self.pile_has_changed_signal.emit()
1682 if self.automatic_updates:
1683 self.update()
1685 def set_gathering(self, gather=None, color=None):
1687 if gather is None:
1688 def gather_func(tr):
1689 return tr.nslc_id
1691 gather = (0, 1, 2, 3)
1693 else:
1694 def gather_func(tr):
1695 return (
1696 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1698 if color is None:
1699 def color(tr):
1700 return tr.location
1702 self.gather = gather_func
1703 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1705 self.color_gather = color
1706 self.color_keys = self.pile.gather_keys(color)
1707 previous_ntracks = self.ntracks
1708 self.set_ntracks(len(keys))
1710 if self.shown_tracks_range is None or \
1711 previous_ntracks == 0 or \
1712 self.show_all:
1714 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1715 key_at_top = None
1716 n = high-low
1718 else:
1719 low, high = self.shown_tracks_range
1720 key_at_top = self.track_keys[low]
1721 n = high-low
1723 self.track_keys = sorted(keys)
1725 track_patterns = []
1726 for k in self.track_keys:
1727 pat = ['*', '*', '*', '*']
1728 for i, j in enumerate(gather):
1729 pat[j] = k[-len(gather)+i]
1731 track_patterns.append(pat)
1733 self.track_patterns = track_patterns
1735 if key_at_top is not None:
1736 try:
1737 ind = self.track_keys.index(key_at_top)
1738 low = ind
1739 high = low+n
1740 except Exception:
1741 pass
1743 self.set_tracks_range((low, high))
1745 self.key_to_row = dict(
1746 [(key, i) for (i, key) in enumerate(self.track_keys)])
1748 def inrange(x, r):
1749 return r[0] <= x and x < r[1]
1751 def trace_selector(trace):
1752 gt = self.gather(trace)
1753 return (
1754 gt in self.key_to_row and
1755 inrange(self.key_to_row[gt], self.shown_tracks_range))
1757 self.trace_selector = lambda x: \
1758 (self.trace_filter is None or self.trace_filter(x)) \
1759 and trace_selector(x)
1761 if self.tmin == working_system_time_range[0] and \
1762 self.tmax == working_system_time_range[1] or \
1763 self.show_all:
1765 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1766 if tmin is not None and tmax is not None:
1767 tlen = (tmax - tmin)
1768 tpad = tlen * 5./self.width()
1769 self.set_time_range(tmin-tpad, tmax+tpad)
1771 def set_time_range(self, tmin, tmax):
1772 if tmin is None:
1773 tmin = initial_time_range[0]
1775 if tmax is None:
1776 tmax = initial_time_range[1]
1778 if tmin > tmax:
1779 tmin, tmax = tmax, tmin
1781 if tmin == tmax:
1782 tmin -= 1.
1783 tmax += 1.
1785 tmin = max(working_system_time_range[0], tmin)
1786 tmax = min(working_system_time_range[1], tmax)
1788 min_deltat = self.content_deltat_range()[0]
1789 if (tmax - tmin < min_deltat):
1790 m = (tmin + tmax) / 2.
1791 tmin = m - min_deltat/2.
1792 tmax = m + min_deltat/2.
1794 self.time_projection.set_in_range(tmin, tmax)
1795 self.tmin, self.tmax = tmin, tmax
1797 def get_time_range(self):
1798 return self.tmin, self.tmax
1800 def ypart(self, y):
1801 if y < self.ax_height:
1802 return -1
1803 elif y > self.height()-self.ax_height:
1804 return 1
1805 else:
1806 return 0
1808 def time_fractional_digits(self):
1809 min_deltat = self.content_deltat_range()[0]
1810 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1812 def write_markers(self, fn=None):
1813 caption = "Choose a file name to write markers"
1814 if not fn:
1815 fn, _ = qw.QFileDialog.getSaveFileName(
1816 self, caption, options=qfiledialog_options)
1817 if fn:
1818 try:
1819 Marker.save_markers(
1820 self.markers, fn,
1821 fdigits=self.time_fractional_digits())
1823 except Exception as e:
1824 self.fail('Failed to write marker file: %s' % str(e))
1826 def write_selected_markers(self, fn=None):
1827 caption = "Choose a file name to write selected markers"
1828 if not fn:
1829 fn, _ = qw.QFileDialog.getSaveFileName(
1830 self, caption, options=qfiledialog_options)
1831 if fn:
1832 try:
1833 Marker.save_markers(
1834 self.iter_selected_markers(),
1835 fn,
1836 fdigits=self.time_fractional_digits())
1838 except Exception as e:
1839 self.fail('Failed to write marker file: %s' % str(e))
1841 def read_events(self, fn=None):
1842 '''
1843 Open QFileDialog to open, read and add
1844 :py:class:`pyrocko.model.Event` instances and their marker
1845 representation to the pile viewer.
1846 '''
1847 caption = "Selet one or more 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_events(pyrocko.model.load_events(fn))
1854 self.associate_phases_to_events()
1856 except Exception as e:
1857 self.fail('Failed to read event file: %s' % str(e))
1859 def read_markers(self, fn=None):
1860 '''
1861 Open QFileDialog to open, read and add markers to the pile viewer.
1862 '''
1863 caption = "Selet one or more marker files to open"
1864 if not fn:
1865 fn, _ = qw.QFileDialog.getOpenFileName(
1866 self, caption, options=qfiledialog_options)
1867 if fn:
1868 try:
1869 self.add_markers(Marker.load_markers(fn))
1870 self.associate_phases_to_events()
1872 except Exception as e:
1873 self.fail('Failed to read marker file: %s' % str(e))
1875 def associate_phases_to_events(self):
1876 associate_phases_to_events(self.markers)
1878 def add_marker(self, marker):
1879 # need index to inform QAbstactTableModel about upcoming change,
1880 # but have to restore current state in order to not cause problems
1881 self.markers.insert(marker)
1882 i = self.markers.remove(marker)
1884 self.begin_markers_add.emit(i, i)
1885 self.markers.insert(marker)
1886 self.end_markers_add.emit()
1887 self.markers_deltat_max = max(
1888 self.markers_deltat_max, marker.tmax - marker.tmin)
1890 def add_markers(self, markers):
1891 if not self.markers:
1892 self.begin_markers_add.emit(0, len(markers) - 1)
1893 self.markers.insert_many(markers)
1894 self.end_markers_add.emit()
1895 self.update_markers_deltat_max()
1896 else:
1897 for marker in markers:
1898 self.add_marker(marker)
1900 def update_markers_deltat_max(self):
1901 if self.markers:
1902 self.markers_deltat_max = max(
1903 marker.tmax - marker.tmin for marker in self.markers)
1905 def remove_marker(self, marker):
1906 '''
1907 Remove a ``marker`` from the :py:class:`PileViewer`.
1909 :param marker: :py:class:`Marker` (or subclass) instance
1910 '''
1912 if marker is self.active_event_marker:
1913 self.deactivate_event_marker()
1915 try:
1916 i = self.markers.index(marker)
1917 self.begin_markers_remove.emit(i, i)
1918 self.markers.remove_at(i)
1919 self.end_markers_remove.emit()
1920 except ValueError:
1921 pass
1923 def remove_markers(self, markers):
1924 '''
1925 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1927 :param markers: list of :py:class:`Marker` (or subclass)
1928 instances
1929 '''
1931 if markers is self.markers:
1932 markers = list(markers)
1934 for marker in markers:
1935 self.remove_marker(marker)
1937 self.update_markers_deltat_max()
1939 def remove_selected_markers(self):
1940 def delete_segment(istart, iend):
1941 self.begin_markers_remove.emit(istart, iend-1)
1942 for _ in range(iend - istart):
1943 self.markers.remove_at(istart)
1945 self.end_markers_remove.emit()
1947 istart = None
1948 ipos = 0
1949 markers = self.markers
1950 nmarkers = len(self.markers)
1951 while ipos < nmarkers:
1952 marker = markers[ipos]
1953 if marker.is_selected():
1954 if marker is self.active_event_marker:
1955 self.deactivate_event_marker()
1957 if istart is None:
1958 istart = ipos
1959 else:
1960 if istart is not None:
1961 delete_segment(istart, ipos)
1962 nmarkers -= ipos - istart
1963 ipos = istart - 1
1964 istart = None
1966 ipos += 1
1968 if istart is not None:
1969 delete_segment(istart, ipos)
1971 self.update_markers_deltat_max()
1973 def selected_markers(self):
1974 return [marker for marker in self.markers if marker.is_selected()]
1976 def iter_selected_markers(self):
1977 for marker in self.markers:
1978 if marker.is_selected():
1979 yield marker
1981 def get_markers(self):
1982 return self.markers
1984 def mousePressEvent(self, mouse_ev):
1985 self.show_all = False
1986 point = self.mapFromGlobal(mouse_ev.globalPos())
1988 if mouse_ev.button() == qc.Qt.LeftButton:
1989 marker = self.marker_under_cursor(point.x(), point.y())
1990 if self.picking:
1991 if self.picking_down is None:
1992 self.picking_down = (
1993 self.time_projection.rev(mouse_ev.x()),
1994 mouse_ev.y())
1996 elif marker is not None:
1997 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1998 self.deselect_all()
1999 marker.selected = True
2000 self.emit_selected_markers()
2001 self.update()
2002 else:
2003 self.track_start = mouse_ev.x(), mouse_ev.y()
2004 self.track_trange = self.tmin, self.tmax
2006 if mouse_ev.button() == qc.Qt.RightButton \
2007 and isinstance(self.menu, qw.QMenu):
2008 self.menu.exec_(qg.QCursor.pos())
2009 self.update_status()
2011 def mouseReleaseEvent(self, mouse_ev):
2012 if self.ignore_releases:
2013 self.ignore_releases -= 1
2014 return
2016 if self.picking:
2017 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2018 self.emit_selected_markers()
2020 if self.track_start:
2021 self.update()
2023 self.track_start = None
2024 self.track_trange = None
2025 self.update_status()
2027 def mouseDoubleClickEvent(self, mouse_ev):
2028 self.show_all = False
2029 self.start_picking(None)
2030 self.ignore_releases = 1
2032 def mouseMoveEvent(self, mouse_ev):
2033 point = self.mapFromGlobal(mouse_ev.globalPos())
2035 if self.picking:
2036 self.update_picking(point.x(), point.y())
2038 elif self.track_start is not None:
2039 x0, y0 = self.track_start
2040 dx = (point.x() - x0)/float(self.width())
2041 dy = (point.y() - y0)/float(self.height())
2042 if self.ypart(y0) == 1:
2043 dy = 0
2045 tmin0, tmax0 = self.track_trange
2047 scale = math.exp(-dy*5.)
2048 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2049 frac = x0/float(self.width())
2050 dt = dx*(tmax0-tmin0)*scale
2052 self.interrupt_following()
2053 self.set_time_range(
2054 tmin0 - dt - dtr*frac,
2055 tmax0 - dt + dtr*(1.-frac))
2057 self.update()
2058 else:
2059 self.hoovering(point.x(), point.y())
2061 self.update_status()
2063 def nslc_ids_under_cursor(self, x, y):
2064 ftrack = self.track_to_screen.rev(y)
2065 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2066 return nslc_ids
2068 def marker_under_cursor(self, x, y):
2069 mouset = self.time_projection.rev(x)
2070 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2071 relevant_nslc_ids = None
2072 for marker in self.markers:
2073 if marker.kind not in self.visible_marker_kinds:
2074 continue
2076 if (abs(mouset-marker.tmin) < deltat or
2077 abs(mouset-marker.tmax) < deltat):
2079 if relevant_nslc_ids is None:
2080 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2082 marker_nslc_ids = marker.get_nslc_ids()
2083 if not marker_nslc_ids:
2084 return marker
2086 for nslc_id in marker_nslc_ids:
2087 if nslc_id in relevant_nslc_ids:
2088 return marker
2090 def hoovering(self, x, y):
2091 mouset = self.time_projection.rev(x)
2092 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2093 needupdate = False
2094 haveone = False
2095 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2096 for marker in self.markers:
2097 if marker.kind not in self.visible_marker_kinds:
2098 continue
2100 state = abs(mouset-marker.tmin) < deltat or \
2101 abs(mouset-marker.tmax) < deltat and not haveone
2103 if state:
2104 xstate = False
2106 marker_nslc_ids = marker.get_nslc_ids()
2107 if not marker_nslc_ids:
2108 xstate = True
2110 for nslc in relevant_nslc_ids:
2111 if marker.match_nslc(nslc):
2112 xstate = True
2114 state = xstate
2116 if state:
2117 haveone = True
2118 oldstate = marker.is_alerted()
2119 if oldstate != state:
2120 needupdate = True
2121 marker.set_alerted(state)
2122 if state:
2123 self.message = marker.hoover_message()
2125 if not haveone:
2126 self.message = None
2128 if needupdate:
2129 self.update()
2131 def event(self, event):
2132 if event.type() == qc.QEvent.KeyPress:
2133 self.keyPressEvent(event)
2134 return True
2135 else:
2136 return base.event(self, event)
2138 def keyPressEvent(self, key_event):
2139 self.show_all = False
2140 dt = self.tmax - self.tmin
2141 tmid = (self.tmin + self.tmax) / 2.
2143 key = key_event.key()
2144 try:
2145 keytext = str(key_event.text())
2146 except UnicodeEncodeError:
2147 return
2149 if key == qc.Qt.Key_Space:
2150 self.interrupt_following()
2151 self.set_time_range(self.tmin+dt, self.tmax+dt)
2153 elif key == qc.Qt.Key_Up:
2154 for m in self.selected_markers():
2155 if isinstance(m, PhaseMarker):
2156 if key_event.modifiers() & qc.Qt.ShiftModifier:
2157 p = 0
2158 else:
2159 p = 1 if m.get_polarity() != 1 else None
2160 m.set_polarity(p)
2162 elif key == qc.Qt.Key_Down:
2163 for m in self.selected_markers():
2164 if isinstance(m, PhaseMarker):
2165 if key_event.modifiers() & qc.Qt.ShiftModifier:
2166 p = 0
2167 else:
2168 p = -1 if m.get_polarity() != -1 else None
2169 m.set_polarity(p)
2171 elif key == qc.Qt.Key_B:
2172 dt = self.tmax - self.tmin
2173 self.interrupt_following()
2174 self.set_time_range(self.tmin-dt, self.tmax-dt)
2176 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2177 self.interrupt_following()
2179 tgo = None
2181 class TraceDummy(object):
2182 def __init__(self, marker):
2183 self._marker = marker
2185 @property
2186 def nslc_id(self):
2187 return self._marker.one_nslc()
2189 def marker_to_itrack(marker):
2190 try:
2191 return self.key_to_row.get(
2192 self.gather(TraceDummy(marker)), -1)
2194 except MarkerOneNSLCRequired:
2195 return -1
2197 emarker, pmarkers = self.get_active_markers()
2198 pmarkers = [
2199 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2200 pmarkers.sort(key=lambda m: (
2201 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2203 if key == qc.Qt.Key_Backtab:
2204 pmarkers.reverse()
2206 smarkers = self.selected_markers()
2207 iselected = []
2208 for sm in smarkers:
2209 try:
2210 iselected.append(pmarkers.index(sm))
2211 except ValueError:
2212 pass
2214 if iselected:
2215 icurrent = max(iselected) + 1
2216 else:
2217 icurrent = 0
2219 if icurrent < len(pmarkers):
2220 self.deselect_all()
2221 cmarker = pmarkers[icurrent]
2222 cmarker.selected = True
2223 tgo = cmarker.tmin
2224 if not self.tmin < tgo < self.tmax:
2225 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2227 itrack = marker_to_itrack(cmarker)
2228 if itrack != -1:
2229 if itrack < self.shown_tracks_range[0]:
2230 self.scroll_tracks(
2231 - (self.shown_tracks_range[0] - itrack))
2232 elif self.shown_tracks_range[1] <= itrack:
2233 self.scroll_tracks(
2234 itrack - self.shown_tracks_range[1]+1)
2236 if itrack not in self.track_to_nslc_ids:
2237 self.go_to_selection()
2239 elif keytext in ('p', 'n', 'P', 'N'):
2240 smarkers = self.selected_markers()
2241 tgo = None
2242 dir = str(keytext)
2243 if smarkers:
2244 tmid = smarkers[0].tmin
2245 for smarker in smarkers:
2246 if dir == 'n':
2247 tmid = max(smarker.tmin, tmid)
2248 else:
2249 tmid = min(smarker.tmin, tmid)
2251 tgo = tmid
2253 if dir.lower() == 'n':
2254 for marker in sorted(
2255 self.markers,
2256 key=operator.attrgetter('tmin')):
2258 t = marker.tmin
2259 if t > tmid and \
2260 marker.kind in self.visible_marker_kinds and \
2261 (dir == 'n' or
2262 isinstance(marker, EventMarker)):
2264 self.deselect_all()
2265 marker.selected = True
2266 tgo = t
2267 break
2268 else:
2269 for marker in sorted(
2270 self.markers,
2271 key=operator.attrgetter('tmin'),
2272 reverse=True):
2274 t = marker.tmin
2275 if t < tmid and \
2276 marker.kind in self.visible_marker_kinds and \
2277 (dir == 'p' or
2278 isinstance(marker, EventMarker)):
2279 self.deselect_all()
2280 marker.selected = True
2281 tgo = t
2282 break
2284 if tgo is not None:
2285 self.interrupt_following()
2286 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2288 elif keytext == 'r':
2289 if self.pile.reload_modified():
2290 self.reloaded = True
2292 elif keytext == 'R':
2293 self.setup_snufflings()
2295 elif key == qc.Qt.Key_Backspace:
2296 self.remove_selected_markers()
2298 elif keytext == 'a':
2299 for marker in self.markers:
2300 if ((self.tmin <= marker.tmin <= self.tmax or
2301 self.tmin <= marker.tmax <= self.tmax) and
2302 marker.kind in self.visible_marker_kinds):
2303 marker.selected = True
2304 else:
2305 marker.selected = False
2307 elif keytext == 'A':
2308 for marker in self.markers:
2309 if marker.kind in self.visible_marker_kinds:
2310 marker.selected = True
2312 elif keytext == 'd':
2313 self.deselect_all()
2315 elif keytext == 'E':
2316 self.deactivate_event_marker()
2318 elif keytext == 'e':
2319 markers = self.selected_markers()
2320 event_markers_in_spe = [
2321 marker for marker in markers
2322 if not isinstance(marker, PhaseMarker)]
2324 phase_markers = [
2325 marker for marker in markers
2326 if isinstance(marker, PhaseMarker)]
2328 if len(event_markers_in_spe) == 1:
2329 event_marker = event_markers_in_spe[0]
2330 if not isinstance(event_marker, EventMarker):
2331 nslcs = list(event_marker.nslc_ids)
2332 lat, lon = 0.0, 0.0
2333 old = self.get_active_event()
2334 if len(nslcs) == 1:
2335 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2336 elif old is not None:
2337 lat, lon = old.lat, old.lon
2339 event_marker.convert_to_event_marker(lat, lon)
2341 self.set_active_event_marker(event_marker)
2342 event = event_marker.get_event()
2343 for marker in phase_markers:
2344 marker.set_event(event)
2346 else:
2347 for marker in event_markers_in_spe:
2348 marker.convert_to_event_marker()
2350 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2351 for marker in self.selected_markers():
2352 marker.set_kind(int(keytext))
2353 self.emit_selected_markers()
2355 elif key in fkey_map:
2356 self.handle_fkeys(key)
2358 elif key == qc.Qt.Key_Escape:
2359 if self.picking:
2360 self.stop_picking(0, 0, abort=True)
2362 elif key == qc.Qt.Key_PageDown:
2363 self.scroll_tracks(
2364 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2366 elif key == qc.Qt.Key_PageUp:
2367 self.scroll_tracks(
2368 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2370 elif key == qc.Qt.Key_Plus:
2371 self.zoom_tracks(0., 1.)
2373 elif key == qc.Qt.Key_Minus:
2374 self.zoom_tracks(0., -1.)
2376 elif key == qc.Qt.Key_Equal:
2377 ntracks_shown = self.shown_tracks_range[1] - \
2378 self.shown_tracks_range[0]
2379 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2380 self.zoom_tracks(0., dtracks)
2382 elif key == qc.Qt.Key_Colon:
2383 self.want_input.emit()
2385 elif keytext == 'f':
2386 self.toggle_fullscreen()
2388 elif keytext == 'g':
2389 self.go_to_selection()
2391 elif keytext == 'G':
2392 self.go_to_selection(tight=True)
2394 elif keytext == 'm':
2395 self.toggle_marker_editor()
2397 elif keytext == 'c':
2398 self.toggle_main_controls()
2400 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2401 dir = 1
2402 amount = 1
2403 if key_event.key() == qc.Qt.Key_Left:
2404 dir = -1
2405 if key_event.modifiers() & qc.Qt.ShiftModifier:
2406 amount = 10
2407 self.nudge_selected_markers(dir*amount)
2408 else:
2409 super().keyPressEvent(key_event)
2411 if keytext != '' and keytext in 'degaApPnN':
2412 self.emit_selected_markers()
2414 self.update()
2415 self.update_status()
2417 def handle_fkeys(self, key):
2418 self.set_phase_kind(
2419 self.selected_markers(),
2420 fkey_map[key] + 1)
2421 self.emit_selected_markers()
2423 def emit_selected_markers(self):
2424 ibounds = []
2425 last_selected = False
2426 for imarker, marker in enumerate(self.markers):
2427 this_selected = marker.is_selected()
2428 if this_selected != last_selected:
2429 ibounds.append(imarker)
2431 last_selected = this_selected
2433 if last_selected:
2434 ibounds.append(len(self.markers))
2436 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2437 self.n_selected_markers = sum(
2438 chunk[1] - chunk[0] for chunk in chunks)
2439 self.marker_selection_changed.emit(chunks)
2441 def toggle_marker_editor(self):
2442 self.panel_parent.toggle_marker_editor()
2444 def toggle_main_controls(self):
2445 self.panel_parent.toggle_main_controls()
2447 def nudge_selected_markers(self, npixels):
2448 a, b = self.time_projection.ur
2449 c, d = self.time_projection.xr
2450 for marker in self.selected_markers():
2451 if not isinstance(marker, EventMarker):
2452 marker.tmin += npixels * (d-c)/b
2453 marker.tmax += npixels * (d-c)/b
2455 def toggle_fullscreen(self):
2456 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2457 self.window().windowState() & qc.Qt.WindowMaximized:
2458 self.window().showNormal()
2459 else:
2460 if is_macos:
2461 self.window().showMaximized()
2462 else:
2463 self.window().showFullScreen()
2465 def about(self):
2466 fn = pyrocko.util.data_file('snuffler.png')
2467 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2468 txt = f.read()
2469 label = qw.QLabel(txt % {'logo': fn})
2470 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2471 self.show_doc('About', [label], target='tab')
2473 def help(self):
2474 class MyScrollArea(qw.QScrollArea):
2476 def sizeHint(self):
2477 s = qc.QSize()
2478 s.setWidth(self.widget().sizeHint().width())
2479 s.setHeight(self.widget().sizeHint().height())
2480 return s
2482 with open(pyrocko.util.data_file(
2483 'snuffler_help.html')) as f:
2484 hcheat = qw.QLabel(f.read())
2486 with open(pyrocko.util.data_file(
2487 'snuffler_help_epilog.html')) as f:
2488 hepilog = qw.QLabel(f.read())
2490 for h in [hcheat, hepilog]:
2491 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2492 h.setWordWrap(True)
2494 self.show_doc('Help', [hcheat, hepilog], target='panel')
2496 def show_doc(self, name, labels, target='panel'):
2497 scroller = qw.QScrollArea()
2498 frame = qw.QFrame(scroller)
2499 frame.setLineWidth(0)
2500 layout = qw.QVBoxLayout()
2501 layout.setContentsMargins(0, 0, 0, 0)
2502 layout.setSpacing(0)
2503 frame.setLayout(layout)
2504 scroller.setWidget(frame)
2505 scroller.setWidgetResizable(True)
2506 frame.setBackgroundRole(qg.QPalette.Base)
2507 for h in labels:
2508 h.setParent(frame)
2509 h.setMargin(3)
2510 h.setTextInteractionFlags(
2511 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2512 h.setBackgroundRole(qg.QPalette.Base)
2513 layout.addWidget(h)
2514 h.linkActivated.connect(
2515 self.open_link)
2517 if self.panel_parent is not None:
2518 if target == 'panel':
2519 self.panel_parent.add_panel(
2520 name, scroller, True, volatile=False)
2521 else:
2522 self.panel_parent.add_tab(name, scroller)
2524 def open_link(self, link):
2525 qg.QDesktopServices.openUrl(qc.QUrl(link))
2527 def wheelEvent(self, wheel_event):
2528 self.wheel_pos += wheel_event.angleDelta().y()
2530 n = self.wheel_pos // 120
2531 self.wheel_pos = self.wheel_pos % 120
2532 if n == 0:
2533 return
2535 amount = max(
2536 1.,
2537 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2538 wdelta = amount * n
2540 trmin, trmax = self.track_to_screen.get_in_range()
2541 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2542 / (trmax-trmin)
2544 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2545 self.zoom_tracks(anchor, wdelta)
2546 else:
2547 self.scroll_tracks(-wdelta)
2549 def dragEnterEvent(self, event):
2550 if event.mimeData().hasUrls():
2551 if any(url.toLocalFile() for url in event.mimeData().urls()):
2552 event.setDropAction(qc.Qt.LinkAction)
2553 event.accept()
2555 def dropEvent(self, event):
2556 if event.mimeData().hasUrls():
2557 paths = list(
2558 str(url.toLocalFile()) for url in event.mimeData().urls())
2559 event.acceptProposedAction()
2560 self.load(paths)
2562 def get_phase_name(self, kind):
2563 return self.config.get_phase_name(kind)
2565 def set_phase_kind(self, markers, kind):
2566 phasename = self.get_phase_name(kind)
2568 for marker in markers:
2569 if isinstance(marker, PhaseMarker):
2570 if kind == 10:
2571 marker.convert_to_marker()
2572 else:
2573 marker.set_phasename(phasename)
2574 marker.set_event(self.get_active_event())
2576 elif isinstance(marker, EventMarker):
2577 pass
2579 else:
2580 if kind != 10:
2581 event = self.get_active_event()
2582 marker.convert_to_phase_marker(
2583 event, phasename, None, False)
2585 def set_ntracks(self, ntracks):
2586 if self.ntracks != ntracks:
2587 self.ntracks = ntracks
2588 if self.shown_tracks_range is not None:
2589 l, h = self.shown_tracks_range
2590 else:
2591 l, h = 0, self.ntracks
2593 self.tracks_range_changed.emit(self.ntracks, l, h)
2595 def set_tracks_range(self, range, start=None):
2597 low, high = range
2598 low = min(self.ntracks-1, low)
2599 high = min(self.ntracks, high)
2600 low = max(0, low)
2601 high = max(1, high)
2603 if start is None:
2604 start = float(low)
2606 if self.shown_tracks_range != (low, high):
2607 self.shown_tracks_range = low, high
2608 self.shown_tracks_start = start
2610 self.tracks_range_changed.emit(self.ntracks, low, high)
2612 def scroll_tracks(self, shift):
2613 shown = self.shown_tracks_range
2614 shiftmin = -shown[0]
2615 shiftmax = self.ntracks-shown[1]
2616 shift = max(shiftmin, shift)
2617 shift = min(shiftmax, shift)
2618 shown = shown[0] + shift, shown[1] + shift
2620 self.set_tracks_range((int(shown[0]), int(shown[1])))
2622 self.update()
2624 def zoom_tracks(self, anchor, delta):
2625 ntracks_shown = self.shown_tracks_range[1] \
2626 - self.shown_tracks_range[0]
2628 if (ntracks_shown == 1 and delta <= 0) or \
2629 (ntracks_shown == self.ntracks and delta >= 0):
2630 return
2632 ntracks_shown += int(round(delta))
2633 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2635 u = self.shown_tracks_start
2636 nu = max(0., u-anchor*delta)
2637 nv = nu + ntracks_shown
2638 if nv > self.ntracks:
2639 nu -= nv - self.ntracks
2640 nv -= nv - self.ntracks
2642 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2644 self.ntracks_shown_max = self.shown_tracks_range[1] \
2645 - self.shown_tracks_range[0]
2647 self.update()
2649 def content_time_range(self):
2650 pile = self.get_pile()
2651 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2652 if tmin is None:
2653 tmin = initial_time_range[0]
2654 if tmax is None:
2655 tmax = initial_time_range[1]
2657 return tmin, tmax
2659 def content_deltat_range(self):
2660 pile = self.get_pile()
2662 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2664 if deltatmin is None:
2665 deltatmin = 0.001
2667 if deltatmax is None:
2668 deltatmax = 1000.0
2670 return deltatmin, deltatmax
2672 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2673 if tmax < tmin:
2674 tmin, tmax = tmax, tmin
2676 deltatmin = self.content_deltat_range()[0]
2677 dt = deltatmin * self.visible_length * 0.95
2679 if dt == 0.0:
2680 dt = 1.0
2682 if tight:
2683 if tmax != tmin:
2684 dtm = tmax - tmin
2685 tmin -= dtm*0.1
2686 tmax += dtm*0.1
2687 return tmin, tmax
2688 else:
2689 tcenter = (tmin + tmax) / 2.
2690 tmin = tcenter - 0.5*dt
2691 tmax = tcenter + 0.5*dt
2692 return tmin, tmax
2694 if tmax-tmin < dt:
2695 vmin, vmax = self.get_time_range()
2696 dt = min(vmax - vmin, dt)
2698 tcenter = (tmin+tmax)/2.
2699 etmin, etmax = tmin, tmax
2700 tmin = min(etmin, tcenter - 0.5*dt)
2701 tmax = max(etmax, tcenter + 0.5*dt)
2702 dtm = tmax-tmin
2703 if etmin == tmin:
2704 tmin -= dtm*0.1
2705 if etmax == tmax:
2706 tmax += dtm*0.1
2708 else:
2709 dtm = tmax-tmin
2710 tmin -= dtm*0.1
2711 tmax += dtm*0.1
2713 return tmin, tmax
2715 def go_to_selection(self, tight=False):
2716 markers = self.selected_markers()
2717 if markers:
2718 tmax, tmin = self.content_time_range()
2719 for marker in markers:
2720 tmin = min(tmin, marker.tmin)
2721 tmax = max(tmax, marker.tmax)
2723 else:
2724 if tight:
2725 vmin, vmax = self.get_time_range()
2726 tmin = tmax = (vmin + vmax) / 2.
2727 else:
2728 tmin, tmax = self.content_time_range()
2730 tmin, tmax = self.make_good_looking_time_range(
2731 tmin, tmax, tight=tight)
2733 self.interrupt_following()
2734 self.set_time_range(tmin, tmax)
2735 self.update()
2737 def go_to_time(self, t, tlen=None):
2738 tmax = t
2739 if tlen is not None:
2740 tmax = t+tlen
2741 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2742 self.interrupt_following()
2743 self.set_time_range(tmin, tmax)
2744 self.update()
2746 def go_to_event_by_name(self, name):
2747 for marker in self.markers:
2748 if isinstance(marker, EventMarker):
2749 event = marker.get_event()
2750 if event.name and event.name.lower() == name.lower():
2751 tmin, tmax = self.make_good_looking_time_range(
2752 event.time, event.time)
2754 self.interrupt_following()
2755 self.set_time_range(tmin, tmax)
2757 def printit(self):
2758 from .qt_compat import qprint
2759 printer = qprint.QPrinter()
2760 printer.setOrientation(qprint.QPrinter.Landscape)
2762 dialog = qprint.QPrintDialog(printer, self)
2763 dialog.setWindowTitle('Print')
2765 if dialog.exec_() != qw.QDialog.Accepted:
2766 return
2768 painter = qg.QPainter()
2769 painter.begin(printer)
2770 page = printer.pageRect()
2771 self.drawit(
2772 painter, printmode=False, w=page.width(), h=page.height())
2774 painter.end()
2776 def savesvg(self, fn=None):
2778 if not fn:
2779 fn, _ = qw.QFileDialog.getSaveFileName(
2780 self,
2781 'Save as SVG|PNG',
2782 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2783 'SVG|PNG (*.svg *.png)',
2784 options=qfiledialog_options)
2786 if fn == '':
2787 return
2789 fn = str(fn)
2791 if fn.lower().endswith('.svg'):
2792 try:
2793 w, h = 842, 595
2794 margin = 0.025
2795 m = max(w, h)*margin
2797 generator = qsvg.QSvgGenerator()
2798 generator.setFileName(fn)
2799 generator.setSize(qc.QSize(w, h))
2800 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2802 painter = qg.QPainter()
2803 painter.begin(generator)
2804 self.drawit(painter, printmode=False, w=w, h=h)
2805 painter.end()
2807 except Exception as e:
2808 self.fail('Failed to write SVG file: %s' % str(e))
2810 elif fn.lower().endswith('.png'):
2811 pixmap = self.grab()
2813 try:
2814 pixmap.save(fn)
2816 except Exception as e:
2817 self.fail('Failed to write PNG file: %s' % str(e))
2819 else:
2820 self.fail(
2821 'Unsupported file type: filename must end with ".svg" or '
2822 '".png".')
2824 def paintEvent(self, paint_ev):
2825 '''
2826 Called by QT whenever widget needs to be painted.
2827 '''
2828 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2829 # was called twice (by different threads?), causing segfaults.
2830 if self.in_paint_event:
2831 logger.warning('Blocking reentrant call to paintEvent().')
2832 return
2834 self.in_paint_event = True
2836 painter = qg.QPainter(self)
2838 if self.menuitem_antialias.isChecked():
2839 painter.setRenderHint(qg.QPainter.Antialiasing)
2841 self.drawit(painter)
2843 logger.debug(
2844 'Time spent drawing: '
2845 ' user:%.3f sys:%.3f children_user:%.3f'
2846 ' childred_sys:%.3f elapsed:%.3f' %
2847 (self.timer_draw - self.timer_cutout))
2849 logger.debug(
2850 'Time spent processing:'
2851 ' user:%.3f sys:%.3f children_user:%.3f'
2852 ' childred_sys:%.3f elapsed:%.3f' %
2853 self.timer_cutout.get())
2855 self.time_spent_painting = self.timer_draw.get()[-1]
2856 self.time_last_painted = time.time()
2857 self.in_paint_event = False
2859 def determine_box_styles(self):
2861 traces = list(self.pile.iter_traces())
2862 traces.sort(key=operator.attrgetter('full_id'))
2863 istyle = 0
2864 trace_styles = {}
2865 for itr, tr in enumerate(traces):
2866 if itr > 0:
2867 other = traces[itr-1]
2868 if not (
2869 other.nslc_id == tr.nslc_id
2870 and other.deltat == tr.deltat
2871 and abs(other.tmax - tr.tmin)
2872 < gap_lap_tolerance*tr.deltat):
2874 istyle += 1
2876 trace_styles[tr.full_id, tr.deltat] = istyle
2878 self.trace_styles = trace_styles
2880 def draw_trace_boxes(self, p, time_projection, track_projections):
2882 for v_projection in track_projections.values():
2883 v_projection.set_in_range(0., 1.)
2885 def selector(x):
2886 return x.overlaps(*time_projection.get_in_range())
2888 if self.trace_filter is not None:
2889 def tselector(x):
2890 return selector(x) and self.trace_filter(x)
2892 else:
2893 tselector = selector
2895 traces = list(self.pile.iter_traces(
2896 group_selector=selector, trace_selector=tselector))
2898 traces.sort(key=operator.attrgetter('full_id'))
2900 def drawbox(itrack, istyle, traces):
2901 v_projection = track_projections[itrack]
2902 dvmin = v_projection(0.)
2903 dvmax = v_projection(1.)
2904 dtmin = time_projection.clipped(traces[0].tmin, 0)
2905 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2907 style = box_styles[istyle % len(box_styles)]
2908 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2909 p.fillRect(rect, style.fill_brush)
2910 p.setPen(style.frame_pen)
2911 p.drawRect(rect)
2913 traces_by_style = {}
2914 for itr, tr in enumerate(traces):
2915 gt = self.gather(tr)
2916 if gt not in self.key_to_row:
2917 continue
2919 itrack = self.key_to_row[gt]
2920 if itrack not in track_projections:
2921 continue
2923 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2925 if len(traces) < 500:
2926 drawbox(itrack, istyle, [tr])
2927 else:
2928 if (itrack, istyle) not in traces_by_style:
2929 traces_by_style[itrack, istyle] = []
2930 traces_by_style[itrack, istyle].append(tr)
2932 for (itrack, istyle), traces in traces_by_style.items():
2933 drawbox(itrack, istyle, traces)
2935 def draw_visible_markers(
2936 self, p, vcenter_projection, primary_pen):
2938 try:
2939 markers = self.markers.with_key_in_limited(
2940 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2942 except pyrocko.pile.TooMany:
2943 tmin = self.markers[0].tmin
2944 tmax = self.markers[-1].tmax
2945 umin_view, umax_view = self.time_projection.get_out_range()
2946 umin = max(umin_view, self.time_projection(tmin))
2947 umax = min(umax_view, self.time_projection(tmax))
2948 v0, _ = vcenter_projection.get_out_range()
2949 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2951 p.save()
2953 pen = qg.QPen(primary_pen)
2954 pen.setWidth(2)
2955 pen.setStyle(qc.Qt.DotLine)
2956 # pat = [5., 3.]
2957 # pen.setDashPattern(pat)
2958 p.setPen(pen)
2960 if self.n_selected_markers == len(self.markers):
2961 s_selected = ' (all selected)'
2962 elif self.n_selected_markers > 0:
2963 s_selected = ' (%i selected)' % self.n_selected_markers
2964 else:
2965 s_selected = ''
2967 draw_label(
2968 p, umin+10., v0-10.,
2969 '%i Markers' % len(self.markers) + s_selected,
2970 label_bg, 'LB')
2972 line = qc.QLineF(umin, v0, umax, v0)
2973 p.drawLine(line)
2974 p.restore()
2976 return
2978 for marker in markers:
2979 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2980 and marker.kind in self.visible_marker_kinds:
2982 marker.draw(
2983 p, self.time_projection, vcenter_projection,
2984 with_label=True)
2986 def get_squirrel(self):
2987 try:
2988 return self.pile._squirrel
2989 except AttributeError:
2990 return None
2992 def draw_coverage(self, p, time_projection, track_projections):
2993 sq = self.get_squirrel()
2994 if sq is None:
2995 return
2997 def drawbox(itrack, tmin, tmax, style):
2998 v_projection = track_projections[itrack]
2999 dvmin = v_projection(0.)
3000 dvmax = v_projection(1.)
3001 dtmin = time_projection.clipped(tmin, 0)
3002 dtmax = time_projection.clipped(tmax, 1)
3004 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3005 p.fillRect(rect, style.fill_brush)
3006 p.setPen(style.frame_pen)
3007 p.drawRect(rect)
3009 pattern_list = []
3010 pattern_to_itrack = {}
3011 for key in self.track_keys:
3012 itrack = self.key_to_row[key]
3013 if itrack not in track_projections:
3014 continue
3016 pattern = self.track_patterns[itrack]
3017 pattern_to_itrack[tuple(pattern)] = itrack
3018 pattern_list.append(tuple(pattern))
3020 vmin, vmax = self.get_time_range()
3022 for kind in ['waveform', 'waveform_promise']:
3023 for coverage in sq.get_coverage(
3024 kind, vmin, vmax, pattern_list, limit=500):
3025 itrack = pattern_to_itrack[coverage.pattern.nslc]
3027 if coverage.changes is None:
3028 drawbox(
3029 itrack, coverage.tmin, coverage.tmax,
3030 box_styles_coverage[kind][0])
3031 else:
3032 t = None
3033 pcount = 0
3034 for tb, count in coverage.changes:
3035 if t is not None and tb > t:
3036 if pcount > 0:
3037 drawbox(
3038 itrack, t, tb,
3039 box_styles_coverage[kind][
3040 min(len(box_styles_coverage)-1,
3041 pcount)])
3043 t = tb
3044 pcount = count
3046 def drawit(self, p, printmode=False, w=None, h=None):
3047 '''
3048 This performs the actual drawing.
3049 '''
3051 self.timer_draw.start()
3052 show_boxes = self.menuitem_showboxes.isChecked()
3053 sq = self.get_squirrel()
3055 if self.gather is None:
3056 self.set_gathering()
3058 if self.pile_has_changed:
3060 if not self.sortingmode_change_delayed():
3061 self.sortingmode_change()
3063 if show_boxes and sq is None:
3064 self.determine_box_styles()
3066 self.pile_has_changed = False
3068 if h is None:
3069 h = float(self.height())
3070 if w is None:
3071 w = float(self.width())
3073 if printmode:
3074 primary_color = (0, 0, 0)
3075 else:
3076 primary_color = pyrocko.plot.tango_colors['aluminium5']
3078 primary_pen = qg.QPen(qg.QColor(*primary_color))
3080 ax_h = float(self.ax_height)
3082 vbottom_ax_projection = Projection()
3083 vtop_ax_projection = Projection()
3084 vcenter_projection = Projection()
3086 self.time_projection.set_out_range(0., w)
3087 vbottom_ax_projection.set_out_range(h-ax_h, h)
3088 vtop_ax_projection.set_out_range(0., ax_h)
3089 vcenter_projection.set_out_range(ax_h, h-ax_h)
3090 vcenter_projection.set_in_range(0., 1.)
3091 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3093 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3094 track_projections = {}
3095 for i in range(*self.shown_tracks_range):
3096 proj = Projection()
3097 proj.set_out_range(
3098 self.track_to_screen(i+0.05),
3099 self.track_to_screen(i+1.-0.05))
3101 track_projections[i] = proj
3103 if self.tmin > self.tmax:
3104 return
3106 self.time_projection.set_in_range(self.tmin, self.tmax)
3107 vbottom_ax_projection.set_in_range(0, ax_h)
3109 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3111 yscaler = pyrocko.plot.AutoScaler()
3113 p.setPen(primary_pen)
3115 font = qg.QFont()
3116 font.setBold(True)
3118 axannotfont = qg.QFont()
3119 axannotfont.setBold(True)
3120 axannotfont.setPointSize(8)
3122 processed_traces = self.prepare_cutout2(
3123 self.tmin, self.tmax,
3124 trace_selector=self.trace_selector,
3125 degap=self.menuitem_degap.isChecked(),
3126 demean=self.menuitem_demean.isChecked())
3128 if not printmode and show_boxes:
3129 if (self.view_mode is ViewMode.Wiggle) \
3130 or (self.view_mode is ViewMode.Waterfall
3131 and not processed_traces):
3133 if sq is None:
3134 self.draw_trace_boxes(
3135 p, self.time_projection, track_projections)
3137 else:
3138 self.draw_coverage(
3139 p, self.time_projection, track_projections)
3141 p.setFont(font)
3142 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3144 color_lookup = dict(
3145 [(k, i) for (i, k) in enumerate(self.color_keys)])
3147 self.track_to_nslc_ids = {}
3148 nticks = 0
3149 annot_labels = []
3151 if self.view_mode is ViewMode.Waterfall and processed_traces:
3152 waterfall = self.waterfall
3153 waterfall.set_time_range(self.tmin, self.tmax)
3154 waterfall.set_traces(processed_traces)
3155 waterfall.set_cmap(self.waterfall_cmap)
3156 waterfall.set_integrate(self.waterfall_integrate)
3157 waterfall.set_clip(
3158 self.waterfall_clip_min, self.waterfall_clip_max)
3159 waterfall.show_absolute_values(
3160 self.waterfall_show_absolute)
3162 rect = qc.QRectF(
3163 0, self.ax_height,
3164 self.width(), self.height() - self.ax_height*2
3165 )
3166 waterfall.draw_waterfall(p, rect=rect)
3168 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3169 show_scales = self.menuitem_showscalerange.isChecked() \
3170 or self.menuitem_showscaleaxis.isChecked()
3172 fm = qg.QFontMetrics(axannotfont, p.device())
3173 trackheight = self.track_to_screen(1.-0.05) \
3174 - self.track_to_screen(0.05)
3176 nlinesavail = trackheight/float(fm.lineSpacing())
3178 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3179 if self.menuitem_showscaleaxis.isChecked() \
3180 else 15
3182 yscaler = pyrocko.plot.AutoScaler(
3183 no_exp_interval=(-3, 2), approx_ticks=nticks,
3184 snap=show_scales
3185 and not self.menuitem_showscaleaxis.isChecked())
3187 data_ranges = pyrocko.trace.minmax(
3188 processed_traces,
3189 key=self.scaling_key,
3190 mode=self.scaling_base[0],
3191 outer_mode=self.scaling_base[1])
3193 if not self.menuitem_fixscalerange.isChecked():
3194 self.old_data_ranges = data_ranges
3195 else:
3196 data_ranges.update(self.old_data_ranges)
3198 self.apply_scaling_hooks(data_ranges)
3200 trace_to_itrack = {}
3201 track_scaling_keys = {}
3202 track_scaling_colors = {}
3203 for trace in processed_traces:
3204 gt = self.gather(trace)
3205 if gt not in self.key_to_row:
3206 continue
3208 itrack = self.key_to_row[gt]
3209 if itrack not in track_projections:
3210 continue
3212 trace_to_itrack[trace] = itrack
3214 if itrack not in self.track_to_nslc_ids:
3215 self.track_to_nslc_ids[itrack] = set()
3217 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3219 if itrack not in track_scaling_keys:
3220 track_scaling_keys[itrack] = set()
3222 scaling_key = self.scaling_key(trace)
3223 track_scaling_keys[itrack].add(scaling_key)
3225 color = pyrocko.plot.color(
3226 color_lookup[self.color_gather(trace)])
3228 k = itrack, scaling_key
3229 if k not in track_scaling_colors \
3230 and self.menuitem_colortraces.isChecked():
3231 track_scaling_colors[k] = color
3232 else:
3233 track_scaling_colors[k] = primary_color
3235 # y axes, zero lines
3236 trace_projections = {}
3237 for itrack in list(track_projections.keys()):
3238 if itrack not in track_scaling_keys:
3239 continue
3240 uoff = 0
3241 for scaling_key in track_scaling_keys[itrack]:
3242 data_range = data_ranges[scaling_key]
3243 dymin, dymax = data_range
3244 ymin, ymax, yinc = yscaler.make_scale(
3245 (dymin/self.gain, dymax/self.gain))
3246 iexp = yscaler.make_exp(yinc)
3247 factor = 10**iexp
3248 trace_projection = track_projections[itrack].copy()
3249 trace_projection.set_in_range(ymax, ymin)
3250 trace_projections[itrack, scaling_key] = \
3251 trace_projection
3252 umin, umax = self.time_projection.get_out_range()
3253 vmin, vmax = trace_projection.get_out_range()
3254 umax_zeroline = umax
3255 uoffnext = uoff
3257 if show_scales:
3258 pen = qg.QPen(primary_pen)
3259 k = itrack, scaling_key
3260 if k in track_scaling_colors:
3261 c = qg.QColor(*track_scaling_colors[
3262 itrack, scaling_key])
3264 pen.setColor(c)
3266 p.setPen(pen)
3267 if nlinesavail > 3:
3268 if self.menuitem_showscaleaxis.isChecked():
3269 ymin_annot = math.ceil(ymin/yinc)*yinc
3270 ny_annot = int(
3271 math.floor(ymax/yinc)
3272 - math.ceil(ymin/yinc)) + 1
3274 for iy_annot in range(ny_annot):
3275 y = ymin_annot + iy_annot*yinc
3276 v = trace_projection(y)
3277 line = qc.QLineF(
3278 umax-10-uoff, v, umax-uoff, v)
3280 p.drawLine(line)
3281 if iy_annot == ny_annot - 1 \
3282 and iexp != 0:
3283 sexp = ' × ' \
3284 '10<sup>%i</sup>' % iexp
3285 else:
3286 sexp = ''
3288 snum = num_to_html(y/factor)
3289 lab = Label(
3290 p,
3291 umax-20-uoff,
3292 v, '%s%s' % (snum, sexp),
3293 label_bg=None,
3294 anchor='MR',
3295 font=axannotfont,
3296 color=c)
3298 uoffnext = max(
3299 lab.rect.width()+30., uoffnext)
3301 annot_labels.append(lab)
3302 if y == 0.:
3303 umax_zeroline = \
3304 umax - 20 \
3305 - lab.rect.width() - 10 \
3306 - uoff
3307 else:
3308 if not show_boxes:
3309 qpoints = make_QPolygonF(
3310 [umax-20-uoff,
3311 umax-10-uoff,
3312 umax-10-uoff,
3313 umax-20-uoff],
3314 [vmax, vmax, vmin, vmin])
3315 p.drawPolyline(qpoints)
3317 snum = num_to_html(ymin)
3318 labmin = Label(
3319 p, umax-15-uoff, vmax, snum,
3320 label_bg=None,
3321 anchor='BR',
3322 font=axannotfont,
3323 color=c)
3325 annot_labels.append(labmin)
3326 snum = num_to_html(ymax)
3327 labmax = Label(
3328 p, umax-15-uoff, vmin, snum,
3329 label_bg=None,
3330 anchor='TR',
3331 font=axannotfont,
3332 color=c)
3334 annot_labels.append(labmax)
3336 for lab in (labmin, labmax):
3337 uoffnext = max(
3338 lab.rect.width()+10., uoffnext)
3340 if self.menuitem_showzeroline.isChecked():
3341 v = trace_projection(0.)
3342 if vmin <= v <= vmax:
3343 line = qc.QLineF(umin, v, umax_zeroline, v)
3344 p.drawLine(line)
3346 uoff = uoffnext
3348 p.setFont(font)
3349 p.setPen(primary_pen)
3350 for trace in processed_traces:
3351 if self.view_mode is not ViewMode.Wiggle:
3352 break
3354 if trace not in trace_to_itrack:
3355 continue
3357 itrack = trace_to_itrack[trace]
3358 scaling_key = self.scaling_key(trace)
3359 trace_projection = trace_projections[
3360 itrack, scaling_key]
3362 vdata = trace_projection(trace.get_ydata())
3364 udata_min = float(self.time_projection(trace.tmin))
3365 udata_max = float(self.time_projection(
3366 trace.tmin+trace.deltat*(vdata.size-1)))
3367 udata = num.linspace(udata_min, udata_max, vdata.size)
3369 qpoints = make_QPolygonF(udata, vdata)
3371 umin, umax = self.time_projection.get_out_range()
3372 vmin, vmax = trace_projection.get_out_range()
3374 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3376 if self.menuitem_cliptraces.isChecked():
3377 p.setClipRect(trackrect)
3379 if self.menuitem_colortraces.isChecked():
3380 color = pyrocko.plot.color(
3381 color_lookup[self.color_gather(trace)])
3382 pen = qg.QPen(qg.QColor(*color), 1)
3383 p.setPen(pen)
3385 p.drawPolyline(qpoints)
3387 if self.floating_marker:
3388 self.floating_marker.draw_trace(
3389 self, p, trace,
3390 self.time_projection, trace_projection, 1.0)
3392 for marker in self.markers.with_key_in(
3393 self.tmin - self.markers_deltat_max,
3394 self.tmax):
3396 if marker.tmin < self.tmax \
3397 and self.tmin < marker.tmax \
3398 and marker.kind \
3399 in self.visible_marker_kinds:
3400 marker.draw_trace(
3401 self, p, trace, self.time_projection,
3402 trace_projection, 1.0)
3404 p.setPen(primary_pen)
3406 if self.menuitem_cliptraces.isChecked():
3407 p.setClipRect(0, 0, int(w), int(h))
3409 if self.floating_marker:
3410 self.floating_marker.draw(
3411 p, self.time_projection, vcenter_projection)
3413 self.draw_visible_markers(
3414 p, vcenter_projection, primary_pen)
3416 p.setPen(primary_pen)
3417 while font.pointSize() > 2:
3418 fm = qg.QFontMetrics(font, p.device())
3419 trackheight = self.track_to_screen(1.-0.05) \
3420 - self.track_to_screen(0.05)
3421 nlinesavail = trackheight/float(fm.lineSpacing())
3422 if nlinesavail > 1:
3423 break
3425 font.setPointSize(font.pointSize()-1)
3427 p.setFont(font)
3428 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3430 for key in self.track_keys:
3431 itrack = self.key_to_row[key]
3432 if itrack in track_projections:
3433 plabel = ' '.join(
3434 [str(x) for x in key if x is not None])
3435 lx = 10
3436 ly = self.track_to_screen(itrack+0.5)
3438 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3439 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3440 continue
3442 contains_cursor = \
3443 self.track_to_screen(itrack) \
3444 < mouse_pos.y() \
3445 < self.track_to_screen(itrack+1)
3447 if not contains_cursor:
3448 continue
3450 font_large = p.font()
3451 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3452 p.setFont(font_large)
3453 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3454 p.setFont(font)
3456 for lab in annot_labels:
3457 lab.draw()
3459 self.timer_draw.stop()
3461 def see_data_params(self):
3463 min_deltat = self.content_deltat_range()[0]
3465 # determine padding and downampling requirements
3466 if self.lowpass is not None:
3467 deltat_target = 1./self.lowpass * 0.25
3468 ndecimate = min(
3469 50,
3470 max(1, int(round(deltat_target / min_deltat))))
3471 tpad = 1./self.lowpass * 2.
3472 else:
3473 ndecimate = 1
3474 tpad = min_deltat*5.
3476 if self.highpass is not None:
3477 tpad = max(1./self.highpass * 2., tpad)
3479 nsee_points_per_trace = 5000*10
3480 tsee = ndecimate*nsee_points_per_trace*min_deltat
3482 return ndecimate, tpad, tsee
3484 def clean_update(self):
3485 self.cached_processed_traces = None
3486 self.update()
3488 def get_adequate_tpad(self):
3489 tpad = 0.
3490 for f in [self.highpass, self.lowpass]:
3491 if f is not None:
3492 tpad = max(tpad, 1.0/f)
3494 for snuffling in self.snufflings:
3495 if snuffling._post_process_hook_enabled \
3496 or snuffling._pre_process_hook_enabled:
3498 tpad = max(tpad, snuffling.get_tpad())
3500 return tpad
3502 def prepare_cutout2(
3503 self, tmin, tmax, trace_selector=None, degap=True,
3504 demean=True, nmax=6000):
3506 if self.pile.is_empty():
3507 return []
3509 nmax = self.visible_length
3511 self.timer_cutout.start()
3513 tsee = tmax-tmin
3514 min_deltat_wo_decimate = tsee/nmax
3515 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3517 min_deltat_allow = min_deltat_wo_decimate
3518 if self.lowpass is not None:
3519 target_deltat_lp = 0.25/self.lowpass
3520 if target_deltat_lp > min_deltat_wo_decimate:
3521 min_deltat_allow = min_deltat_w_decimate
3523 min_deltat_allow = math.exp(
3524 int(math.floor(math.log(min_deltat_allow))))
3526 tmin_ = tmin
3527 tmax_ = tmax
3529 # fetch more than needed?
3530 if self.menuitem_liberal_fetch.isChecked():
3531 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3532 tmin = math.floor(tmin/tlen) * tlen
3533 tmax = math.ceil(tmax/tlen) * tlen
3535 fft_filtering = self.menuitem_fft_filtering.isChecked()
3536 lphp = self.menuitem_lphp.isChecked()
3537 ads = self.menuitem_allowdownsampling.isChecked()
3539 tpad = self.get_adequate_tpad()
3540 tpad = max(tpad, tsee)
3542 # state vector to decide if cached traces can be used
3543 vec = (
3544 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3545 self.highpass, fft_filtering, lphp,
3546 min_deltat_allow, self.rotate, self.shown_tracks_range,
3547 ads, self.pile.get_update_count())
3549 if (self.cached_vec
3550 and self.cached_vec[0] <= vec[0]
3551 and vec[1] <= self.cached_vec[1]
3552 and vec[2:] == self.cached_vec[2:]
3553 and not (self.reloaded or self.menuitem_watch.isChecked())
3554 and self.cached_processed_traces is not None):
3556 logger.debug('Using cached traces')
3557 processed_traces = self.cached_processed_traces
3559 else:
3560 processed_traces = []
3561 if self.pile.deltatmax >= min_deltat_allow:
3563 if isinstance(self.pile, pyrocko.pile.Pile):
3564 def group_selector(gr):
3565 return gr.deltatmax >= min_deltat_allow
3567 kwargs = dict(group_selector=group_selector)
3568 else:
3569 kwargs = {}
3571 if trace_selector is not None:
3572 def trace_selectorx(tr):
3573 return tr.deltat >= min_deltat_allow \
3574 and trace_selector(tr)
3575 else:
3576 def trace_selectorx(tr):
3577 return tr.deltat >= min_deltat_allow
3579 for traces in self.pile.chopper(
3580 tmin=tmin, tmax=tmax, tpad=tpad,
3581 want_incomplete=True,
3582 degap=degap,
3583 maxgap=gap_lap_tolerance,
3584 maxlap=gap_lap_tolerance,
3585 keep_current_files_open=True,
3586 trace_selector=trace_selectorx,
3587 accessor_id=id(self),
3588 snap=(math.floor, math.ceil),
3589 include_last=True, **kwargs):
3591 if demean:
3592 for tr in traces:
3593 if (tr.meta and tr.meta.get('tabu', False)):
3594 continue
3595 y = tr.get_ydata()
3596 tr.set_ydata(y - num.mean(y))
3598 traces = self.pre_process_hooks(traces)
3600 for trace in traces:
3602 if not (trace.meta
3603 and trace.meta.get('tabu', False)):
3605 if fft_filtering:
3606 but = pyrocko.response.ButterworthResponse
3607 multres = pyrocko.response.MultiplyResponse
3608 if self.lowpass is not None \
3609 or self.highpass is not None:
3611 it = num.arange(
3612 trace.data_len(), dtype=float)
3613 detr_data, m, b = detrend(
3614 it, trace.get_ydata())
3616 trace.set_ydata(detr_data)
3618 freqs, fdata = trace.spectrum(
3619 pad_to_pow2=True, tfade=None)
3621 nfreqs = fdata.size
3623 key = (trace.deltat, nfreqs)
3625 if key not in self.tf_cache:
3626 resps = []
3627 if self.lowpass is not None:
3628 resps.append(but(
3629 order=4,
3630 corner=self.lowpass,
3631 type='low'))
3633 if self.highpass is not None:
3634 resps.append(but(
3635 order=4,
3636 corner=self.highpass,
3637 type='high'))
3639 resp = multres(resps)
3640 self.tf_cache[key] = \
3641 resp.evaluate(freqs)
3643 filtered_data = num.fft.irfft(
3644 fdata*self.tf_cache[key]
3645 )[:trace.data_len()]
3647 retrended_data = retrend(
3648 it, filtered_data, m, b)
3650 trace.set_ydata(retrended_data)
3652 else:
3654 if ads and self.lowpass is not None:
3655 while trace.deltat \
3656 < min_deltat_wo_decimate:
3658 trace.downsample(2, demean=False)
3660 fmax = 0.5/trace.deltat
3661 if not lphp and (
3662 self.lowpass is not None
3663 and self.highpass is not None
3664 and self.lowpass < fmax
3665 and self.highpass < fmax
3666 and self.highpass < self.lowpass):
3668 trace.bandpass(
3669 2, self.highpass, self.lowpass)
3670 else:
3671 if self.lowpass is not None:
3672 if self.lowpass < 0.5/trace.deltat:
3673 trace.lowpass(
3674 4, self.lowpass,
3675 demean=False)
3677 if self.highpass is not None:
3678 if self.lowpass is None \
3679 or self.highpass \
3680 < self.lowpass:
3682 if self.highpass < \
3683 0.5/trace.deltat:
3684 trace.highpass(
3685 4, self.highpass,
3686 demean=False)
3688 processed_traces.append(trace)
3690 if self.rotate != 0.0:
3691 phi = self.rotate/180.*math.pi
3692 cphi = math.cos(phi)
3693 sphi = math.sin(phi)
3694 for a in processed_traces:
3695 for b in processed_traces:
3696 if (a.network == b.network
3697 and a.station == b.station
3698 and a.location == b.location
3699 and ((a.channel.lower().endswith('n')
3700 and b.channel.lower().endswith('e'))
3701 or (a.channel.endswith('1')
3702 and b.channel.endswith('2')))
3703 and abs(a.deltat-b.deltat) < a.deltat*0.001
3704 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3705 len(a.get_ydata()) == len(b.get_ydata())):
3707 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3708 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3709 a.set_ydata(aydata)
3710 b.set_ydata(bydata)
3712 processed_traces = self.post_process_hooks(processed_traces)
3714 self.cached_processed_traces = processed_traces
3715 self.cached_vec = vec
3717 chopped_traces = []
3718 for trace in processed_traces:
3719 chop_tmin = tmin_ - trace.deltat*4
3720 chop_tmax = tmax_ + trace.deltat*4
3722 try:
3723 ctrace = trace.chop(
3724 chop_tmin, chop_tmax,
3725 inplace=False)
3727 except pyrocko.trace.NoData:
3728 continue
3730 if ctrace.data_len() < 2:
3731 continue
3733 chopped_traces.append(ctrace)
3735 self.timer_cutout.stop()
3736 return chopped_traces
3738 def pre_process_hooks(self, traces):
3739 for snuffling in self.snufflings:
3740 if snuffling._pre_process_hook_enabled:
3741 traces = snuffling.pre_process_hook(traces)
3743 return traces
3745 def post_process_hooks(self, traces):
3746 for snuffling in self.snufflings:
3747 if snuffling._post_process_hook_enabled:
3748 traces = snuffling.post_process_hook(traces)
3750 return traces
3752 def visible_length_change(self, ignore=None):
3753 for menuitem, vlen in self.menuitems_visible_length:
3754 if menuitem.isChecked():
3755 self.visible_length = vlen
3757 def scaling_base_change(self, ignore=None):
3758 for menuitem, scaling_base in self.menuitems_scaling_base:
3759 if menuitem.isChecked():
3760 self.scaling_base = scaling_base
3762 def scalingmode_change(self, ignore=None):
3763 for menuitem, scaling_key in self.menuitems_scaling:
3764 if menuitem.isChecked():
3765 self.scaling_key = scaling_key
3766 self.update()
3768 def apply_scaling_hooks(self, data_ranges):
3769 for k in sorted(self.scaling_hooks.keys()):
3770 hook = self.scaling_hooks[k]
3771 hook(data_ranges)
3773 def viewmode_change(self, ignore=True):
3774 for item, mode in self.menuitems_viewmode:
3775 if item.isChecked():
3776 self.view_mode = mode
3777 break
3778 else:
3779 raise AttributeError('unknown view mode')
3781 items_waterfall_disabled = (
3782 self.menuitem_showscaleaxis,
3783 self.menuitem_showscalerange,
3784 self.menuitem_showzeroline,
3785 self.menuitem_colortraces,
3786 self.menuitem_cliptraces,
3787 *(itm[0] for itm in self.menuitems_visible_length)
3788 )
3790 if self.view_mode is ViewMode.Waterfall:
3791 self.parent().show_colorbar_ctrl(True)
3792 self.parent().show_gain_ctrl(False)
3794 for item in items_waterfall_disabled:
3795 item.setDisabled(True)
3797 self.visible_length = 180.
3798 else:
3799 self.parent().show_colorbar_ctrl(False)
3800 self.parent().show_gain_ctrl(True)
3802 for item in items_waterfall_disabled:
3803 item.setDisabled(False)
3805 self.visible_length_change()
3806 self.update()
3808 def set_scaling_hook(self, k, hook):
3809 self.scaling_hooks[k] = hook
3811 def remove_scaling_hook(self, k):
3812 del self.scaling_hooks[k]
3814 def remove_scaling_hooks(self):
3815 self.scaling_hooks = {}
3817 def s_sortingmode_change(self, ignore=None):
3818 for menuitem, valfunc in self.menuitems_ssorting:
3819 if menuitem.isChecked():
3820 self._ssort = valfunc
3822 self.sortingmode_change()
3824 def sortingmode_change(self, ignore=None):
3825 for menuitem, (gather, color) in self.menuitems_sorting:
3826 if menuitem.isChecked():
3827 self.set_gathering(gather, color)
3829 self.sortingmode_change_time = time.time()
3831 def lowpass_change(self, value, ignore=None):
3832 self.lowpass = value
3833 self.passband_check()
3834 self.tf_cache = {}
3835 self.update()
3837 def highpass_change(self, value, ignore=None):
3838 self.highpass = value
3839 self.passband_check()
3840 self.tf_cache = {}
3841 self.update()
3843 def passband_check(self):
3844 if self.highpass and self.lowpass \
3845 and self.highpass >= self.lowpass:
3847 self.message = 'Corner frequency of highpass larger than ' \
3848 'corner frequency of lowpass! I will now ' \
3849 'deactivate the highpass.'
3851 self.update_status()
3852 else:
3853 oldmess = self.message
3854 self.message = None
3855 if oldmess is not None:
3856 self.update_status()
3858 def gain_change(self, value, ignore):
3859 self.gain = value
3860 self.update()
3862 def rot_change(self, value, ignore):
3863 self.rotate = value
3864 self.update()
3866 def waterfall_cmap_change(self, cmap):
3867 self.waterfall_cmap = cmap
3868 self.update()
3870 def waterfall_clip_change(self, clip_min, clip_max):
3871 self.waterfall_clip_min = clip_min
3872 self.waterfall_clip_max = clip_max
3873 self.update()
3875 def waterfall_show_absolute_change(self, toggle):
3876 self.waterfall_show_absolute = toggle
3877 self.update()
3879 def waterfall_set_integrate(self, toggle):
3880 self.waterfall_integrate = toggle
3881 self.update()
3883 def set_selected_markers(self, markers):
3884 '''
3885 Set a list of markers selected
3887 :param markers: list of markers
3888 '''
3889 self.deselect_all()
3890 for m in markers:
3891 m.selected = True
3893 self.update()
3895 def deselect_all(self):
3896 for marker in self.markers:
3897 marker.selected = False
3899 def animate_picking(self):
3900 point = self.mapFromGlobal(qg.QCursor.pos())
3901 self.update_picking(point.x(), point.y(), doshift=True)
3903 def get_nslc_ids_for_track(self, ftrack):
3904 itrack = int(ftrack)
3905 return self.track_to_nslc_ids.get(itrack, [])
3907 def stop_picking(self, x, y, abort=False):
3908 if self.picking:
3909 self.update_picking(x, y, doshift=False)
3910 self.picking = None
3911 self.picking_down = None
3912 self.picking_timer.stop()
3913 self.picking_timer = None
3914 if not abort:
3915 self.add_marker(self.floating_marker)
3916 self.floating_marker.selected = True
3917 self.emit_selected_markers()
3919 self.floating_marker = None
3921 def start_picking(self, ignore):
3923 if not self.picking:
3924 self.deselect_all()
3925 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3926 point = self.mapFromGlobal(qg.QCursor.pos())
3928 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3929 self.picking.setGeometry(
3930 gpoint.x(), gpoint.y(), 1, self.height())
3931 t = self.time_projection.rev(point.x())
3933 ftrack = self.track_to_screen.rev(point.y())
3934 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3935 self.floating_marker = Marker(nslc_ids, t, t)
3936 self.floating_marker.selected = True
3938 self.picking_timer = qc.QTimer()
3939 self.picking_timer.timeout.connect(
3940 self.animate_picking)
3942 self.picking_timer.setInterval(50)
3943 self.picking_timer.start()
3945 def update_picking(self, x, y, doshift=False):
3946 if self.picking:
3947 mouset = self.time_projection.rev(x)
3948 dt = 0.0
3949 if mouset < self.tmin or mouset > self.tmax:
3950 if mouset < self.tmin:
3951 dt = -(self.tmin - mouset)
3952 else:
3953 dt = mouset - self.tmax
3954 ddt = self.tmax-self.tmin
3955 dt = max(dt, -ddt/10.)
3956 dt = min(dt, ddt/10.)
3958 x0 = x
3959 if self.picking_down is not None:
3960 x0 = self.time_projection(self.picking_down[0])
3962 w = abs(x-x0)
3963 x0 = min(x0, x)
3965 tmin, tmax = (
3966 self.time_projection.rev(x0),
3967 self.time_projection.rev(x0+w))
3969 tmin, tmax = (
3970 max(working_system_time_range[0], tmin),
3971 min(working_system_time_range[1], tmax))
3973 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3975 self.picking.setGeometry(
3976 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3978 ftrack = self.track_to_screen.rev(y)
3979 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3980 self.floating_marker.set(nslc_ids, tmin, tmax)
3982 if dt != 0.0 and doshift:
3983 self.interrupt_following()
3984 self.set_time_range(self.tmin+dt, self.tmax+dt)
3986 self.update()
3988 def update_status(self):
3990 if self.message is None:
3991 point = self.mapFromGlobal(qg.QCursor.pos())
3993 mouse_t = self.time_projection.rev(point.x())
3994 if not is_working_time(mouse_t):
3995 return
3997 if self.floating_marker:
3998 tmi, tma = (
3999 self.floating_marker.tmin,
4000 self.floating_marker.tmax)
4002 tt, ms = gmtime_x(tmi)
4004 if tmi == tma:
4005 message = mystrftime(
4006 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4007 tt=tt, milliseconds=ms)
4008 else:
4009 srange = '%g s' % (tma-tmi)
4010 message = mystrftime(
4011 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4012 tt=tt, milliseconds=ms)
4013 else:
4014 tt, ms = gmtime_x(mouse_t)
4016 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4017 else:
4018 message = self.message
4020 sb = self.window().statusBar()
4021 sb.clearMessage()
4022 sb.showMessage(message)
4024 def set_sortingmode_change_delay_time(self, dt):
4025 self.sortingmode_change_delay_time = dt
4027 def sortingmode_change_delayed(self):
4028 now = time.time()
4029 return (
4030 self.sortingmode_change_delay_time is not None
4031 and now - self.sortingmode_change_time
4032 < self.sortingmode_change_delay_time)
4034 def set_visible_marker_kinds(self, kinds):
4035 self.deselect_all()
4036 self.visible_marker_kinds = tuple(kinds)
4037 self.emit_selected_markers()
4039 def following(self):
4040 return self.follow_timer is not None \
4041 and not self.following_interrupted()
4043 def interrupt_following(self):
4044 self.interactive_range_change_time = time.time()
4046 def following_interrupted(self, now=None):
4047 if now is None:
4048 now = time.time()
4049 return now - self.interactive_range_change_time \
4050 < self.interactive_range_change_delay_time
4052 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4053 if tmax_start is None:
4054 tmax_start = time.time()
4055 self.show_all = False
4056 self.follow_time = tlen
4057 self.follow_timer = qc.QTimer(self)
4058 self.follow_timer.timeout.connect(
4059 self.follow_update)
4060 self.follow_timer.setInterval(interval)
4061 self.follow_timer.start()
4062 self.follow_started = time.time()
4063 self.follow_lapse = lapse
4064 self.follow_tshift = self.follow_started - tmax_start
4065 self.interactive_range_change_time = 0.0
4067 def unfollow(self):
4068 if self.follow_timer is not None:
4069 self.follow_timer.stop()
4070 self.follow_timer = None
4071 self.interactive_range_change_time = 0.0
4073 def follow_update(self):
4074 rnow = time.time()
4075 if self.follow_lapse is None:
4076 now = rnow
4077 else:
4078 now = self.follow_started + (rnow - self.follow_started) \
4079 * self.follow_lapse
4081 if self.following_interrupted(rnow):
4082 return
4083 self.set_time_range(
4084 now-self.follow_time-self.follow_tshift,
4085 now-self.follow_tshift)
4087 self.update()
4089 def myclose(self, return_tag=''):
4090 self.return_tag = return_tag
4091 self.window().close()
4093 def cleanup(self):
4094 self.about_to_close.emit()
4095 self.timer.stop()
4096 if self.follow_timer is not None:
4097 self.follow_timer.stop()
4099 for snuffling in list(self.snufflings):
4100 self.remove_snuffling(snuffling)
4102 def set_error_message(self, key, value):
4103 if value is None:
4104 if key in self.error_messages:
4105 del self.error_messages[key]
4106 else:
4107 self.error_messages[key] = value
4109 def inputline_changed(self, text):
4110 pass
4112 def inputline_finished(self, text):
4113 line = str(text)
4115 toks = line.split()
4116 clearit, hideit, error = False, True, None
4117 if len(toks) >= 1:
4118 command = toks[0].lower()
4120 try:
4121 quick_filter_commands = {
4122 'n': '%s.*.*.*',
4123 's': '*.%s.*.*',
4124 'l': '*.*.%s.*',
4125 'c': '*.*.*.%s'}
4127 if command in quick_filter_commands:
4128 if len(toks) >= 2:
4129 patterns = [
4130 quick_filter_commands[toks[0]] % pat
4131 for pat in toks[1:]]
4132 self.set_quick_filter_patterns(patterns, line)
4133 else:
4134 self.set_quick_filter_patterns(None)
4136 self.update()
4138 elif command in ('hide', 'unhide'):
4139 if len(toks) >= 2:
4140 patterns = []
4141 if len(toks) == 2:
4142 patterns = [toks[1]]
4143 elif len(toks) >= 3:
4144 x = {
4145 'n': '%s.*.*.*',
4146 's': '*.%s.*.*',
4147 'l': '*.*.%s.*',
4148 'c': '*.*.*.%s'}
4150 if toks[1] in x:
4151 patterns.extend(
4152 x[toks[1]] % tok for tok in toks[2:])
4154 for pattern in patterns:
4155 if command == 'hide':
4156 self.add_blacklist_pattern(pattern)
4157 else:
4158 self.remove_blacklist_pattern(pattern)
4160 elif command == 'unhide' and len(toks) == 1:
4161 self.clear_blacklist()
4163 clearit = True
4165 self.update()
4167 elif command == 'markers':
4168 if len(toks) == 2:
4169 if toks[1] == 'all':
4170 kinds = self.all_marker_kinds
4171 else:
4172 kinds = []
4173 for x in toks[1]:
4174 try:
4175 kinds.append(int(x))
4176 except Exception:
4177 pass
4179 self.set_visible_marker_kinds(kinds)
4181 elif len(toks) == 1:
4182 self.set_visible_marker_kinds(())
4184 self.update()
4186 elif command == 'scaling':
4187 if len(toks) == 2:
4188 hideit = False
4189 error = 'wrong number of arguments'
4191 if len(toks) >= 3:
4192 vmin, vmax = [
4193 pyrocko.model.float_or_none(x)
4194 for x in toks[-2:]]
4196 def upd(d, k, vmin, vmax):
4197 if k in d:
4198 if vmin is not None:
4199 d[k] = vmin, d[k][1]
4200 if vmax is not None:
4201 d[k] = d[k][0], vmax
4203 if len(toks) == 1:
4204 self.remove_scaling_hooks()
4206 elif len(toks) == 3:
4207 def hook(data_ranges):
4208 for k in data_ranges:
4209 upd(data_ranges, k, vmin, vmax)
4211 self.set_scaling_hook('_', hook)
4213 elif len(toks) == 4:
4214 pattern = toks[1]
4216 def hook(data_ranges):
4217 for k in pyrocko.util.match_nslcs(
4218 pattern, list(data_ranges.keys())):
4220 upd(data_ranges, k, vmin, vmax)
4222 self.set_scaling_hook(pattern, hook)
4224 elif command == 'goto':
4225 toks2 = line.split(None, 1)
4226 if len(toks2) == 2:
4227 arg = toks2[1]
4228 m = re.match(
4229 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4230 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4231 if m:
4232 tlen = None
4233 if not m.group(1):
4234 tlen = 12*32*24*60*60
4235 elif not m.group(2):
4236 tlen = 32*24*60*60
4237 elif not m.group(3):
4238 tlen = 24*60*60
4239 elif not m.group(4):
4240 tlen = 60*60
4241 elif not m.group(5):
4242 tlen = 60
4244 supl = '1970-01-01 00:00:00'
4245 if len(supl) > len(arg):
4246 arg = arg + supl[-(len(supl)-len(arg)):]
4247 t = pyrocko.util.str_to_time(arg)
4248 self.go_to_time(t, tlen=tlen)
4250 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4251 supl = '00:00:00'
4252 if len(supl) > len(arg):
4253 arg = arg + supl[-(len(supl)-len(arg)):]
4254 tmin, tmax = self.get_time_range()
4255 sdate = pyrocko.util.time_to_str(
4256 tmin/2.+tmax/2., format='%Y-%m-%d')
4257 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4258 self.go_to_time(t)
4260 elif arg == 'today':
4261 self.go_to_time(
4262 day_start(
4263 time.time()), tlen=24*60*60)
4265 elif arg == 'yesterday':
4266 self.go_to_time(
4267 day_start(
4268 time.time()-24*60*60), tlen=24*60*60)
4270 else:
4271 self.go_to_event_by_name(arg)
4273 else:
4274 raise PileViewerMainException(
4275 'No such command: %s' % command)
4277 except PileViewerMainException as e:
4278 error = str(e)
4279 hideit = False
4281 return clearit, hideit, error
4283 return PileViewerMain
4286PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4287GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4290class LineEditWithAbort(qw.QLineEdit):
4292 aborted = qc.pyqtSignal()
4293 history_down = qc.pyqtSignal()
4294 history_up = qc.pyqtSignal()
4296 def keyPressEvent(self, key_event):
4297 if key_event.key() == qc.Qt.Key_Escape:
4298 self.aborted.emit()
4299 elif key_event.key() == qc.Qt.Key_Down:
4300 self.history_down.emit()
4301 elif key_event.key() == qc.Qt.Key_Up:
4302 self.history_up.emit()
4303 else:
4304 return qw.QLineEdit.keyPressEvent(self, key_event)
4307class PileViewer(qw.QFrame):
4308 '''
4309 PileViewerMain + Controls + Inputline
4310 '''
4312 def __init__(
4313 self, pile,
4314 ntracks_shown_max=20,
4315 marker_editor_sortable=True,
4316 use_opengl=None,
4317 panel_parent=None,
4318 *args):
4320 qw.QFrame.__init__(self, *args)
4322 layout = qw.QGridLayout()
4323 layout.setContentsMargins(0, 0, 0, 0)
4324 layout.setSpacing(0)
4326 self.menu = PileViewerMenuBar(self)
4328 if use_opengl is None:
4329 use_opengl = is_macos
4331 if use_opengl:
4332 self.viewer = GLPileViewerMain(
4333 pile,
4334 ntracks_shown_max=ntracks_shown_max,
4335 panel_parent=panel_parent,
4336 menu=self.menu)
4337 else:
4338 self.viewer = PileViewerMain(
4339 pile,
4340 ntracks_shown_max=ntracks_shown_max,
4341 panel_parent=panel_parent,
4342 menu=self.menu)
4344 self.marker_editor_sortable = marker_editor_sortable
4346 # self.setFrameShape(qw.QFrame.StyledPanel)
4347 # self.setFrameShadow(qw.QFrame.Sunken)
4349 self.input_area = qw.QFrame(self)
4350 ia_layout = qw.QGridLayout()
4351 ia_layout.setContentsMargins(11, 11, 11, 11)
4352 self.input_area.setLayout(ia_layout)
4354 self.inputline = LineEditWithAbort(self.input_area)
4355 self.inputline.returnPressed.connect(
4356 self.inputline_returnpressed)
4357 self.inputline.editingFinished.connect(
4358 self.inputline_finished)
4359 self.inputline.aborted.connect(
4360 self.inputline_aborted)
4362 self.inputline.history_down.connect(
4363 lambda: self.step_through_history(1))
4364 self.inputline.history_up.connect(
4365 lambda: self.step_through_history(-1))
4367 self.inputline.textEdited.connect(
4368 self.inputline_changed)
4370 self.inputline.setPlaceholderText(
4371 u'Quick commands: e.g. \'c HH?\' to select channels. '
4372 u'Use ↑ or ↓ to navigate.')
4373 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4374 self.input_area.hide()
4375 self.history = None
4377 self.inputline_error_str = None
4379 self.inputline_error = qw.QLabel()
4380 self.inputline_error.hide()
4382 ia_layout.addWidget(self.inputline, 0, 0)
4383 ia_layout.addWidget(self.inputline_error, 1, 0)
4384 layout.addWidget(self.input_area, 0, 0, 1, 2)
4385 layout.addWidget(self.viewer, 1, 0)
4387 pb = Progressbars(self)
4388 layout.addWidget(pb, 2, 0, 1, 2)
4389 self.progressbars = pb
4391 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4392 self.scrollbar = scrollbar
4393 layout.addWidget(scrollbar, 1, 1)
4394 self.scrollbar.valueChanged.connect(
4395 self.scrollbar_changed)
4397 self.block_scrollbar_changes = False
4399 self.viewer.want_input.connect(
4400 self.inputline_show)
4401 self.viewer.tracks_range_changed.connect(
4402 self.tracks_range_changed)
4403 self.viewer.pile_has_changed_signal.connect(
4404 self.adjust_controls)
4405 self.viewer.about_to_close.connect(
4406 self.save_inputline_history)
4408 self.setLayout(layout)
4410 def cleanup(self):
4411 self.viewer.cleanup()
4413 def get_progressbars(self):
4414 return self.progressbars
4416 def inputline_show(self):
4417 if not self.history:
4418 self.load_inputline_history()
4420 self.input_area.show()
4421 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4422 self.inputline.selectAll()
4424 def inputline_set_error(self, string):
4425 self.inputline_error_str = string
4426 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4427 self.inputline.selectAll()
4428 self.inputline_error.setText(string)
4429 self.input_area.show()
4430 self.inputline_error.show()
4432 def inputline_clear_error(self):
4433 if self.inputline_error_str:
4434 self.inputline.setPalette(qw.QApplication.palette())
4435 self.inputline_error_str = None
4436 self.inputline_error.clear()
4437 self.inputline_error.hide()
4439 def inputline_changed(self, line):
4440 self.viewer.inputline_changed(str(line))
4441 self.inputline_clear_error()
4443 def inputline_returnpressed(self):
4444 line = str(self.inputline.text())
4445 clearit, hideit, error = self.viewer.inputline_finished(line)
4447 if error:
4448 self.inputline_set_error(error)
4450 line = line.strip()
4452 if line != '' and not error:
4453 if not (len(self.history) >= 1 and line == self.history[-1]):
4454 self.history.append(line)
4456 if clearit:
4458 self.inputline.blockSignals(True)
4459 qpat, qinp = self.viewer.get_quick_filter_patterns()
4460 if qpat is None:
4461 self.inputline.clear()
4462 else:
4463 self.inputline.setText(qinp)
4464 self.inputline.blockSignals(False)
4466 if hideit and not error:
4467 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4468 self.input_area.hide()
4470 self.hist_ind = len(self.history)
4472 def inputline_aborted(self):
4473 '''
4474 Hide the input line.
4475 '''
4476 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4477 self.hist_ind = len(self.history)
4478 self.input_area.hide()
4480 def save_inputline_history(self):
4481 '''
4482 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4483 '''
4484 if not self.history:
4485 return
4487 conf = pyrocko.config
4488 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4489 with open(fn_hist, 'w') as f:
4490 i = min(100, len(self.history))
4491 for c in self.history[-i:]:
4492 f.write('%s\n' % c)
4494 def load_inputline_history(self):
4495 '''
4496 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4497 '''
4498 conf = pyrocko.config
4499 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4500 if not os.path.exists(fn_hist):
4501 with open(fn_hist, 'w+') as f:
4502 f.write('\n')
4504 with open(fn_hist, 'r') as f:
4505 self.history = [line.strip() for line in f.readlines()]
4507 self.hist_ind = len(self.history)
4509 def step_through_history(self, ud=1):
4510 '''
4511 Step through input line history and set the input line text.
4512 '''
4513 n = len(self.history)
4514 self.hist_ind += ud
4515 self.hist_ind %= (n + 1)
4516 if len(self.history) != 0 and self.hist_ind != n:
4517 self.inputline.setText(self.history[self.hist_ind])
4518 else:
4519 self.inputline.setText('')
4521 def inputline_finished(self):
4522 pass
4524 def tracks_range_changed(self, ntracks, ilo, ihi):
4525 if self.block_scrollbar_changes:
4526 return
4528 self.scrollbar.blockSignals(True)
4529 self.scrollbar.setPageStep(ihi-ilo)
4530 vmax = max(0, ntracks-(ihi-ilo))
4531 self.scrollbar.setRange(0, vmax)
4532 self.scrollbar.setValue(ilo)
4533 self.scrollbar.setHidden(vmax == 0)
4534 self.scrollbar.blockSignals(False)
4536 def scrollbar_changed(self, value):
4537 self.block_scrollbar_changes = True
4538 ilo = value
4539 ihi = ilo + self.scrollbar.pageStep()
4540 self.viewer.set_tracks_range((ilo, ihi))
4541 self.block_scrollbar_changes = False
4542 self.update_contents()
4544 def controls(self):
4545 frame = qw.QFrame(self)
4546 layout = qw.QGridLayout()
4547 frame.setLayout(layout)
4549 minfreq = 0.001
4550 maxfreq = 1000.0
4551 self.lowpass_control = ValControl(high_is_none=True)
4552 self.lowpass_control.setup(
4553 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4554 self.highpass_control = ValControl(low_is_none=True)
4555 self.highpass_control.setup(
4556 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4557 self.gain_control = ValControl()
4558 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4559 self.rot_control = LinValControl()
4560 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4561 self.colorbar_control = ColorbarControl(self)
4563 self.lowpass_control.valchange.connect(
4564 self.viewer.lowpass_change)
4565 self.highpass_control.valchange.connect(
4566 self.viewer.highpass_change)
4567 self.gain_control.valchange.connect(
4568 self.viewer.gain_change)
4569 self.rot_control.valchange.connect(
4570 self.viewer.rot_change)
4571 self.colorbar_control.cmap_changed.connect(
4572 self.viewer.waterfall_cmap_change
4573 )
4574 self.colorbar_control.clip_changed.connect(
4575 self.viewer.waterfall_clip_change
4576 )
4577 self.colorbar_control.show_absolute_toggled.connect(
4578 self.viewer.waterfall_show_absolute_change
4579 )
4580 self.colorbar_control.show_integrate_toggled.connect(
4581 self.viewer.waterfall_set_integrate
4582 )
4584 for icontrol, control in enumerate((
4585 self.highpass_control,
4586 self.lowpass_control,
4587 self.gain_control,
4588 self.rot_control,
4589 self.colorbar_control)):
4591 for iwidget, widget in enumerate(control.widgets()):
4592 layout.addWidget(widget, icontrol, iwidget)
4594 spacer = qw.QSpacerItem(
4595 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4596 layout.addItem(spacer, 4, 0, 1, 3)
4598 self.adjust_controls()
4599 self.viewer.viewmode_change(ViewMode.Wiggle)
4600 return frame
4602 def marker_editor(self):
4603 editor = pyrocko.gui.marker_editor.MarkerEditor(
4604 self, sortable=self.marker_editor_sortable)
4606 editor.set_viewer(self.get_view())
4607 editor.get_marker_model().dataChanged.connect(
4608 self.update_contents)
4609 return editor
4611 def adjust_controls(self):
4612 dtmin, dtmax = self.viewer.content_deltat_range()
4613 maxfreq = 0.5/dtmin
4614 minfreq = (0.5/dtmax)*0.0001
4615 self.lowpass_control.set_range(minfreq, maxfreq)
4616 self.highpass_control.set_range(minfreq, maxfreq)
4618 def setup_snufflings(self):
4619 self.viewer.setup_snufflings()
4621 def get_view(self):
4622 return self.viewer
4624 def update_contents(self):
4625 self.viewer.update()
4627 def get_pile(self):
4628 return self.viewer.get_pile()
4630 def show_colorbar_ctrl(self, show):
4631 for w in self.colorbar_control.widgets():
4632 w.setVisible(show)
4634 def show_gain_ctrl(self, show):
4635 for w in self.gain_control.widgets():
4636 w.setVisible(show)