1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import os
7import time
8import calendar
9import datetime
10import re
11import math
12import logging
13import operator
14import copy
15import enum
16from itertools import groupby
18import numpy as num
19import pyrocko.model
20import pyrocko.pile
21import pyrocko.trace
22import pyrocko.response
23import pyrocko.util
24import pyrocko.plot
25import pyrocko.gui.snuffling
26import pyrocko.gui.snufflings
27import pyrocko.gui.marker_editor
29from pyrocko.util import hpfloat, gmtime_x, mystrftime
31from .marker import associate_phases_to_events, MarkerOneNSLCRequired
33from .util import (ValControl, LinValControl, Marker, EventMarker,
34 PhaseMarker, make_QPolygonF, draw_label, Label,
35 Progressbars, ColorbarControl)
37from .qt_compat import qc, qg, qw, qsvg
39from .pile_viewer_waterfall import TraceWaterfall
41import scipy.stats as sstats
42import platform
44MIN_LABEL_SIZE_PT = 6
47qc.QString = str
49qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
50 qw.QFileDialog.DontUseSheet
52is_macos = platform.uname()[0] == 'Darwin'
54logger = logging.getLogger('pyrocko.gui.pile_viewer')
57def detrend(x, y):
58 slope, offset, _, _, _ = sstats.linregress(x, y)
59 y_detrended = y - slope * x - offset
60 return y_detrended, slope, offset
63def retrend(x, y_detrended, slope, offset):
64 return x * slope + y_detrended + offset
67class Global(object):
68 appOnDemand = None
71class NSLC(object):
72 def __init__(self, n, s, l=None, c=None): # noqa
73 self.network = n
74 self.station = s
75 self.location = l
76 self.channel = c
79class m_float(float):
81 def __str__(self):
82 if abs(self) >= 10000.:
83 return '%g km' % round(self/1000., 0)
84 elif abs(self) >= 1000.:
85 return '%g km' % round(self/1000., 1)
86 else:
87 return '%.5g m' % self
89 def __lt__(self, other):
90 if other is None:
91 return True
92 return float(self) < float(other)
94 def __gt__(self, other):
95 if other is None:
96 return False
97 return float(self) > float(other)
100def m_float_or_none(x):
101 if x is None:
102 return None
103 else:
104 return m_float(x)
107def make_chunks(items):
108 '''
109 Split a list of integers into sublists of consecutive elements.
110 '''
111 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
112 enumerate(items), (lambda x: x[1]-x[0]))]
115class deg_float(float):
117 def __str__(self):
118 return '%4.0f' % self
120 def __lt__(self, other):
121 if other is None:
122 return True
123 return float(self) < float(other)
125 def __gt__(self, other):
126 if other is None:
127 return False
128 return float(self) > float(other)
131def deg_float_or_none(x):
132 if x is None:
133 return None
134 else:
135 return deg_float(x)
138class sector_int(int):
140 def __str__(self):
141 return '[%i]' % self
143 def __lt__(self, other):
144 if other is None:
145 return True
146 return int(self) < int(other)
148 def __gt__(self, other):
149 if other is None:
150 return False
151 return int(self) > int(other)
154def num_to_html(num):
155 snum = '%g' % num
156 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
157 if m:
158 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
160 return snum
163gap_lap_tolerance = 5.
166class ViewMode(enum.Enum):
167 Wiggle = 1
168 Waterfall = 2
171class Timer(object):
172 def __init__(self):
173 self._start = None
174 self._stop = None
176 def start(self):
177 self._start = os.times()
179 def stop(self):
180 self._stop = os.times()
182 def get(self):
183 a = self._start
184 b = self._stop
185 if a is not None and b is not None:
186 return tuple([b[i] - a[i] for i in range(5)])
187 else:
188 return tuple([0.] * 5)
190 def __sub__(self, other):
191 a = self.get()
192 b = other.get()
193 return tuple([a[i] - b[i] for i in range(5)])
196class ObjectStyle(object):
197 def __init__(self, frame_pen, fill_brush):
198 self.frame_pen = frame_pen
199 self.fill_brush = fill_brush
202box_styles = []
203box_alpha = 100
204for color in 'orange skyblue butter chameleon chocolate plum ' \
205 'scarletred'.split():
207 box_styles.append(ObjectStyle(
208 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
209 qg.QBrush(qg.QColor(
210 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
211 ))
213box_styles_coverage = {}
215box_styles_coverage['waveform'] = [
216 ObjectStyle(
217 qg.QPen(
218 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
219 1, qc.Qt.DashLine),
220 qg.QBrush(qg.QColor(
221 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
222 ),
223 ObjectStyle(
224 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
225 qg.QBrush(qg.QColor(
226 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
227 ),
228 ObjectStyle(
229 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
230 qg.QBrush(qg.QColor(
231 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
232 )]
234box_styles_coverage['waveform_promise'] = [
235 ObjectStyle(
236 qg.QPen(
237 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
238 1, qc.Qt.DashLine),
239 qg.QBrush(qg.QColor(
240 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
241 ),
242 ObjectStyle(
243 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
244 qg.QBrush(qg.QColor(
245 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
246 ),
247 ObjectStyle(
248 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
249 qg.QBrush(qg.QColor(
250 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
251 )]
253sday = 60*60*24. # \
254smonth = 60*60*24*30. # | only used as approx. intervals...
255syear = 60*60*24*365. # /
257acceptable_tincs = num.array([
258 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
259 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
262working_system_time_range = \
263 pyrocko.util.working_system_time_range()
265initial_time_range = []
267try:
268 initial_time_range.append(
269 calendar.timegm((1950, 1, 1, 0, 0, 0)))
270except Exception:
271 initial_time_range.append(working_system_time_range[0])
273try:
274 initial_time_range.append(
275 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
276except Exception:
277 initial_time_range.append(working_system_time_range[1])
280def is_working_time(t):
281 return working_system_time_range[0] <= t and \
282 t <= working_system_time_range[1]
285def fancy_time_ax_format(inc):
286 l0_fmt_brief = ''
287 l2_fmt = ''
288 l2_trig = 0
289 if inc < 0.000001:
290 l0_fmt = '.%n'
291 l0_center = False
292 l1_fmt = '%H:%M:%S'
293 l1_trig = 6
294 l2_fmt = '%b %d, %Y'
295 l2_trig = 3
296 elif inc < 0.001:
297 l0_fmt = '.%u'
298 l0_center = False
299 l1_fmt = '%H:%M:%S'
300 l1_trig = 6
301 l2_fmt = '%b %d, %Y'
302 l2_trig = 3
303 elif inc < 1:
304 l0_fmt = '.%r'
305 l0_center = False
306 l1_fmt = '%H:%M:%S'
307 l1_trig = 6
308 l2_fmt = '%b %d, %Y'
309 l2_trig = 3
310 elif inc < 60:
311 l0_fmt = '%H:%M:%S'
312 l0_center = False
313 l1_fmt = '%b %d, %Y'
314 l1_trig = 3
315 elif inc < 3600:
316 l0_fmt = '%H:%M'
317 l0_center = False
318 l1_fmt = '%b %d, %Y'
319 l1_trig = 3
320 elif inc < sday:
321 l0_fmt = '%H:%M'
322 l0_center = False
323 l1_fmt = '%b %d, %Y'
324 l1_trig = 3
325 elif inc < smonth:
326 l0_fmt = '%a %d'
327 l0_fmt_brief = '%d'
328 l0_center = True
329 l1_fmt = '%b, %Y'
330 l1_trig = 2
331 elif inc < syear:
332 l0_fmt = '%b'
333 l0_center = True
334 l1_fmt = '%Y'
335 l1_trig = 1
336 else:
337 l0_fmt = '%Y'
338 l0_center = False
339 l1_fmt = ''
340 l1_trig = 0
342 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
345def day_start(timestamp):
346 tt = time.gmtime(int(timestamp))
347 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
348 return calendar.timegm(tts)
351def month_start(timestamp):
352 tt = time.gmtime(int(timestamp))
353 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
354 return calendar.timegm(tts)
357def year_start(timestamp):
358 tt = time.gmtime(int(timestamp))
359 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
360 return calendar.timegm(tts)
363def time_nice_value(inc0):
364 if inc0 < acceptable_tincs[0]:
365 return pyrocko.plot.nice_value(inc0)
366 elif inc0 > acceptable_tincs[-1]:
367 return pyrocko.plot.nice_value(inc0/syear)*syear
368 else:
369 i = num.argmin(num.abs(acceptable_tincs-inc0))
370 return acceptable_tincs[i]
373class TimeScaler(pyrocko.plot.AutoScaler):
374 def __init__(self):
375 pyrocko.plot.AutoScaler.__init__(self)
376 self.mode = 'min-max'
378 def make_scale(self, data_range):
379 assert self.mode in ('min-max', 'off'), \
380 'mode must be "min-max" or "off" for TimeScaler'
382 data_min = min(data_range)
383 data_max = max(data_range)
384 is_reverse = (data_range[0] > data_range[1])
386 mi, ma = data_min, data_max
387 nmi = mi
388 if self.mode != 'off':
389 nmi = mi - self.space*(ma-mi)
391 nma = ma
392 if self.mode != 'off':
393 nma = ma + self.space*(ma-mi)
395 mi, ma = nmi, nma
397 if mi == ma and self.mode != 'off':
398 mi -= 1.0
399 ma += 1.0
401 mi = max(working_system_time_range[0], mi)
402 ma = min(working_system_time_range[1], ma)
404 # make nice tick increment
405 if self.inc is not None:
406 inc = self.inc
407 else:
408 if self.approx_ticks > 0.:
409 inc = time_nice_value((ma-mi)/self.approx_ticks)
410 else:
411 inc = time_nice_value((ma-mi)*10.)
413 if inc == 0.0:
414 inc = 1.0
416 if is_reverse:
417 return ma, mi, -inc
418 else:
419 return mi, ma, inc
421 def make_ticks(self, data_range):
422 mi, ma, inc = self.make_scale(data_range)
424 is_reverse = False
425 if inc < 0:
426 mi, ma, inc = ma, mi, -inc
427 is_reverse = True
429 ticks = []
431 if inc < sday:
432 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
433 if inc < 0.001:
434 mi_day = hpfloat(mi_day)
436 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
437 if inc < 0.001:
438 base = hpfloat(base)
440 base_day = mi_day
441 i = 0
442 while True:
443 tick = base+i*inc
444 if tick > ma:
445 break
447 tick_day = day_start(tick)
448 if tick_day > base_day:
449 base_day = tick_day
450 base = base_day
451 i = 0
452 else:
453 ticks.append(tick)
454 i += 1
456 elif inc < smonth:
457 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
458 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
459 delta = datetime.timedelta(days=int(round(inc/sday)))
460 if mi_day == mi:
461 dt_base += delta
462 i = 0
463 while True:
464 current = dt_base + i*delta
465 tick = calendar.timegm(current.timetuple())
466 if tick > ma:
467 break
468 ticks.append(tick)
469 i += 1
471 elif inc < syear:
472 mi_month = month_start(max(
473 mi, working_system_time_range[0]+smonth*1.5))
475 y, m = time.gmtime(mi_month)[:2]
476 while True:
477 tick = calendar.timegm((y, m, 1, 0, 0, 0))
478 m += 1
479 if m > 12:
480 y, m = y+1, 1
482 if tick > ma:
483 break
485 if tick >= mi:
486 ticks.append(tick)
488 else:
489 mi_year = year_start(max(
490 mi, working_system_time_range[0]+syear*1.5))
492 incy = int(round(inc/syear))
493 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
495 while True:
496 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
497 y += incy
498 if tick > ma:
499 break
500 if tick >= mi:
501 ticks.append(tick)
503 if is_reverse:
504 ticks.reverse()
506 return ticks, inc
509def need_l1_tick(tt, ms, l1_trig):
510 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
513def tick_to_labels(tick, inc):
514 tt, ms = gmtime_x(tick)
515 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
516 fancy_time_ax_format(inc)
518 l0 = mystrftime(l0_fmt, tt, ms)
519 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
520 l1, l2 = None, None
521 if need_l1_tick(tt, ms, l1_trig):
522 l1 = mystrftime(l1_fmt, tt, ms)
523 if need_l1_tick(tt, ms, l2_trig):
524 l2 = mystrftime(l2_fmt, tt, ms)
526 return l0, l0_brief, l0_center, l1, l2
529def l1_l2_tick(tick, inc):
530 tt, ms = gmtime_x(tick)
531 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
532 fancy_time_ax_format(inc)
534 l1 = mystrftime(l1_fmt, tt, ms)
535 l2 = mystrftime(l2_fmt, tt, ms)
536 return l1, l2
539class TimeAx(TimeScaler):
540 def __init__(self, *args):
541 TimeScaler.__init__(self, *args)
543 def drawit(self, p, xprojection, yprojection):
544 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
545 p.setPen(pen)
546 font = qg.QFont()
547 font.setBold(True)
548 p.setFont(font)
549 fm = p.fontMetrics()
550 ticklen = 10
551 pad = 10
552 tmin, tmax = xprojection.get_in_range()
553 ticks, inc = self.make_ticks((tmin, tmax))
554 l1_hits = 0
555 l2_hits = 0
557 vmin, vmax = yprojection(0), yprojection(ticklen)
558 uumin, uumax = xprojection.get_out_range()
559 first_tick_with_label = None
561 data = []
562 for tick in ticks:
563 umin = xprojection(tick)
565 umin_approx_next = xprojection(tick+inc)
566 umax = xprojection(tick)
568 pinc_approx = umin_approx_next - umin
570 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
571 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
573 if tick == 0.0 and tmax - tmin < 3600*24:
574 # hide year at epoch
575 # (we assume that synthetic data is shown)
576 if l2:
577 l2 = None
578 elif l1:
579 l1 = None
581 if l0_center:
582 ushift = (umin_approx_next-umin)/2.
583 else:
584 ushift = 0.
586 abbr_level = 0
587 for l0x in (l0, l0_brief, ''):
588 label0 = l0x
589 rect0 = fm.boundingRect(label0)
590 if rect0.width() <= pinc_approx*0.9:
591 break
593 abbr_level += 1
595 data.append((
596 l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
597 pinc_approx))
599 for (l0, l0_brief, l0_center, l1, l2, tick, ushift, umin,
600 pinc_approx) in data:
602 label0 = (l0, l0_brief, '')[abbr_level]
603 rect0 = fm.boundingRect(label0)
605 if uumin+pad < umin-rect0.width()/2.+ushift and \
606 umin+rect0.width()/2.+ushift < uumax-pad:
608 if first_tick_with_label is None:
609 first_tick_with_label = tick
610 p.drawText(qc.QPointF(
611 umin-rect0.width()/2.+ushift,
612 vmin+rect0.height()+ticklen), label0)
614 if l1:
615 label1 = l1
616 rect1 = fm.boundingRect(label1)
617 if uumin+pad < umin-rect1.width()/2. and \
618 umin+rect1.width()/2. < uumax-pad:
620 p.drawText(qc.QPointF(
621 umin-rect1.width()/2.,
622 vmin+rect0.height()+rect1.height()+ticklen),
623 label1)
625 l1_hits += 1
627 if l2:
628 label2 = l2
629 rect2 = fm.boundingRect(label2)
630 if uumin+pad < umin-rect2.width()/2. and \
631 umin+rect2.width()/2. < uumax-pad:
633 p.drawText(qc.QPointF(
634 umin-rect2.width()/2.,
635 vmin+rect0.height()+rect1.height()+rect2.height() +
636 ticklen), label2)
638 l2_hits += 1
640 if first_tick_with_label is None:
641 first_tick_with_label = tmin
643 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
645 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
646 tmax - tmin < 3600*24:
648 # hide year at epoch (we assume that synthetic data is shown)
649 if l2:
650 l2 = None
651 elif l1:
652 l1 = None
654 if l1_hits == 0 and l1:
655 label1 = l1
656 rect1 = fm.boundingRect(label1)
657 p.drawText(qc.QPointF(
658 uumin+pad,
659 vmin+rect0.height()+rect1.height()+ticklen),
660 label1)
662 l1_hits += 1
664 if l2_hits == 0 and l2:
665 label2 = l2
666 rect2 = fm.boundingRect(label2)
667 p.drawText(qc.QPointF(
668 uumin+pad,
669 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
670 label2)
672 v = yprojection(0)
673 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
676class Projection(object):
677 def __init__(self):
678 self.xr = 0., 1.
679 self.ur = 0., 1.
681 def set_in_range(self, xmin, xmax):
682 if xmax == xmin:
683 xmax = xmin + 1.
685 self.xr = xmin, xmax
687 def get_in_range(self):
688 return self.xr
690 def set_out_range(self, umin, umax):
691 if umax == umin:
692 umax = umin + 1.
694 self.ur = umin, umax
696 def get_out_range(self):
697 return self.ur
699 def __call__(self, x):
700 umin, umax = self.ur
701 xmin, xmax = self.xr
702 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
704 def clipped(self, x, umax_pad):
705 umin, umax = self.ur
706 xmin, xmax = self.xr
707 return min(
708 umax-umax_pad,
709 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
711 def rev(self, u):
712 umin, umax = self.ur
713 xmin, xmax = self.xr
714 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
716 def copy(self):
717 return copy.copy(self)
720def add_radiobuttongroup(menu, menudef, target, default=None):
721 group = qw.QActionGroup(menu)
722 group.setExclusive(True)
723 menuitems = []
725 for name, value, *shortcut in menudef:
726 action = menu.addAction(name)
727 action.setCheckable(True)
728 action.setActionGroup(group)
729 if shortcut:
730 action.setShortcut(shortcut[0])
732 menuitems.append((action, value))
733 if default is not None and (
734 name.lower().replace(' ', '_') == default or
735 value == default):
736 action.setChecked(True)
738 group.triggered.connect(target)
740 if default is None:
741 menuitems[0][0].setChecked(True)
743 return menuitems
746def sort_actions(menu):
747 actions = [act for act in menu.actions() if not act.menu()]
748 for action in actions:
749 menu.removeAction(action)
750 actions.sort(key=lambda x: str(x.text()))
752 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
753 if help_action:
754 actions.insert(0, actions.pop(actions.index(help_action[0])))
755 for action in actions:
756 menu.addAction(action)
759fkey_map = dict(zip(
760 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
761 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
762 range(10)))
765class PileViewerMainException(Exception):
766 pass
769class PileViewerMenuBar(qw.QMenuBar):
770 ...
773class PileViewerMenu(qw.QMenu):
774 ...
777def MakePileViewerMainClass(base):
779 class PileViewerMain(base):
781 want_input = qc.pyqtSignal()
782 about_to_close = qc.pyqtSignal()
783 pile_has_changed_signal = qc.pyqtSignal()
784 tracks_range_changed = qc.pyqtSignal(int, int, int)
786 begin_markers_add = qc.pyqtSignal(int, int)
787 end_markers_add = qc.pyqtSignal()
788 begin_markers_remove = qc.pyqtSignal(int, int)
789 end_markers_remove = qc.pyqtSignal()
791 marker_selection_changed = qc.pyqtSignal(list)
792 active_event_marker_changed = qc.pyqtSignal()
794 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
795 menu=None):
796 base.__init__(self, *args)
798 self.pile = pile
799 self.ax_height = 80
800 self.panel_parent = panel_parent
802 self.click_tolerance = 5
804 self.ntracks_shown_max = ntracks_shown_max
805 self.initial_ntracks_shown_max = ntracks_shown_max
806 self.ntracks = 0
807 self.show_all = True
808 self.shown_tracks_range = None
809 self.track_start = None
810 self.track_trange = None
812 self.lowpass = None
813 self.highpass = None
814 self.gain = 1.0
815 self.rotate = 0.0
816 self.picking_down = None
817 self.picking = None
818 self.floating_marker = None
819 self.markers = pyrocko.pile.Sorted([], 'tmin')
820 self.markers_deltat_max = 0.
821 self.n_selected_markers = 0
822 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
823 self.visible_marker_kinds = self.all_marker_kinds
824 self.active_event_marker = None
825 self.ignore_releases = 0
826 self.message = None
827 self.reloaded = False
828 self.pile_has_changed = False
829 self.config = pyrocko.config.config('snuffler')
831 self.tax = TimeAx()
832 self.setBackgroundRole(qg.QPalette.Base)
833 self.setAutoFillBackground(True)
834 poli = qw.QSizePolicy(
835 qw.QSizePolicy.Expanding,
836 qw.QSizePolicy.Expanding)
838 self.setSizePolicy(poli)
839 self.setMinimumSize(300, 200)
840 self.setFocusPolicy(qc.Qt.ClickFocus)
842 self.menu = menu or PileViewerMenu(self)
844 file_menu = self.menu.addMenu('&File')
845 view_menu = self.menu.addMenu('&View')
846 options_menu = self.menu.addMenu('&Options')
847 scale_menu = self.menu.addMenu('&Scaling')
848 sort_menu = self.menu.addMenu('Sor&ting')
849 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
851 help_menu = self.menu.addMenu('&Help')
853 self.snufflings_menu = self.toggle_panel_menu.addMenu(
854 'Run Snuffling')
855 self.toggle_panel_menu.addSeparator()
856 self.snuffling_help = help_menu.addMenu('Snuffling Help')
857 help_menu.addSeparator()
859 file_menu.addAction(
860 qg.QIcon.fromTheme('document-open'),
861 'Open waveform files...',
862 self.open_waveforms,
863 qg.QKeySequence.Open)
865 file_menu.addAction(
866 qg.QIcon.fromTheme('document-open'),
867 'Open waveform directory...',
868 self.open_waveform_directory)
870 file_menu.addAction(
871 'Open station files...',
872 self.open_stations)
874 file_menu.addAction(
875 'Open StationXML files...',
876 self.open_stations_xml)
878 file_menu.addAction(
879 'Open event file...',
880 self.read_events)
882 file_menu.addSeparator()
883 file_menu.addAction(
884 'Open marker file...',
885 self.read_markers)
887 file_menu.addAction(
888 qg.QIcon.fromTheme('document-save'),
889 'Save markers...',
890 self.write_markers,
891 qg.QKeySequence.Save)
893 file_menu.addAction(
894 qg.QIcon.fromTheme('document-save-as'),
895 'Save selected markers...',
896 self.write_selected_markers,
897 qg.QKeySequence.SaveAs)
899 file_menu.addSeparator()
900 file_menu.addAction(
901 qg.QIcon.fromTheme('document-print'),
902 'Print',
903 self.printit,
904 qg.QKeySequence.Print)
906 file_menu.addAction(
907 qg.QIcon.fromTheme('insert-image'),
908 'Save as SVG or PNG',
909 self.savesvg,
910 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
912 file_menu.addSeparator()
913 close = file_menu.addAction(
914 qg.QIcon.fromTheme('window-close'),
915 'Close',
916 self.myclose)
917 close.setShortcuts(
918 (qg.QKeySequence(qc.Qt.Key_Q),
919 qg.QKeySequence(qc.Qt.Key_X)))
921 # Scale Menu
922 menudef = [
923 ('Individual Scale',
924 lambda tr: tr.nslc_id,
925 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
926 ('Common Scale',
927 lambda tr: None,
928 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
929 ('Common Scale per Station',
930 lambda tr: (tr.network, tr.station),
931 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
932 ('Common Scale per Station Location',
933 lambda tr: (tr.network, tr.station, tr.location)),
934 ('Common Scale per Component',
935 lambda tr: (tr.channel)),
936 ]
938 self.menuitems_scaling = add_radiobuttongroup(
939 scale_menu, menudef, self.scalingmode_change,
940 default=self.config.trace_scale)
941 scale_menu.addSeparator()
943 self.scaling_key = self.menuitems_scaling[0][1]
944 self.scaling_hooks = {}
945 self.scalingmode_change()
947 menudef = [
948 ('Scaling based on Minimum and Maximum',
949 ('minmax', 'minmax')),
950 ('Scaling based on Minimum and Maximum (Robust)',
951 ('minmax', 'robust')),
952 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
953 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
954 ]
956 self.menuitems_scaling_base = add_radiobuttongroup(
957 scale_menu, menudef, self.scaling_base_change)
959 self.scaling_base = self.menuitems_scaling_base[0][1]
960 scale_menu.addSeparator()
962 self.menuitem_fixscalerange = scale_menu.addAction(
963 'Fix Scale Ranges')
964 self.menuitem_fixscalerange.setCheckable(True)
966 # Sort Menu
967 def sector_dist(sta):
968 if sta.dist_m is None:
969 return None, None
970 else:
971 return (
972 sector_int(round((sta.azimuth+15.)/30.)),
973 m_float(sta.dist_m))
975 menudef = [
976 ('Sort by Names',
977 lambda tr: (),
978 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
979 ('Sort by Distance',
980 lambda tr: self.station_attrib(
981 tr,
982 lambda sta: (m_float_or_none(sta.dist_m),),
983 lambda tr: (None,)),
984 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
985 ('Sort by Azimuth',
986 lambda tr: self.station_attrib(
987 tr,
988 lambda sta: (deg_float_or_none(sta.azimuth),),
989 lambda tr: (None,))),
990 ('Sort by Distance in 12 Azimuthal Blocks',
991 lambda tr: self.station_attrib(
992 tr,
993 sector_dist,
994 lambda tr: (None, None))),
995 ('Sort by Backazimuth',
996 lambda tr: self.station_attrib(
997 tr,
998 lambda sta: (deg_float_or_none(sta.backazimuth),),
999 lambda tr: (None,))),
1000 ]
1001 self.menuitems_ssorting = add_radiobuttongroup(
1002 sort_menu, menudef, self.s_sortingmode_change)
1003 sort_menu.addSeparator()
1005 self._ssort = lambda tr: ()
1007 self.menu.addSeparator()
1009 menudef = [
1010 ('Subsort by Network, Station, Location, Channel',
1011 ((0, 1, 2, 3), # gathering
1012 lambda tr: tr.location)), # coloring
1013 ('Subsort by Network, Station, Channel, Location',
1014 ((0, 1, 3, 2),
1015 lambda tr: tr.channel)),
1016 ('Subsort by Station, Network, Channel, Location',
1017 ((1, 0, 3, 2),
1018 lambda tr: tr.channel)),
1019 ('Subsort by Location, Network, Station, Channel',
1020 ((2, 0, 1, 3),
1021 lambda tr: tr.channel)),
1022 ('Subsort by Channel, Network, Station, Location',
1023 ((3, 0, 1, 2),
1024 lambda tr: (tr.network, tr.station, tr.location))),
1025 ('Subsort by Network, Station, Channel (Grouped by Location)',
1026 ((0, 1, 3),
1027 lambda tr: tr.location)),
1028 ('Subsort by Station, Network, Channel (Grouped by Location)',
1029 ((1, 0, 3),
1030 lambda tr: tr.location)),
1031 ]
1033 self.menuitems_sorting = add_radiobuttongroup(
1034 sort_menu, menudef, self.sortingmode_change)
1036 menudef = [(x.key, x.value) for x in
1037 self.config.visible_length_setting]
1039 # View menu
1040 self.menuitems_visible_length = add_radiobuttongroup(
1041 view_menu, menudef,
1042 self.visible_length_change)
1043 view_menu.addSeparator()
1045 view_modes = [
1046 ('Wiggle Plot', ViewMode.Wiggle),
1047 ('Waterfall', ViewMode.Waterfall)
1048 ]
1050 self.menuitems_viewmode = add_radiobuttongroup(
1051 view_menu, view_modes,
1052 self.viewmode_change, default=ViewMode.Wiggle)
1053 view_menu.addSeparator()
1055 self.menuitem_cliptraces = view_menu.addAction(
1056 'Clip Traces')
1057 self.menuitem_cliptraces.setCheckable(True)
1058 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1060 self.menuitem_showboxes = view_menu.addAction(
1061 'Show Boxes')
1062 self.menuitem_showboxes.setCheckable(True)
1063 self.menuitem_showboxes.setChecked(
1064 self.config.show_boxes)
1066 self.menuitem_colortraces = view_menu.addAction(
1067 'Color Traces')
1068 self.menuitem_colortraces.setCheckable(True)
1069 self.menuitem_antialias = view_menu.addAction(
1070 'Antialiasing')
1071 self.menuitem_antialias.setCheckable(True)
1073 view_menu.addSeparator()
1074 self.menuitem_showscalerange = view_menu.addAction(
1075 'Show Scale Ranges')
1076 self.menuitem_showscalerange.setCheckable(True)
1077 self.menuitem_showscalerange.setChecked(
1078 self.config.show_scale_ranges)
1080 self.menuitem_showscaleaxis = view_menu.addAction(
1081 'Show Scale Axes')
1082 self.menuitem_showscaleaxis.setCheckable(True)
1083 self.menuitem_showscaleaxis.setChecked(
1084 self.config.show_scale_axes)
1086 self.menuitem_showzeroline = view_menu.addAction(
1087 'Show Zero Lines')
1088 self.menuitem_showzeroline.setCheckable(True)
1090 view_menu.addSeparator()
1091 view_menu.addAction(
1092 qg.QIcon.fromTheme('view-fullscreen'),
1093 'Fullscreen',
1094 self.toggle_fullscreen,
1095 qg.QKeySequence(qc.Qt.Key_F11))
1097 # Options Menu
1098 self.menuitem_demean = options_menu.addAction('Demean')
1099 self.menuitem_demean.setCheckable(True)
1100 self.menuitem_demean.setChecked(self.config.demean)
1101 self.menuitem_demean.setShortcut(
1102 qg.QKeySequence(qc.Qt.Key_Underscore))
1104 self.menuitem_distances_3d = options_menu.addAction(
1105 '3D distances',
1106 self.distances_3d_changed)
1107 self.menuitem_distances_3d.setCheckable(True)
1109 self.menuitem_allowdownsampling = options_menu.addAction(
1110 'Allow Downsampling')
1111 self.menuitem_allowdownsampling.setCheckable(True)
1112 self.menuitem_allowdownsampling.setChecked(True)
1114 self.menuitem_degap = options_menu.addAction(
1115 'Allow Degapping')
1116 self.menuitem_degap.setCheckable(True)
1117 self.menuitem_degap.setChecked(True)
1119 options_menu.addSeparator()
1121 self.menuitem_fft_filtering = options_menu.addAction(
1122 'FFT Filtering')
1123 self.menuitem_fft_filtering.setCheckable(True)
1125 self.menuitem_lphp = options_menu.addAction(
1126 'Bandpass is Low- + Highpass')
1127 self.menuitem_lphp.setCheckable(True)
1128 self.menuitem_lphp.setChecked(True)
1130 options_menu.addSeparator()
1131 self.menuitem_watch = options_menu.addAction(
1132 'Watch Files')
1133 self.menuitem_watch.setCheckable(True)
1135 self.menuitem_liberal_fetch = options_menu.addAction(
1136 'Liberal Fetch Optimization')
1137 self.menuitem_liberal_fetch.setCheckable(True)
1139 self.visible_length = menudef[0][1]
1141 self.snufflings_menu.addAction(
1142 'Reload Snufflings',
1143 self.setup_snufflings)
1145 # Disable ShadowPileTest
1146 if False:
1147 test_action = self.menu.addAction(
1148 'Test',
1149 self.toggletest)
1150 test_action.setCheckable(True)
1152 help_menu.addAction(
1153 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1154 'Snuffler Controls',
1155 self.help,
1156 qg.QKeySequence(qc.Qt.Key_Question))
1158 help_menu.addAction(
1159 'About',
1160 self.about)
1162 self.time_projection = Projection()
1163 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1164 self.time_projection.set_out_range(0., self.width())
1166 self.gather = None
1168 self.trace_filter = None
1169 self.quick_filter = None
1170 self.quick_filter_patterns = None, None
1171 self.blacklist = []
1173 self.track_to_screen = Projection()
1174 self.track_to_nslc_ids = {}
1176 self.cached_vec = None
1177 self.cached_processed_traces = None
1179 self.timer = qc.QTimer(self)
1180 self.timer.timeout.connect(self.periodical)
1181 self.timer.setInterval(1000)
1182 self.timer.start()
1183 self.pile.add_listener(self)
1184 self.trace_styles = {}
1185 if self.get_squirrel() is None:
1186 self.determine_box_styles()
1188 self.setMouseTracking(True)
1190 user_home_dir = os.path.expanduser('~')
1191 self.snuffling_modules = {}
1192 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1193 self.default_snufflings = None
1194 self.snufflings = []
1196 self.stations = {}
1198 self.timer_draw = Timer()
1199 self.timer_cutout = Timer()
1200 self.time_spent_painting = 0.0
1201 self.time_last_painted = time.time()
1203 self.interactive_range_change_time = 0.0
1204 self.interactive_range_change_delay_time = 10.0
1205 self.follow_timer = None
1207 self.sortingmode_change_time = 0.0
1208 self.sortingmode_change_delay_time = None
1210 self.old_data_ranges = {}
1212 self.error_messages = {}
1213 self.return_tag = None
1214 self.wheel_pos = 60
1216 self.setAcceptDrops(True)
1217 self._paths_to_load = []
1219 self.tf_cache = {}
1221 self.waterfall = TraceWaterfall()
1222 self.waterfall_cmap = 'viridis'
1223 self.waterfall_clip_min = 0.
1224 self.waterfall_clip_max = 1.
1225 self.waterfall_show_absolute = False
1226 self.waterfall_integrate = False
1227 self.view_mode = ViewMode.Wiggle
1229 self.automatic_updates = True
1231 self.closing = False
1232 self.in_paint_event = False
1234 def fail(self, reason):
1235 box = qw.QMessageBox(self)
1236 box.setText(reason)
1237 box.exec_()
1239 def set_trace_filter(self, filter_func):
1240 self.trace_filter = filter_func
1241 self.sortingmode_change()
1243 def update_trace_filter(self):
1244 if self.blacklist:
1246 def blacklist_func(tr):
1247 return not pyrocko.util.match_nslc(
1248 self.blacklist, tr.nslc_id)
1250 else:
1251 blacklist_func = None
1253 if self.quick_filter is None and blacklist_func is None:
1254 self.set_trace_filter(None)
1255 elif self.quick_filter is None:
1256 self.set_trace_filter(blacklist_func)
1257 elif blacklist_func is None:
1258 self.set_trace_filter(self.quick_filter)
1259 else:
1260 self.set_trace_filter(
1261 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1263 def set_quick_filter(self, filter_func):
1264 self.quick_filter = filter_func
1265 self.update_trace_filter()
1267 def set_quick_filter_patterns(self, patterns, inputline=None):
1268 if patterns is not None:
1269 self.set_quick_filter(
1270 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1271 else:
1272 self.set_quick_filter(None)
1274 self.quick_filter_patterns = patterns, inputline
1276 def get_quick_filter_patterns(self):
1277 return self.quick_filter_patterns
1279 def add_blacklist_pattern(self, pattern):
1280 if pattern == 'empty':
1281 keys = set(self.pile.nslc_ids)
1282 trs = self.pile.all(
1283 tmin=self.tmin,
1284 tmax=self.tmax,
1285 load_data=False,
1286 degap=False)
1288 for tr in trs:
1289 if tr.nslc_id in keys:
1290 keys.remove(tr.nslc_id)
1292 for key in keys:
1293 xpattern = '.'.join(key)
1294 if xpattern not in self.blacklist:
1295 self.blacklist.append(xpattern)
1297 else:
1298 if pattern in self.blacklist:
1299 self.blacklist.remove(pattern)
1301 self.blacklist.append(pattern)
1303 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1304 self.update_trace_filter()
1306 def remove_blacklist_pattern(self, pattern):
1307 if pattern in self.blacklist:
1308 self.blacklist.remove(pattern)
1309 else:
1310 raise PileViewerMainException(
1311 'Pattern not found in blacklist.')
1313 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1314 self.update_trace_filter()
1316 def clear_blacklist(self):
1317 self.blacklist = []
1318 self.update_trace_filter()
1320 def ssort(self, tr):
1321 return self._ssort(tr)
1323 def station_key(self, x):
1324 return x.network, x.station
1326 def station_keys(self, x):
1327 return [
1328 (x.network, x.station, x.location),
1329 (x.network, x.station)]
1331 def station_attrib(self, tr, getter, default_getter):
1332 for sk in self.station_keys(tr):
1333 if sk in self.stations:
1334 station = self.stations[sk]
1335 return getter(station)
1337 return default_getter(tr)
1339 def get_station(self, sk):
1340 return self.stations[sk]
1342 def has_station(self, station):
1343 for sk in self.station_keys(station):
1344 if sk in self.stations:
1345 return True
1347 return False
1349 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1350 return self.station_attrib(
1351 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1353 def set_stations(self, stations):
1354 self.stations = {}
1355 self.add_stations(stations)
1357 def add_stations(self, stations):
1358 for station in stations:
1359 for sk in self.station_keys(station):
1360 self.stations[sk] = station
1362 ev = self.get_active_event()
1363 if ev:
1364 self.set_origin(ev)
1366 def add_event(self, event):
1367 marker = EventMarker(event)
1368 self.add_marker(marker)
1370 def add_events(self, events):
1371 markers = [EventMarker(e) for e in events]
1372 self.add_markers(markers)
1374 def set_event_marker_as_origin(self, ignore=None):
1375 selected = self.selected_markers()
1376 if not selected:
1377 self.fail('An event marker must be selected.')
1378 return
1380 m = selected[0]
1381 if not isinstance(m, EventMarker):
1382 self.fail('Selected marker is not an event.')
1383 return
1385 self.set_active_event_marker(m)
1387 def deactivate_event_marker(self):
1388 if self.active_event_marker:
1389 self.active_event_marker.active = False
1391 self.active_event_marker_changed.emit()
1392 self.active_event_marker = None
1394 def set_active_event_marker(self, event_marker):
1395 if self.active_event_marker:
1396 self.active_event_marker.active = False
1398 self.active_event_marker = event_marker
1399 event_marker.active = True
1400 event = event_marker.get_event()
1401 self.set_origin(event)
1402 self.active_event_marker_changed.emit()
1404 def set_active_event(self, event):
1405 for marker in self.markers:
1406 if isinstance(marker, EventMarker):
1407 if marker.get_event() is event:
1408 self.set_active_event_marker(marker)
1410 def get_active_event_marker(self):
1411 return self.active_event_marker
1413 def get_active_event(self):
1414 m = self.get_active_event_marker()
1415 if m is not None:
1416 return m.get_event()
1417 else:
1418 return None
1420 def get_active_markers(self):
1421 emarker = self.get_active_event_marker()
1422 if emarker is None:
1423 return None, []
1425 else:
1426 ev = emarker.get_event()
1427 pmarkers = [
1428 m for m in self.markers
1429 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1431 return emarker, pmarkers
1433 def set_origin(self, location):
1434 for station in self.stations.values():
1435 station.set_event_relative_data(
1436 location,
1437 distance_3d=self.menuitem_distances_3d.isChecked())
1439 self.sortingmode_change()
1441 def distances_3d_changed(self):
1442 ignore = self.menuitem_distances_3d.isChecked()
1443 self.set_event_marker_as_origin(ignore)
1445 def iter_snuffling_modules(self):
1446 pjoin = os.path.join
1447 for path in self.snuffling_paths:
1449 if not os.path.isdir(path):
1450 os.mkdir(path)
1452 for entry in os.listdir(path):
1453 directory = path
1454 fn = entry
1455 d = pjoin(path, entry)
1456 if os.path.isdir(d):
1457 directory = d
1458 if os.path.isfile(
1459 os.path.join(directory, 'snuffling.py')):
1460 fn = 'snuffling.py'
1462 if not fn.endswith('.py'):
1463 continue
1465 name = fn[:-3]
1467 if (directory, name) not in self.snuffling_modules:
1468 self.snuffling_modules[directory, name] = \
1469 pyrocko.gui.snuffling.SnufflingModule(
1470 directory, name, self)
1472 yield self.snuffling_modules[directory, name]
1474 def setup_snufflings(self):
1475 # user snufflings
1476 for mod in self.iter_snuffling_modules():
1477 try:
1478 mod.load_if_needed()
1479 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1480 logger.warning('Snuffling module "%s" is broken' % e)
1482 # load the default snufflings on first run
1483 if self.default_snufflings is None:
1484 self.default_snufflings = pyrocko.gui\
1485 .snufflings.__snufflings__()
1486 for snuffling in self.default_snufflings:
1487 self.add_snuffling(snuffling)
1489 def set_panel_parent(self, panel_parent):
1490 self.panel_parent = panel_parent
1492 def get_panel_parent(self):
1493 return self.panel_parent
1495 def add_snuffling(self, snuffling, reloaded=False):
1496 logger.debug('Adding snuffling %s' % snuffling.get_name())
1497 snuffling.init_gui(
1498 self, self.get_panel_parent(), self, reloaded=reloaded)
1499 self.snufflings.append(snuffling)
1500 self.update()
1502 def remove_snuffling(self, snuffling):
1503 snuffling.delete_gui()
1504 self.update()
1505 self.snufflings.remove(snuffling)
1506 snuffling.pre_destroy()
1508 def add_snuffling_menuitem(self, item):
1509 self.snufflings_menu.addAction(item)
1510 item.setParent(self.snufflings_menu)
1511 sort_actions(self.snufflings_menu)
1513 def remove_snuffling_menuitem(self, item):
1514 self.snufflings_menu.removeAction(item)
1516 def add_snuffling_help_menuitem(self, item):
1517 self.snuffling_help.addAction(item)
1518 item.setParent(self.snuffling_help)
1519 sort_actions(self.snuffling_help)
1521 def remove_snuffling_help_menuitem(self, item):
1522 self.snuffling_help.removeAction(item)
1524 def add_panel_toggler(self, item):
1525 self.toggle_panel_menu.addAction(item)
1526 item.setParent(self.toggle_panel_menu)
1527 sort_actions(self.toggle_panel_menu)
1529 def remove_panel_toggler(self, item):
1530 self.toggle_panel_menu.removeAction(item)
1532 def load(self, paths, regex=None, format='detect',
1533 cache_dir=None, force_cache=False):
1535 if cache_dir is None:
1536 cache_dir = pyrocko.config.config().cache_dir
1537 if isinstance(paths, str):
1538 paths = [paths]
1540 fns = pyrocko.util.select_files(
1541 paths, selector=None, include=regex, show_progress=False)
1543 if not fns:
1544 return
1546 cache = pyrocko.pile.get_cache(cache_dir)
1548 t = [time.time()]
1550 def update_bar(label, value):
1551 pbs = self.parent().get_progressbars()
1552 if label.lower() == 'looking at files':
1553 label = 'Looking at %i files' % len(fns)
1554 else:
1555 label = 'Scanning %i files' % len(fns)
1557 return pbs.set_status(label, value)
1559 def update_progress(label, i, n):
1560 abort = False
1562 qw.qApp.processEvents()
1563 if n != 0:
1564 perc = i*100/n
1565 else:
1566 perc = 100
1567 abort |= update_bar(label, perc)
1568 abort |= self.window().is_closing()
1570 tnow = time.time()
1571 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1572 self.update()
1573 t[0] = tnow
1575 return abort
1577 self.automatic_updates = False
1579 self.pile.load_files(
1580 sorted(fns),
1581 filename_attributes=regex,
1582 cache=cache,
1583 fileformat=format,
1584 show_progress=False,
1585 update_progress=update_progress)
1587 self.automatic_updates = True
1588 self.update()
1590 def load_queued(self):
1591 if not self._paths_to_load:
1592 return
1593 paths = self._paths_to_load
1594 self._paths_to_load = []
1595 self.load(paths)
1597 def load_soon(self, paths):
1598 self._paths_to_load.extend(paths)
1599 qc.QTimer.singleShot(200, self.load_queued)
1601 def open_waveforms(self):
1602 caption = 'Select one or more files to open'
1604 fns, _ = qw.QFileDialog.getOpenFileNames(
1605 self, caption, options=qfiledialog_options)
1607 if fns:
1608 self.load(list(str(fn) for fn in fns))
1610 def open_waveform_directory(self):
1611 caption = 'Select directory to scan for waveform files'
1613 dn = qw.QFileDialog.getExistingDirectory(
1614 self, caption, options=qfiledialog_options)
1616 if dn:
1617 self.load([str(dn)])
1619 def open_stations(self, fns=None):
1620 caption = 'Select one or more Pyrocko station files to open'
1622 if not fns:
1623 fns, _ = qw.QFileDialog.getOpenFileNames(
1624 self, caption, options=qfiledialog_options)
1626 try:
1627 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1628 for stat in stations:
1629 self.add_stations(stat)
1631 except Exception as e:
1632 self.fail('Failed to read station file: %s' % str(e))
1634 def open_stations_xml(self, fns=None):
1635 from pyrocko.io import stationxml
1637 caption = 'Select one or more StationXML files'
1638 if not fns:
1639 fns, _ = qw.QFileDialog.getOpenFileNames(
1640 self, caption, options=qfiledialog_options,
1641 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1642 ';;All files (*)')
1644 try:
1645 stations = [
1646 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1647 for x in fns]
1649 for stat in stations:
1650 self.add_stations(stat)
1652 except Exception as e:
1653 self.fail('Failed to read StationXML file: %s' % str(e))
1655 def add_traces(self, traces):
1656 if traces:
1657 mtf = pyrocko.pile.MemTracesFile(None, traces)
1658 self.pile.add_file(mtf)
1659 ticket = (self.pile, mtf)
1660 return ticket
1661 else:
1662 return (None, None)
1664 def release_data(self, tickets):
1665 for ticket in tickets:
1666 pile, mtf = ticket
1667 if pile is not None:
1668 pile.remove_file(mtf)
1670 def periodical(self):
1671 if self.menuitem_watch.isChecked():
1672 if self.pile.reload_modified():
1673 self.update()
1675 def get_pile(self):
1676 return self.pile
1678 def pile_changed(self, what):
1679 self.pile_has_changed = True
1680 self.pile_has_changed_signal.emit()
1681 if self.automatic_updates:
1682 self.update()
1684 def set_gathering(self, gather=None, color=None):
1686 if gather is None:
1687 def gather_func(tr):
1688 return tr.nslc_id
1690 gather = (0, 1, 2, 3)
1692 else:
1693 def gather_func(tr):
1694 return (
1695 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1697 if color is None:
1698 def color(tr):
1699 return tr.location
1701 self.gather = gather_func
1702 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1704 self.color_gather = color
1705 self.color_keys = self.pile.gather_keys(color)
1706 previous_ntracks = self.ntracks
1707 self.set_ntracks(len(keys))
1709 if self.shown_tracks_range is None or \
1710 previous_ntracks == 0 or \
1711 self.show_all:
1713 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1714 key_at_top = None
1715 n = high-low
1717 else:
1718 low, high = self.shown_tracks_range
1719 key_at_top = self.track_keys[low]
1720 n = high-low
1722 self.track_keys = sorted(keys)
1724 track_patterns = []
1725 for k in self.track_keys:
1726 pat = ['*', '*', '*', '*']
1727 for i, j in enumerate(gather):
1728 pat[j] = k[-len(gather)+i]
1730 track_patterns.append(pat)
1732 self.track_patterns = track_patterns
1734 if key_at_top is not None:
1735 try:
1736 ind = self.track_keys.index(key_at_top)
1737 low = ind
1738 high = low+n
1739 except Exception:
1740 pass
1742 self.set_tracks_range((low, high))
1744 self.key_to_row = dict(
1745 [(key, i) for (i, key) in enumerate(self.track_keys)])
1747 def inrange(x, r):
1748 return r[0] <= x and x < r[1]
1750 def trace_selector(trace):
1751 gt = self.gather(trace)
1752 return (
1753 gt in self.key_to_row and
1754 inrange(self.key_to_row[gt], self.shown_tracks_range))
1756 self.trace_selector = lambda x: \
1757 (self.trace_filter is None or self.trace_filter(x)) \
1758 and trace_selector(x)
1760 if self.tmin == working_system_time_range[0] and \
1761 self.tmax == working_system_time_range[1] or \
1762 self.show_all:
1764 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1765 if tmin is not None and tmax is not None:
1766 tlen = (tmax - tmin)
1767 tpad = tlen * 5./self.width()
1768 self.set_time_range(tmin-tpad, tmax+tpad)
1770 def set_time_range(self, tmin, tmax):
1771 if tmin is None:
1772 tmin = initial_time_range[0]
1774 if tmax is None:
1775 tmax = initial_time_range[1]
1777 if tmin > tmax:
1778 tmin, tmax = tmax, tmin
1780 if tmin == tmax:
1781 tmin -= 1.
1782 tmax += 1.
1784 tmin = max(working_system_time_range[0], tmin)
1785 tmax = min(working_system_time_range[1], tmax)
1787 min_deltat = self.content_deltat_range()[0]
1788 if (tmax - tmin < min_deltat):
1789 m = (tmin + tmax) / 2.
1790 tmin = m - min_deltat/2.
1791 tmax = m + min_deltat/2.
1793 self.time_projection.set_in_range(tmin, tmax)
1794 self.tmin, self.tmax = tmin, tmax
1796 def get_time_range(self):
1797 return self.tmin, self.tmax
1799 def ypart(self, y):
1800 if y < self.ax_height:
1801 return -1
1802 elif y > self.height()-self.ax_height:
1803 return 1
1804 else:
1805 return 0
1807 def time_fractional_digits(self):
1808 min_deltat = self.content_deltat_range()[0]
1809 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1811 def write_markers(self, fn=None):
1812 caption = "Choose a file name to write markers"
1813 if not fn:
1814 fn, _ = qw.QFileDialog.getSaveFileName(
1815 self, caption, options=qfiledialog_options)
1816 if fn:
1817 try:
1818 Marker.save_markers(
1819 self.markers, fn,
1820 fdigits=self.time_fractional_digits())
1822 except Exception as e:
1823 self.fail('Failed to write marker file: %s' % str(e))
1825 def write_selected_markers(self, fn=None):
1826 caption = "Choose a file name to write selected markers"
1827 if not fn:
1828 fn, _ = qw.QFileDialog.getSaveFileName(
1829 self, caption, options=qfiledialog_options)
1830 if fn:
1831 try:
1832 Marker.save_markers(
1833 self.iter_selected_markers(),
1834 fn,
1835 fdigits=self.time_fractional_digits())
1837 except Exception as e:
1838 self.fail('Failed to write marker file: %s' % str(e))
1840 def read_events(self, fn=None):
1841 '''
1842 Open QFileDialog to open, read and add
1843 :py:class:`pyrocko.model.Event` instances and their marker
1844 representation to the pile viewer.
1845 '''
1846 caption = "Selet one or more files to open"
1847 if not fn:
1848 fn, _ = qw.QFileDialog.getOpenFileName(
1849 self, caption, options=qfiledialog_options)
1850 if fn:
1851 try:
1852 self.add_events(pyrocko.model.load_events(fn))
1853 self.associate_phases_to_events()
1855 except Exception as e:
1856 self.fail('Failed to read event file: %s' % str(e))
1858 def read_markers(self, fn=None):
1859 '''
1860 Open QFileDialog to open, read and add markers to the pile viewer.
1861 '''
1862 caption = "Selet one or more marker files to open"
1863 if not fn:
1864 fn, _ = qw.QFileDialog.getOpenFileName(
1865 self, caption, options=qfiledialog_options)
1866 if fn:
1867 try:
1868 self.add_markers(Marker.load_markers(fn))
1869 self.associate_phases_to_events()
1871 except Exception as e:
1872 self.fail('Failed to read marker file: %s' % str(e))
1874 def associate_phases_to_events(self):
1875 associate_phases_to_events(self.markers)
1877 def add_marker(self, marker):
1878 # need index to inform QAbstactTableModel about upcoming change,
1879 # but have to restore current state in order to not cause problems
1880 self.markers.insert(marker)
1881 i = self.markers.remove(marker)
1883 self.begin_markers_add.emit(i, i)
1884 self.markers.insert(marker)
1885 self.end_markers_add.emit()
1886 self.markers_deltat_max = max(
1887 self.markers_deltat_max, marker.tmax - marker.tmin)
1889 def add_markers(self, markers):
1890 if not self.markers:
1891 self.begin_markers_add.emit(0, len(markers) - 1)
1892 self.markers.insert_many(markers)
1893 self.end_markers_add.emit()
1894 self.update_markers_deltat_max()
1895 else:
1896 for marker in markers:
1897 self.add_marker(marker)
1899 def update_markers_deltat_max(self):
1900 if self.markers:
1901 self.markers_deltat_max = max(
1902 marker.tmax - marker.tmin for marker in self.markers)
1904 def remove_marker(self, marker):
1905 '''
1906 Remove a ``marker`` from the :py:class:`PileViewer`.
1908 :param marker: :py:class:`Marker` (or subclass) instance
1909 '''
1911 if marker is self.active_event_marker:
1912 self.deactivate_event_marker()
1914 try:
1915 i = self.markers.index(marker)
1916 self.begin_markers_remove.emit(i, i)
1917 self.markers.remove_at(i)
1918 self.end_markers_remove.emit()
1919 except ValueError:
1920 pass
1922 def remove_markers(self, markers):
1923 '''
1924 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1926 :param markers: list of :py:class:`Marker` (or subclass)
1927 instances
1928 '''
1930 if markers is self.markers:
1931 markers = list(markers)
1933 for marker in markers:
1934 self.remove_marker(marker)
1936 self.update_markers_deltat_max()
1938 def remove_selected_markers(self):
1939 def delete_segment(istart, iend):
1940 self.begin_markers_remove.emit(istart, iend-1)
1941 for _ in range(iend - istart):
1942 self.markers.remove_at(istart)
1944 self.end_markers_remove.emit()
1946 istart = None
1947 ipos = 0
1948 markers = self.markers
1949 nmarkers = len(self.markers)
1950 while ipos < nmarkers:
1951 marker = markers[ipos]
1952 if marker.is_selected():
1953 if marker is self.active_event_marker:
1954 self.deactivate_event_marker()
1956 if istart is None:
1957 istart = ipos
1958 else:
1959 if istart is not None:
1960 delete_segment(istart, ipos)
1961 nmarkers -= ipos - istart
1962 ipos = istart - 1
1963 istart = None
1965 ipos += 1
1967 if istart is not None:
1968 delete_segment(istart, ipos)
1970 self.update_markers_deltat_max()
1972 def selected_markers(self):
1973 return [marker for marker in self.markers if marker.is_selected()]
1975 def iter_selected_markers(self):
1976 for marker in self.markers:
1977 if marker.is_selected():
1978 yield marker
1980 def get_markers(self):
1981 return self.markers
1983 def mousePressEvent(self, mouse_ev):
1984 self.show_all = False
1985 point = self.mapFromGlobal(mouse_ev.globalPos())
1987 if mouse_ev.button() == qc.Qt.LeftButton:
1988 marker = self.marker_under_cursor(point.x(), point.y())
1989 if self.picking:
1990 if self.picking_down is None:
1991 self.picking_down = (
1992 self.time_projection.rev(mouse_ev.x()),
1993 mouse_ev.y())
1995 elif marker is not None:
1996 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1997 self.deselect_all()
1998 marker.selected = True
1999 self.emit_selected_markers()
2000 self.update()
2001 else:
2002 self.track_start = mouse_ev.x(), mouse_ev.y()
2003 self.track_trange = self.tmin, self.tmax
2005 if mouse_ev.button() == qc.Qt.RightButton \
2006 and isinstance(self.menu, qw.QMenu):
2007 self.menu.exec_(qg.QCursor.pos())
2008 self.update_status()
2010 def mouseReleaseEvent(self, mouse_ev):
2011 if self.ignore_releases:
2012 self.ignore_releases -= 1
2013 return
2015 if self.picking:
2016 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2017 self.emit_selected_markers()
2019 if self.track_start:
2020 self.update()
2022 self.track_start = None
2023 self.track_trange = None
2024 self.update_status()
2026 def mouseDoubleClickEvent(self, mouse_ev):
2027 self.show_all = False
2028 self.start_picking(None)
2029 self.ignore_releases = 1
2031 def mouseMoveEvent(self, mouse_ev):
2032 point = self.mapFromGlobal(mouse_ev.globalPos())
2034 if self.picking:
2035 self.update_picking(point.x(), point.y())
2037 elif self.track_start is not None:
2038 x0, y0 = self.track_start
2039 dx = (point.x() - x0)/float(self.width())
2040 dy = (point.y() - y0)/float(self.height())
2041 if self.ypart(y0) == 1:
2042 dy = 0
2044 tmin0, tmax0 = self.track_trange
2046 scale = math.exp(-dy*5.)
2047 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2048 frac = x0/float(self.width())
2049 dt = dx*(tmax0-tmin0)*scale
2051 self.interrupt_following()
2052 self.set_time_range(
2053 tmin0 - dt - dtr*frac,
2054 tmax0 - dt + dtr*(1.-frac))
2056 self.update()
2057 else:
2058 self.hoovering(point.x(), point.y())
2060 self.update_status()
2062 def nslc_ids_under_cursor(self, x, y):
2063 ftrack = self.track_to_screen.rev(y)
2064 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2065 return nslc_ids
2067 def marker_under_cursor(self, x, y):
2068 mouset = self.time_projection.rev(x)
2069 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2070 relevant_nslc_ids = None
2071 for marker in self.markers:
2072 if marker.kind not in self.visible_marker_kinds:
2073 continue
2075 if (abs(mouset-marker.tmin) < deltat or
2076 abs(mouset-marker.tmax) < deltat):
2078 if relevant_nslc_ids is None:
2079 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2081 marker_nslc_ids = marker.get_nslc_ids()
2082 if not marker_nslc_ids:
2083 return marker
2085 for nslc_id in marker_nslc_ids:
2086 if nslc_id in relevant_nslc_ids:
2087 return marker
2089 def hoovering(self, x, y):
2090 mouset = self.time_projection.rev(x)
2091 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2092 needupdate = False
2093 haveone = False
2094 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2095 for marker in self.markers:
2096 if marker.kind not in self.visible_marker_kinds:
2097 continue
2099 state = abs(mouset-marker.tmin) < deltat or \
2100 abs(mouset-marker.tmax) < deltat and not haveone
2102 if state:
2103 xstate = False
2105 marker_nslc_ids = marker.get_nslc_ids()
2106 if not marker_nslc_ids:
2107 xstate = True
2109 for nslc in relevant_nslc_ids:
2110 if marker.match_nslc(nslc):
2111 xstate = True
2113 state = xstate
2115 if state:
2116 haveone = True
2117 oldstate = marker.is_alerted()
2118 if oldstate != state:
2119 needupdate = True
2120 marker.set_alerted(state)
2121 if state:
2122 self.message = marker.hoover_message()
2124 if not haveone:
2125 self.message = None
2127 if needupdate:
2128 self.update()
2130 def event(self, event):
2131 if event.type() == qc.QEvent.KeyPress:
2132 self.keyPressEvent(event)
2133 return True
2134 else:
2135 return base.event(self, event)
2137 def keyPressEvent(self, key_event):
2138 self.show_all = False
2139 dt = self.tmax - self.tmin
2140 tmid = (self.tmin + self.tmax) / 2.
2142 key = key_event.key()
2143 try:
2144 keytext = str(key_event.text())
2145 except UnicodeEncodeError:
2146 return
2148 if key == qc.Qt.Key_Space:
2149 self.interrupt_following()
2150 self.set_time_range(self.tmin+dt, self.tmax+dt)
2152 elif key == qc.Qt.Key_Up:
2153 for m in self.selected_markers():
2154 if isinstance(m, PhaseMarker):
2155 if key_event.modifiers() & qc.Qt.ShiftModifier:
2156 p = 0
2157 else:
2158 p = 1 if m.get_polarity() != 1 else None
2159 m.set_polarity(p)
2161 elif key == qc.Qt.Key_Down:
2162 for m in self.selected_markers():
2163 if isinstance(m, PhaseMarker):
2164 if key_event.modifiers() & qc.Qt.ShiftModifier:
2165 p = 0
2166 else:
2167 p = -1 if m.get_polarity() != -1 else None
2168 m.set_polarity(p)
2170 elif key == qc.Qt.Key_B:
2171 dt = self.tmax - self.tmin
2172 self.interrupt_following()
2173 self.set_time_range(self.tmin-dt, self.tmax-dt)
2175 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2176 self.interrupt_following()
2178 tgo = None
2180 class TraceDummy(object):
2181 def __init__(self, marker):
2182 self._marker = marker
2184 @property
2185 def nslc_id(self):
2186 return self._marker.one_nslc()
2188 def marker_to_itrack(marker):
2189 try:
2190 return self.key_to_row.get(
2191 self.gather(TraceDummy(marker)), -1)
2193 except MarkerOneNSLCRequired:
2194 return -1
2196 emarker, pmarkers = self.get_active_markers()
2197 pmarkers = [
2198 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2199 pmarkers.sort(key=lambda m: (
2200 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2202 if key == qc.Qt.Key_Backtab:
2203 pmarkers.reverse()
2205 smarkers = self.selected_markers()
2206 iselected = []
2207 for sm in smarkers:
2208 try:
2209 iselected.append(pmarkers.index(sm))
2210 except ValueError:
2211 pass
2213 if iselected:
2214 icurrent = max(iselected) + 1
2215 else:
2216 icurrent = 0
2218 if icurrent < len(pmarkers):
2219 self.deselect_all()
2220 cmarker = pmarkers[icurrent]
2221 cmarker.selected = True
2222 tgo = cmarker.tmin
2223 if not self.tmin < tgo < self.tmax:
2224 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2226 itrack = marker_to_itrack(cmarker)
2227 if itrack != -1:
2228 if itrack < self.shown_tracks_range[0]:
2229 self.scroll_tracks(
2230 - (self.shown_tracks_range[0] - itrack))
2231 elif self.shown_tracks_range[1] <= itrack:
2232 self.scroll_tracks(
2233 itrack - self.shown_tracks_range[1]+1)
2235 if itrack not in self.track_to_nslc_ids:
2236 self.go_to_selection()
2238 elif keytext in ('p', 'n', 'P', 'N'):
2239 smarkers = self.selected_markers()
2240 tgo = None
2241 dir = str(keytext)
2242 if smarkers:
2243 tmid = smarkers[0].tmin
2244 for smarker in smarkers:
2245 if dir == 'n':
2246 tmid = max(smarker.tmin, tmid)
2247 else:
2248 tmid = min(smarker.tmin, tmid)
2250 tgo = tmid
2252 if dir.lower() == 'n':
2253 for marker in sorted(
2254 self.markers,
2255 key=operator.attrgetter('tmin')):
2257 t = marker.tmin
2258 if t > tmid and \
2259 marker.kind in self.visible_marker_kinds and \
2260 (dir == 'n' or
2261 isinstance(marker, EventMarker)):
2263 self.deselect_all()
2264 marker.selected = True
2265 tgo = t
2266 break
2267 else:
2268 for marker in sorted(
2269 self.markers,
2270 key=operator.attrgetter('tmin'),
2271 reverse=True):
2273 t = marker.tmin
2274 if t < tmid and \
2275 marker.kind in self.visible_marker_kinds and \
2276 (dir == 'p' or
2277 isinstance(marker, EventMarker)):
2278 self.deselect_all()
2279 marker.selected = True
2280 tgo = t
2281 break
2283 if tgo is not None:
2284 self.interrupt_following()
2285 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2287 elif keytext == 'r':
2288 if self.pile.reload_modified():
2289 self.reloaded = True
2291 elif keytext == 'R':
2292 self.setup_snufflings()
2294 elif key == qc.Qt.Key_Backspace:
2295 self.remove_selected_markers()
2297 elif keytext == 'a':
2298 for marker in self.markers:
2299 if ((self.tmin <= marker.tmin <= self.tmax or
2300 self.tmin <= marker.tmax <= self.tmax) and
2301 marker.kind in self.visible_marker_kinds):
2302 marker.selected = True
2303 else:
2304 marker.selected = False
2306 elif keytext == 'A':
2307 for marker in self.markers:
2308 if marker.kind in self.visible_marker_kinds:
2309 marker.selected = True
2311 elif keytext == 'd':
2312 self.deselect_all()
2314 elif keytext == 'E':
2315 self.deactivate_event_marker()
2317 elif keytext == 'e':
2318 markers = self.selected_markers()
2319 event_markers_in_spe = [
2320 marker for marker in markers
2321 if not isinstance(marker, PhaseMarker)]
2323 phase_markers = [
2324 marker for marker in markers
2325 if isinstance(marker, PhaseMarker)]
2327 if len(event_markers_in_spe) == 1:
2328 event_marker = event_markers_in_spe[0]
2329 if not isinstance(event_marker, EventMarker):
2330 nslcs = list(event_marker.nslc_ids)
2331 lat, lon = 0.0, 0.0
2332 old = self.get_active_event()
2333 if len(nslcs) == 1:
2334 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2335 elif old is not None:
2336 lat, lon = old.lat, old.lon
2338 event_marker.convert_to_event_marker(lat, lon)
2340 self.set_active_event_marker(event_marker)
2341 event = event_marker.get_event()
2342 for marker in phase_markers:
2343 marker.set_event(event)
2345 else:
2346 for marker in event_markers_in_spe:
2347 marker.convert_to_event_marker()
2349 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2350 for marker in self.selected_markers():
2351 marker.set_kind(int(keytext))
2352 self.emit_selected_markers()
2354 elif key in fkey_map:
2355 self.handle_fkeys(key)
2357 elif key == qc.Qt.Key_Escape:
2358 if self.picking:
2359 self.stop_picking(0, 0, abort=True)
2361 elif key == qc.Qt.Key_PageDown:
2362 self.scroll_tracks(
2363 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2365 elif key == qc.Qt.Key_PageUp:
2366 self.scroll_tracks(
2367 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2369 elif key == qc.Qt.Key_Plus:
2370 self.zoom_tracks(0., 1.)
2372 elif key == qc.Qt.Key_Minus:
2373 self.zoom_tracks(0., -1.)
2375 elif key == qc.Qt.Key_Equal:
2376 ntracks_shown = self.shown_tracks_range[1] - \
2377 self.shown_tracks_range[0]
2378 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2379 self.zoom_tracks(0., dtracks)
2381 elif key == qc.Qt.Key_Colon:
2382 self.want_input.emit()
2384 elif keytext == 'f':
2385 self.toggle_fullscreen()
2387 elif keytext == 'g':
2388 self.go_to_selection()
2390 elif keytext == 'G':
2391 self.go_to_selection(tight=True)
2393 elif keytext == 'm':
2394 self.toggle_marker_editor()
2396 elif keytext == 'c':
2397 self.toggle_main_controls()
2399 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2400 dir = 1
2401 amount = 1
2402 if key_event.key() == qc.Qt.Key_Left:
2403 dir = -1
2404 if key_event.modifiers() & qc.Qt.ShiftModifier:
2405 amount = 10
2406 self.nudge_selected_markers(dir*amount)
2407 else:
2408 super().keyPressEvent(key_event)
2410 if keytext != '' and keytext in 'degaApPnN':
2411 self.emit_selected_markers()
2413 self.update()
2414 self.update_status()
2416 def handle_fkeys(self, key):
2417 self.set_phase_kind(
2418 self.selected_markers(),
2419 fkey_map[key] + 1)
2420 self.emit_selected_markers()
2422 def emit_selected_markers(self):
2423 ibounds = []
2424 last_selected = False
2425 for imarker, marker in enumerate(self.markers):
2426 this_selected = marker.is_selected()
2427 if this_selected != last_selected:
2428 ibounds.append(imarker)
2430 last_selected = this_selected
2432 if last_selected:
2433 ibounds.append(len(self.markers))
2435 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2436 self.n_selected_markers = sum(
2437 chunk[1] - chunk[0] for chunk in chunks)
2438 self.marker_selection_changed.emit(chunks)
2440 def toggle_marker_editor(self):
2441 self.panel_parent.toggle_marker_editor()
2443 def toggle_main_controls(self):
2444 self.panel_parent.toggle_main_controls()
2446 def nudge_selected_markers(self, npixels):
2447 a, b = self.time_projection.ur
2448 c, d = self.time_projection.xr
2449 for marker in self.selected_markers():
2450 if not isinstance(marker, EventMarker):
2451 marker.tmin += npixels * (d-c)/b
2452 marker.tmax += npixels * (d-c)/b
2454 def toggle_fullscreen(self):
2455 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2456 self.window().windowState() & qc.Qt.WindowMaximized:
2457 self.window().showNormal()
2458 else:
2459 if is_macos:
2460 self.window().showMaximized()
2461 else:
2462 self.window().showFullScreen()
2464 def about(self):
2465 fn = pyrocko.util.data_file('snuffler.png')
2466 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2467 txt = f.read()
2468 label = qw.QLabel(txt % {'logo': fn})
2469 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2470 self.show_doc('About', [label], target='tab')
2472 def help(self):
2473 class MyScrollArea(qw.QScrollArea):
2475 def sizeHint(self):
2476 s = qc.QSize()
2477 s.setWidth(self.widget().sizeHint().width())
2478 s.setHeight(self.widget().sizeHint().height())
2479 return s
2481 with open(pyrocko.util.data_file(
2482 'snuffler_help.html')) as f:
2483 hcheat = qw.QLabel(f.read())
2485 with open(pyrocko.util.data_file(
2486 'snuffler_help_epilog.html')) as f:
2487 hepilog = qw.QLabel(f.read())
2489 for h in [hcheat, hepilog]:
2490 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2491 h.setWordWrap(True)
2493 self.show_doc('Help', [hcheat, hepilog], target='panel')
2495 def show_doc(self, name, labels, target='panel'):
2496 scroller = qw.QScrollArea()
2497 frame = qw.QFrame(scroller)
2498 frame.setLineWidth(0)
2499 layout = qw.QVBoxLayout()
2500 layout.setContentsMargins(0, 0, 0, 0)
2501 layout.setSpacing(0)
2502 frame.setLayout(layout)
2503 scroller.setWidget(frame)
2504 scroller.setWidgetResizable(True)
2505 frame.setBackgroundRole(qg.QPalette.Base)
2506 for h in labels:
2507 h.setParent(frame)
2508 h.setMargin(3)
2509 h.setTextInteractionFlags(
2510 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2511 h.setBackgroundRole(qg.QPalette.Base)
2512 layout.addWidget(h)
2513 h.linkActivated.connect(
2514 self.open_link)
2516 if self.panel_parent is not None:
2517 if target == 'panel':
2518 self.panel_parent.add_panel(
2519 name, scroller, True, volatile=False)
2520 else:
2521 self.panel_parent.add_tab(name, scroller)
2523 def open_link(self, link):
2524 qg.QDesktopServices.openUrl(qc.QUrl(link))
2526 def wheelEvent(self, wheel_event):
2527 self.wheel_pos += wheel_event.angleDelta().y()
2529 n = self.wheel_pos // 120
2530 self.wheel_pos = self.wheel_pos % 120
2531 if n == 0:
2532 return
2534 amount = max(
2535 1.,
2536 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2537 wdelta = amount * n
2539 trmin, trmax = self.track_to_screen.get_in_range()
2540 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2541 / (trmax-trmin)
2543 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2544 self.zoom_tracks(anchor, wdelta)
2545 else:
2546 self.scroll_tracks(-wdelta)
2548 def dragEnterEvent(self, event):
2549 if event.mimeData().hasUrls():
2550 if any(url.toLocalFile() for url in event.mimeData().urls()):
2551 event.setDropAction(qc.Qt.LinkAction)
2552 event.accept()
2554 def dropEvent(self, event):
2555 if event.mimeData().hasUrls():
2556 paths = list(
2557 str(url.toLocalFile()) for url in event.mimeData().urls())
2558 event.acceptProposedAction()
2559 self.load(paths)
2561 def get_phase_name(self, kind):
2562 return self.config.get_phase_name(kind)
2564 def set_phase_kind(self, markers, kind):
2565 phasename = self.get_phase_name(kind)
2567 for marker in markers:
2568 if isinstance(marker, PhaseMarker):
2569 if kind == 10:
2570 marker.convert_to_marker()
2571 else:
2572 marker.set_phasename(phasename)
2573 marker.set_event(self.get_active_event())
2575 elif isinstance(marker, EventMarker):
2576 pass
2578 else:
2579 if kind != 10:
2580 event = self.get_active_event()
2581 marker.convert_to_phase_marker(
2582 event, phasename, None, False)
2584 def set_ntracks(self, ntracks):
2585 if self.ntracks != ntracks:
2586 self.ntracks = ntracks
2587 if self.shown_tracks_range is not None:
2588 l, h = self.shown_tracks_range
2589 else:
2590 l, h = 0, self.ntracks
2592 self.tracks_range_changed.emit(self.ntracks, l, h)
2594 def set_tracks_range(self, range, start=None):
2596 low, high = range
2597 low = min(self.ntracks-1, low)
2598 high = min(self.ntracks, high)
2599 low = max(0, low)
2600 high = max(1, high)
2602 if start is None:
2603 start = float(low)
2605 if self.shown_tracks_range != (low, high):
2606 self.shown_tracks_range = low, high
2607 self.shown_tracks_start = start
2609 self.tracks_range_changed.emit(self.ntracks, low, high)
2611 def scroll_tracks(self, shift):
2612 shown = self.shown_tracks_range
2613 shiftmin = -shown[0]
2614 shiftmax = self.ntracks-shown[1]
2615 shift = max(shiftmin, shift)
2616 shift = min(shiftmax, shift)
2617 shown = shown[0] + shift, shown[1] + shift
2619 self.set_tracks_range((int(shown[0]), int(shown[1])))
2621 self.update()
2623 def zoom_tracks(self, anchor, delta):
2624 ntracks_shown = self.shown_tracks_range[1] \
2625 - self.shown_tracks_range[0]
2627 if (ntracks_shown == 1 and delta <= 0) or \
2628 (ntracks_shown == self.ntracks and delta >= 0):
2629 return
2631 ntracks_shown += int(round(delta))
2632 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2634 u = self.shown_tracks_start
2635 nu = max(0., u-anchor*delta)
2636 nv = nu + ntracks_shown
2637 if nv > self.ntracks:
2638 nu -= nv - self.ntracks
2639 nv -= nv - self.ntracks
2641 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2643 self.ntracks_shown_max = self.shown_tracks_range[1] \
2644 - self.shown_tracks_range[0]
2646 self.update()
2648 def content_time_range(self):
2649 pile = self.get_pile()
2650 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2651 if tmin is None:
2652 tmin = initial_time_range[0]
2653 if tmax is None:
2654 tmax = initial_time_range[1]
2656 return tmin, tmax
2658 def content_deltat_range(self):
2659 pile = self.get_pile()
2661 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2663 if deltatmin is None:
2664 deltatmin = 0.001
2666 if deltatmax is None:
2667 deltatmax = 1000.0
2669 return deltatmin, deltatmax
2671 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2672 if tmax < tmin:
2673 tmin, tmax = tmax, tmin
2675 deltatmin = self.content_deltat_range()[0]
2676 dt = deltatmin * self.visible_length * 0.95
2678 if dt == 0.0:
2679 dt = 1.0
2681 if tight:
2682 if tmax != tmin:
2683 dtm = tmax - tmin
2684 tmin -= dtm*0.1
2685 tmax += dtm*0.1
2686 return tmin, tmax
2687 else:
2688 tcenter = (tmin + tmax) / 2.
2689 tmin = tcenter - 0.5*dt
2690 tmax = tcenter + 0.5*dt
2691 return tmin, tmax
2693 if tmax-tmin < dt:
2694 vmin, vmax = self.get_time_range()
2695 dt = min(vmax - vmin, dt)
2697 tcenter = (tmin+tmax)/2.
2698 etmin, etmax = tmin, tmax
2699 tmin = min(etmin, tcenter - 0.5*dt)
2700 tmax = max(etmax, tcenter + 0.5*dt)
2701 dtm = tmax-tmin
2702 if etmin == tmin:
2703 tmin -= dtm*0.1
2704 if etmax == tmax:
2705 tmax += dtm*0.1
2707 else:
2708 dtm = tmax-tmin
2709 tmin -= dtm*0.1
2710 tmax += dtm*0.1
2712 return tmin, tmax
2714 def go_to_selection(self, tight=False):
2715 markers = self.selected_markers()
2716 if markers:
2717 tmax, tmin = self.content_time_range()
2718 for marker in markers:
2719 tmin = min(tmin, marker.tmin)
2720 tmax = max(tmax, marker.tmax)
2722 else:
2723 if tight:
2724 vmin, vmax = self.get_time_range()
2725 tmin = tmax = (vmin + vmax) / 2.
2726 else:
2727 tmin, tmax = self.content_time_range()
2729 tmin, tmax = self.make_good_looking_time_range(
2730 tmin, tmax, tight=tight)
2732 self.interrupt_following()
2733 self.set_time_range(tmin, tmax)
2734 self.update()
2736 def go_to_time(self, t, tlen=None):
2737 tmax = t
2738 if tlen is not None:
2739 tmax = t+tlen
2740 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2741 self.interrupt_following()
2742 self.set_time_range(tmin, tmax)
2743 self.update()
2745 def go_to_event_by_name(self, name):
2746 for marker in self.markers:
2747 if isinstance(marker, EventMarker):
2748 event = marker.get_event()
2749 if event.name and event.name.lower() == name.lower():
2750 tmin, tmax = self.make_good_looking_time_range(
2751 event.time, event.time)
2753 self.interrupt_following()
2754 self.set_time_range(tmin, tmax)
2756 def printit(self):
2757 from .qt_compat import qprint
2758 printer = qprint.QPrinter()
2759 printer.setOrientation(qprint.QPrinter.Landscape)
2761 dialog = qprint.QPrintDialog(printer, self)
2762 dialog.setWindowTitle('Print')
2764 if dialog.exec_() != qw.QDialog.Accepted:
2765 return
2767 painter = qg.QPainter()
2768 painter.begin(printer)
2769 page = printer.pageRect()
2770 self.drawit(
2771 painter, printmode=False, w=page.width(), h=page.height())
2773 painter.end()
2775 def savesvg(self, fn=None):
2777 if not fn:
2778 fn, _ = qw.QFileDialog.getSaveFileName(
2779 self,
2780 'Save as SVG|PNG',
2781 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2782 'SVG|PNG (*.svg *.png)',
2783 options=qfiledialog_options)
2785 if fn == '':
2786 return
2788 fn = str(fn)
2790 if fn.lower().endswith('.svg'):
2791 try:
2792 w, h = 842, 595
2793 margin = 0.025
2794 m = max(w, h)*margin
2796 generator = qsvg.QSvgGenerator()
2797 generator.setFileName(fn)
2798 generator.setSize(qc.QSize(w, h))
2799 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2801 painter = qg.QPainter()
2802 painter.begin(generator)
2803 self.drawit(painter, printmode=False, w=w, h=h)
2804 painter.end()
2806 except Exception as e:
2807 self.fail('Failed to write SVG file: %s' % str(e))
2809 elif fn.lower().endswith('.png'):
2810 pixmap = self.grab()
2812 try:
2813 pixmap.save(fn)
2815 except Exception as e:
2816 self.fail('Failed to write PNG file: %s' % str(e))
2818 else:
2819 self.fail(
2820 'Unsupported file type: filename must end with ".svg" or '
2821 '".png".')
2823 def paintEvent(self, paint_ev):
2824 '''
2825 Called by QT whenever widget needs to be painted.
2826 '''
2827 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2828 # was called twice (by different threads?), causing segfaults.
2829 if self.in_paint_event:
2830 logger.warning('Blocking reentrant call to paintEvent().')
2831 return
2833 self.in_paint_event = True
2835 painter = qg.QPainter(self)
2837 if self.menuitem_antialias.isChecked():
2838 painter.setRenderHint(qg.QPainter.Antialiasing)
2840 self.drawit(painter)
2842 logger.debug(
2843 'Time spent drawing: '
2844 ' user:%.3f sys:%.3f children_user:%.3f'
2845 ' childred_sys:%.3f elapsed:%.3f' %
2846 (self.timer_draw - self.timer_cutout))
2848 logger.debug(
2849 'Time spent processing:'
2850 ' user:%.3f sys:%.3f children_user:%.3f'
2851 ' childred_sys:%.3f elapsed:%.3f' %
2852 self.timer_cutout.get())
2854 self.time_spent_painting = self.timer_draw.get()[-1]
2855 self.time_last_painted = time.time()
2856 self.in_paint_event = False
2858 def determine_box_styles(self):
2860 traces = list(self.pile.iter_traces())
2861 traces.sort(key=operator.attrgetter('full_id'))
2862 istyle = 0
2863 trace_styles = {}
2864 for itr, tr in enumerate(traces):
2865 if itr > 0:
2866 other = traces[itr-1]
2867 if not (
2868 other.nslc_id == tr.nslc_id
2869 and other.deltat == tr.deltat
2870 and abs(other.tmax - tr.tmin)
2871 < gap_lap_tolerance*tr.deltat):
2873 istyle += 1
2875 trace_styles[tr.full_id, tr.deltat] = istyle
2877 self.trace_styles = trace_styles
2879 def draw_trace_boxes(self, p, time_projection, track_projections):
2881 for v_projection in track_projections.values():
2882 v_projection.set_in_range(0., 1.)
2884 def selector(x):
2885 return x.overlaps(*time_projection.get_in_range())
2887 if self.trace_filter is not None:
2888 def tselector(x):
2889 return selector(x) and self.trace_filter(x)
2891 else:
2892 tselector = selector
2894 traces = list(self.pile.iter_traces(
2895 group_selector=selector, trace_selector=tselector))
2897 traces.sort(key=operator.attrgetter('full_id'))
2899 def drawbox(itrack, istyle, traces):
2900 v_projection = track_projections[itrack]
2901 dvmin = v_projection(0.)
2902 dvmax = v_projection(1.)
2903 dtmin = time_projection.clipped(traces[0].tmin, 0)
2904 dtmax = time_projection.clipped(traces[-1].tmax, 1)
2906 style = box_styles[istyle % len(box_styles)]
2907 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2908 p.fillRect(rect, style.fill_brush)
2909 p.setPen(style.frame_pen)
2910 p.drawRect(rect)
2912 traces_by_style = {}
2913 for itr, tr in enumerate(traces):
2914 gt = self.gather(tr)
2915 if gt not in self.key_to_row:
2916 continue
2918 itrack = self.key_to_row[gt]
2919 if itrack not in track_projections:
2920 continue
2922 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2924 if len(traces) < 500:
2925 drawbox(itrack, istyle, [tr])
2926 else:
2927 if (itrack, istyle) not in traces_by_style:
2928 traces_by_style[itrack, istyle] = []
2929 traces_by_style[itrack, istyle].append(tr)
2931 for (itrack, istyle), traces in traces_by_style.items():
2932 drawbox(itrack, istyle, traces)
2934 def draw_visible_markers(
2935 self, p, vcenter_projection, primary_pen):
2937 try:
2938 markers = self.markers.with_key_in_limited(
2939 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2941 except pyrocko.pile.TooMany:
2942 tmin = self.markers[0].tmin
2943 tmax = self.markers[-1].tmax
2944 umin_view, umax_view = self.time_projection.get_out_range()
2945 umin = max(umin_view, self.time_projection(tmin))
2946 umax = min(umax_view, self.time_projection(tmax))
2947 v0, _ = vcenter_projection.get_out_range()
2948 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2950 p.save()
2952 pen = qg.QPen(primary_pen)
2953 pen.setWidth(2)
2954 pen.setStyle(qc.Qt.DotLine)
2955 # pat = [5., 3.]
2956 # pen.setDashPattern(pat)
2957 p.setPen(pen)
2959 if self.n_selected_markers == len(self.markers):
2960 s_selected = ' (all selected)'
2961 elif self.n_selected_markers > 0:
2962 s_selected = ' (%i selected)' % self.n_selected_markers
2963 else:
2964 s_selected = ''
2966 draw_label(
2967 p, umin+10., v0-10.,
2968 '%i Markers' % len(self.markers) + s_selected,
2969 label_bg, 'LB')
2971 line = qc.QLineF(umin, v0, umax, v0)
2972 p.drawLine(line)
2973 p.restore()
2975 return
2977 for marker in markers:
2978 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2979 and marker.kind in self.visible_marker_kinds:
2981 marker.draw(
2982 p, self.time_projection, vcenter_projection,
2983 with_label=True)
2985 def get_squirrel(self):
2986 try:
2987 return self.pile._squirrel
2988 except AttributeError:
2989 return None
2991 def draw_coverage(self, p, time_projection, track_projections):
2992 sq = self.get_squirrel()
2993 if sq is None:
2994 return
2996 def drawbox(itrack, tmin, tmax, style):
2997 v_projection = track_projections[itrack]
2998 dvmin = v_projection(0.)
2999 dvmax = v_projection(1.)
3000 dtmin = time_projection.clipped(tmin, 0)
3001 dtmax = time_projection.clipped(tmax, 1)
3003 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
3004 p.fillRect(rect, style.fill_brush)
3005 p.setPen(style.frame_pen)
3006 p.drawRect(rect)
3008 pattern_list = []
3009 pattern_to_itrack = {}
3010 for key in self.track_keys:
3011 itrack = self.key_to_row[key]
3012 if itrack not in track_projections:
3013 continue
3015 pattern = self.track_patterns[itrack]
3016 pattern_to_itrack[tuple(pattern)] = itrack
3017 pattern_list.append(tuple(pattern))
3019 vmin, vmax = self.get_time_range()
3021 for kind in ['waveform', 'waveform_promise']:
3022 for coverage in sq.get_coverage(
3023 kind, vmin, vmax, pattern_list, limit=500):
3024 itrack = pattern_to_itrack[coverage.pattern.nslc]
3026 if coverage.changes is None:
3027 drawbox(
3028 itrack, coverage.tmin, coverage.tmax,
3029 box_styles_coverage[kind][0])
3030 else:
3031 t = None
3032 pcount = 0
3033 for tb, count in coverage.changes:
3034 if t is not None and tb > t:
3035 if pcount > 0:
3036 drawbox(
3037 itrack, t, tb,
3038 box_styles_coverage[kind][
3039 min(len(box_styles_coverage)-1,
3040 pcount)])
3042 t = tb
3043 pcount = count
3045 def drawit(self, p, printmode=False, w=None, h=None):
3046 '''
3047 This performs the actual drawing.
3048 '''
3050 self.timer_draw.start()
3051 show_boxes = self.menuitem_showboxes.isChecked()
3052 sq = self.get_squirrel()
3054 if self.gather is None:
3055 self.set_gathering()
3057 if self.pile_has_changed:
3059 if not self.sortingmode_change_delayed():
3060 self.sortingmode_change()
3062 if show_boxes and sq is None:
3063 self.determine_box_styles()
3065 self.pile_has_changed = False
3067 if h is None:
3068 h = float(self.height())
3069 if w is None:
3070 w = float(self.width())
3072 if printmode:
3073 primary_color = (0, 0, 0)
3074 else:
3075 primary_color = pyrocko.plot.tango_colors['aluminium5']
3077 primary_pen = qg.QPen(qg.QColor(*primary_color))
3079 ax_h = float(self.ax_height)
3081 vbottom_ax_projection = Projection()
3082 vtop_ax_projection = Projection()
3083 vcenter_projection = Projection()
3085 self.time_projection.set_out_range(0., w)
3086 vbottom_ax_projection.set_out_range(h-ax_h, h)
3087 vtop_ax_projection.set_out_range(0., ax_h)
3088 vcenter_projection.set_out_range(ax_h, h-ax_h)
3089 vcenter_projection.set_in_range(0., 1.)
3090 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3092 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3093 track_projections = {}
3094 for i in range(*self.shown_tracks_range):
3095 proj = Projection()
3096 proj.set_out_range(
3097 self.track_to_screen(i+0.05),
3098 self.track_to_screen(i+1.-0.05))
3100 track_projections[i] = proj
3102 if self.tmin > self.tmax:
3103 return
3105 self.time_projection.set_in_range(self.tmin, self.tmax)
3106 vbottom_ax_projection.set_in_range(0, ax_h)
3108 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3110 yscaler = pyrocko.plot.AutoScaler()
3112 p.setPen(primary_pen)
3114 font = qg.QFont()
3115 font.setBold(True)
3117 axannotfont = qg.QFont()
3118 axannotfont.setBold(True)
3119 axannotfont.setPointSize(8)
3121 processed_traces = self.prepare_cutout2(
3122 self.tmin, self.tmax,
3123 trace_selector=self.trace_selector,
3124 degap=self.menuitem_degap.isChecked(),
3125 demean=self.menuitem_demean.isChecked())
3127 if not printmode and show_boxes:
3128 if (self.view_mode is ViewMode.Wiggle) \
3129 or (self.view_mode is ViewMode.Waterfall
3130 and not processed_traces):
3132 if sq is None:
3133 self.draw_trace_boxes(
3134 p, self.time_projection, track_projections)
3136 else:
3137 self.draw_coverage(
3138 p, self.time_projection, track_projections)
3140 p.setFont(font)
3141 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3143 color_lookup = dict(
3144 [(k, i) for (i, k) in enumerate(self.color_keys)])
3146 self.track_to_nslc_ids = {}
3147 nticks = 0
3148 annot_labels = []
3150 if self.view_mode is ViewMode.Waterfall and processed_traces:
3151 waterfall = self.waterfall
3152 waterfall.set_time_range(self.tmin, self.tmax)
3153 waterfall.set_traces(processed_traces)
3154 waterfall.set_cmap(self.waterfall_cmap)
3155 waterfall.set_integrate(self.waterfall_integrate)
3156 waterfall.set_clip(
3157 self.waterfall_clip_min, self.waterfall_clip_max)
3158 waterfall.show_absolute_values(
3159 self.waterfall_show_absolute)
3161 rect = qc.QRectF(
3162 0, self.ax_height,
3163 self.width(), self.height() - self.ax_height*2
3164 )
3165 waterfall.draw_waterfall(p, rect=rect)
3167 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3168 show_scales = self.menuitem_showscalerange.isChecked() \
3169 or self.menuitem_showscaleaxis.isChecked()
3171 fm = qg.QFontMetrics(axannotfont, p.device())
3172 trackheight = self.track_to_screen(1.-0.05) \
3173 - self.track_to_screen(0.05)
3175 nlinesavail = trackheight/float(fm.lineSpacing())
3177 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3178 if self.menuitem_showscaleaxis.isChecked() \
3179 else 15
3181 yscaler = pyrocko.plot.AutoScaler(
3182 no_exp_interval=(-3, 2), approx_ticks=nticks,
3183 snap=show_scales
3184 and not self.menuitem_showscaleaxis.isChecked())
3186 data_ranges = pyrocko.trace.minmax(
3187 processed_traces,
3188 key=self.scaling_key,
3189 mode=self.scaling_base[0],
3190 outer_mode=self.scaling_base[1])
3192 if not self.menuitem_fixscalerange.isChecked():
3193 self.old_data_ranges = data_ranges
3194 else:
3195 data_ranges.update(self.old_data_ranges)
3197 self.apply_scaling_hooks(data_ranges)
3199 trace_to_itrack = {}
3200 track_scaling_keys = {}
3201 track_scaling_colors = {}
3202 for trace in processed_traces:
3203 gt = self.gather(trace)
3204 if gt not in self.key_to_row:
3205 continue
3207 itrack = self.key_to_row[gt]
3208 if itrack not in track_projections:
3209 continue
3211 trace_to_itrack[trace] = itrack
3213 if itrack not in self.track_to_nslc_ids:
3214 self.track_to_nslc_ids[itrack] = set()
3216 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3218 if itrack not in track_scaling_keys:
3219 track_scaling_keys[itrack] = set()
3221 scaling_key = self.scaling_key(trace)
3222 track_scaling_keys[itrack].add(scaling_key)
3224 color = pyrocko.plot.color(
3225 color_lookup[self.color_gather(trace)])
3227 k = itrack, scaling_key
3228 if k not in track_scaling_colors \
3229 and self.menuitem_colortraces.isChecked():
3230 track_scaling_colors[k] = color
3231 else:
3232 track_scaling_colors[k] = primary_color
3234 # y axes, zero lines
3235 trace_projections = {}
3236 for itrack in list(track_projections.keys()):
3237 if itrack not in track_scaling_keys:
3238 continue
3239 uoff = 0
3240 for scaling_key in track_scaling_keys[itrack]:
3241 data_range = data_ranges[scaling_key]
3242 dymin, dymax = data_range
3243 ymin, ymax, yinc = yscaler.make_scale(
3244 (dymin/self.gain, dymax/self.gain))
3245 iexp = yscaler.make_exp(yinc)
3246 factor = 10**iexp
3247 trace_projection = track_projections[itrack].copy()
3248 trace_projection.set_in_range(ymax, ymin)
3249 trace_projections[itrack, scaling_key] = \
3250 trace_projection
3251 umin, umax = self.time_projection.get_out_range()
3252 vmin, vmax = trace_projection.get_out_range()
3253 umax_zeroline = umax
3254 uoffnext = uoff
3256 if show_scales:
3257 pen = qg.QPen(primary_pen)
3258 k = itrack, scaling_key
3259 if k in track_scaling_colors:
3260 c = qg.QColor(*track_scaling_colors[
3261 itrack, scaling_key])
3263 pen.setColor(c)
3265 p.setPen(pen)
3266 if nlinesavail > 3:
3267 if self.menuitem_showscaleaxis.isChecked():
3268 ymin_annot = math.ceil(ymin/yinc)*yinc
3269 ny_annot = int(
3270 math.floor(ymax/yinc)
3271 - math.ceil(ymin/yinc)) + 1
3273 for iy_annot in range(ny_annot):
3274 y = ymin_annot + iy_annot*yinc
3275 v = trace_projection(y)
3276 line = qc.QLineF(
3277 umax-10-uoff, v, umax-uoff, v)
3279 p.drawLine(line)
3280 if iy_annot == ny_annot - 1 \
3281 and iexp != 0:
3282 sexp = ' × ' \
3283 '10<sup>%i</sup>' % iexp
3284 else:
3285 sexp = ''
3287 snum = num_to_html(y/factor)
3288 lab = Label(
3289 p,
3290 umax-20-uoff,
3291 v, '%s%s' % (snum, sexp),
3292 label_bg=None,
3293 anchor='MR',
3294 font=axannotfont,
3295 color=c)
3297 uoffnext = max(
3298 lab.rect.width()+30., uoffnext)
3300 annot_labels.append(lab)
3301 if y == 0.:
3302 umax_zeroline = \
3303 umax - 20 \
3304 - lab.rect.width() - 10 \
3305 - uoff
3306 else:
3307 if not show_boxes:
3308 qpoints = make_QPolygonF(
3309 [umax-20-uoff,
3310 umax-10-uoff,
3311 umax-10-uoff,
3312 umax-20-uoff],
3313 [vmax, vmax, vmin, vmin])
3314 p.drawPolyline(qpoints)
3316 snum = num_to_html(ymin)
3317 labmin = Label(
3318 p, umax-15-uoff, vmax, snum,
3319 label_bg=None,
3320 anchor='BR',
3321 font=axannotfont,
3322 color=c)
3324 annot_labels.append(labmin)
3325 snum = num_to_html(ymax)
3326 labmax = Label(
3327 p, umax-15-uoff, vmin, snum,
3328 label_bg=None,
3329 anchor='TR',
3330 font=axannotfont,
3331 color=c)
3333 annot_labels.append(labmax)
3335 for lab in (labmin, labmax):
3336 uoffnext = max(
3337 lab.rect.width()+10., uoffnext)
3339 if self.menuitem_showzeroline.isChecked():
3340 v = trace_projection(0.)
3341 if vmin <= v <= vmax:
3342 line = qc.QLineF(umin, v, umax_zeroline, v)
3343 p.drawLine(line)
3345 uoff = uoffnext
3347 p.setFont(font)
3348 p.setPen(primary_pen)
3349 for trace in processed_traces:
3350 if self.view_mode is not ViewMode.Wiggle:
3351 break
3353 if trace not in trace_to_itrack:
3354 continue
3356 itrack = trace_to_itrack[trace]
3357 scaling_key = self.scaling_key(trace)
3358 trace_projection = trace_projections[
3359 itrack, scaling_key]
3361 vdata = trace_projection(trace.get_ydata())
3363 udata_min = float(self.time_projection(trace.tmin))
3364 udata_max = float(self.time_projection(
3365 trace.tmin+trace.deltat*(vdata.size-1)))
3366 udata = num.linspace(udata_min, udata_max, vdata.size)
3368 qpoints = make_QPolygonF(udata, vdata)
3370 umin, umax = self.time_projection.get_out_range()
3371 vmin, vmax = trace_projection.get_out_range()
3373 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3375 if self.menuitem_cliptraces.isChecked():
3376 p.setClipRect(trackrect)
3378 if self.menuitem_colortraces.isChecked():
3379 color = pyrocko.plot.color(
3380 color_lookup[self.color_gather(trace)])
3381 pen = qg.QPen(qg.QColor(*color), 1)
3382 p.setPen(pen)
3384 p.drawPolyline(qpoints)
3386 if self.floating_marker:
3387 self.floating_marker.draw_trace(
3388 self, p, trace,
3389 self.time_projection, trace_projection, 1.0)
3391 for marker in self.markers.with_key_in(
3392 self.tmin - self.markers_deltat_max,
3393 self.tmax):
3395 if marker.tmin < self.tmax \
3396 and self.tmin < marker.tmax \
3397 and marker.kind \
3398 in self.visible_marker_kinds:
3399 marker.draw_trace(
3400 self, p, trace, self.time_projection,
3401 trace_projection, 1.0)
3403 p.setPen(primary_pen)
3405 if self.menuitem_cliptraces.isChecked():
3406 p.setClipRect(0, 0, int(w), int(h))
3408 if self.floating_marker:
3409 self.floating_marker.draw(
3410 p, self.time_projection, vcenter_projection)
3412 self.draw_visible_markers(
3413 p, vcenter_projection, primary_pen)
3415 p.setPen(primary_pen)
3416 while font.pointSize() > 2:
3417 fm = qg.QFontMetrics(font, p.device())
3418 trackheight = self.track_to_screen(1.-0.05) \
3419 - self.track_to_screen(0.05)
3420 nlinesavail = trackheight/float(fm.lineSpacing())
3421 if nlinesavail > 1:
3422 break
3424 font.setPointSize(font.pointSize()-1)
3426 p.setFont(font)
3427 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3429 for key in self.track_keys:
3430 itrack = self.key_to_row[key]
3431 if itrack in track_projections:
3432 plabel = ' '.join(
3433 [str(x) for x in key if x is not None])
3434 lx = 10
3435 ly = self.track_to_screen(itrack+0.5)
3437 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3438 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3439 continue
3441 contains_cursor = \
3442 self.track_to_screen(itrack) \
3443 < mouse_pos.y() \
3444 < self.track_to_screen(itrack+1)
3446 if not contains_cursor:
3447 continue
3449 font_large = p.font()
3450 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3451 p.setFont(font_large)
3452 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3453 p.setFont(font)
3455 for lab in annot_labels:
3456 lab.draw()
3458 self.timer_draw.stop()
3460 def see_data_params(self):
3462 min_deltat = self.content_deltat_range()[0]
3464 # determine padding and downampling requirements
3465 if self.lowpass is not None:
3466 deltat_target = 1./self.lowpass * 0.25
3467 ndecimate = min(
3468 50,
3469 max(1, int(round(deltat_target / min_deltat))))
3470 tpad = 1./self.lowpass * 2.
3471 else:
3472 ndecimate = 1
3473 tpad = min_deltat*5.
3475 if self.highpass is not None:
3476 tpad = max(1./self.highpass * 2., tpad)
3478 nsee_points_per_trace = 5000*10
3479 tsee = ndecimate*nsee_points_per_trace*min_deltat
3481 return ndecimate, tpad, tsee
3483 def clean_update(self):
3484 self.cached_processed_traces = None
3485 self.update()
3487 def get_adequate_tpad(self):
3488 tpad = 0.
3489 for f in [self.highpass, self.lowpass]:
3490 if f is not None:
3491 tpad = max(tpad, 1.0/f)
3493 for snuffling in self.snufflings:
3494 if snuffling._post_process_hook_enabled \
3495 or snuffling._pre_process_hook_enabled:
3497 tpad = max(tpad, snuffling.get_tpad())
3499 return tpad
3501 def prepare_cutout2(
3502 self, tmin, tmax, trace_selector=None, degap=True,
3503 demean=True, nmax=6000):
3505 if self.pile.is_empty():
3506 return []
3508 nmax = self.visible_length
3510 self.timer_cutout.start()
3512 tsee = tmax-tmin
3513 min_deltat_wo_decimate = tsee/nmax
3514 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3516 min_deltat_allow = min_deltat_wo_decimate
3517 if self.lowpass is not None:
3518 target_deltat_lp = 0.25/self.lowpass
3519 if target_deltat_lp > min_deltat_wo_decimate:
3520 min_deltat_allow = min_deltat_w_decimate
3522 min_deltat_allow = math.exp(
3523 int(math.floor(math.log(min_deltat_allow))))
3525 tmin_ = tmin
3526 tmax_ = tmax
3528 # fetch more than needed?
3529 if self.menuitem_liberal_fetch.isChecked():
3530 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3531 tmin = math.floor(tmin/tlen) * tlen
3532 tmax = math.ceil(tmax/tlen) * tlen
3534 fft_filtering = self.menuitem_fft_filtering.isChecked()
3535 lphp = self.menuitem_lphp.isChecked()
3536 ads = self.menuitem_allowdownsampling.isChecked()
3538 tpad = self.get_adequate_tpad()
3539 tpad = max(tpad, tsee)
3541 # state vector to decide if cached traces can be used
3542 vec = (
3543 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3544 self.highpass, fft_filtering, lphp,
3545 min_deltat_allow, self.rotate, self.shown_tracks_range,
3546 ads, self.pile.get_update_count())
3548 if (self.cached_vec
3549 and self.cached_vec[0] <= vec[0]
3550 and vec[1] <= self.cached_vec[1]
3551 and vec[2:] == self.cached_vec[2:]
3552 and not (self.reloaded or self.menuitem_watch.isChecked())
3553 and self.cached_processed_traces is not None):
3555 logger.debug('Using cached traces')
3556 processed_traces = self.cached_processed_traces
3558 else:
3559 processed_traces = []
3560 if self.pile.deltatmax >= min_deltat_allow:
3562 if isinstance(self.pile, pyrocko.pile.Pile):
3563 def group_selector(gr):
3564 return gr.deltatmax >= min_deltat_allow
3566 kwargs = dict(group_selector=group_selector)
3567 else:
3568 kwargs = {}
3570 if trace_selector is not None:
3571 def trace_selectorx(tr):
3572 return tr.deltat >= min_deltat_allow \
3573 and trace_selector(tr)
3574 else:
3575 def trace_selectorx(tr):
3576 return tr.deltat >= min_deltat_allow
3578 for traces in self.pile.chopper(
3579 tmin=tmin, tmax=tmax, tpad=tpad,
3580 want_incomplete=True,
3581 degap=degap,
3582 maxgap=gap_lap_tolerance,
3583 maxlap=gap_lap_tolerance,
3584 keep_current_files_open=True,
3585 trace_selector=trace_selectorx,
3586 accessor_id=id(self),
3587 snap=(math.floor, math.ceil),
3588 include_last=True, **kwargs):
3590 if demean:
3591 for tr in traces:
3592 if (tr.meta and tr.meta.get('tabu', False)):
3593 continue
3594 y = tr.get_ydata()
3595 tr.set_ydata(y - num.mean(y))
3597 traces = self.pre_process_hooks(traces)
3599 for trace in traces:
3601 if not (trace.meta
3602 and trace.meta.get('tabu', False)):
3604 if fft_filtering:
3605 but = pyrocko.response.ButterworthResponse
3606 multres = pyrocko.response.MultiplyResponse
3607 if self.lowpass is not None \
3608 or self.highpass is not None:
3610 it = num.arange(
3611 trace.data_len(), dtype=float)
3612 detr_data, m, b = detrend(
3613 it, trace.get_ydata())
3615 trace.set_ydata(detr_data)
3617 freqs, fdata = trace.spectrum(
3618 pad_to_pow2=True, tfade=None)
3620 nfreqs = fdata.size
3622 key = (trace.deltat, nfreqs)
3624 if key not in self.tf_cache:
3625 resps = []
3626 if self.lowpass is not None:
3627 resps.append(but(
3628 order=4,
3629 corner=self.lowpass,
3630 type='low'))
3632 if self.highpass is not None:
3633 resps.append(but(
3634 order=4,
3635 corner=self.highpass,
3636 type='high'))
3638 resp = multres(resps)
3639 self.tf_cache[key] = \
3640 resp.evaluate(freqs)
3642 filtered_data = num.fft.irfft(
3643 fdata*self.tf_cache[key]
3644 )[:trace.data_len()]
3646 retrended_data = retrend(
3647 it, filtered_data, m, b)
3649 trace.set_ydata(retrended_data)
3651 else:
3653 if ads and self.lowpass is not None:
3654 while trace.deltat \
3655 < min_deltat_wo_decimate:
3657 trace.downsample(2, demean=False)
3659 fmax = 0.5/trace.deltat
3660 if not lphp and (
3661 self.lowpass is not None
3662 and self.highpass is not None
3663 and self.lowpass < fmax
3664 and self.highpass < fmax
3665 and self.highpass < self.lowpass):
3667 trace.bandpass(
3668 2, self.highpass, self.lowpass)
3669 else:
3670 if self.lowpass is not None:
3671 if self.lowpass < 0.5/trace.deltat:
3672 trace.lowpass(
3673 4, self.lowpass,
3674 demean=False)
3676 if self.highpass is not None:
3677 if self.lowpass is None \
3678 or self.highpass \
3679 < self.lowpass:
3681 if self.highpass < \
3682 0.5/trace.deltat:
3683 trace.highpass(
3684 4, self.highpass,
3685 demean=False)
3687 processed_traces.append(trace)
3689 if self.rotate != 0.0:
3690 phi = self.rotate/180.*math.pi
3691 cphi = math.cos(phi)
3692 sphi = math.sin(phi)
3693 for a in processed_traces:
3694 for b in processed_traces:
3695 if (a.network == b.network
3696 and a.station == b.station
3697 and a.location == b.location
3698 and ((a.channel.lower().endswith('n')
3699 and b.channel.lower().endswith('e'))
3700 or (a.channel.endswith('1')
3701 and b.channel.endswith('2')))
3702 and abs(a.deltat-b.deltat) < a.deltat*0.001
3703 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3704 len(a.get_ydata()) == len(b.get_ydata())):
3706 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3707 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3708 a.set_ydata(aydata)
3709 b.set_ydata(bydata)
3711 processed_traces = self.post_process_hooks(processed_traces)
3713 self.cached_processed_traces = processed_traces
3714 self.cached_vec = vec
3716 chopped_traces = []
3717 for trace in processed_traces:
3718 chop_tmin = tmin_ - trace.deltat*4
3719 chop_tmax = tmax_ + trace.deltat*4
3721 try:
3722 ctrace = trace.chop(
3723 chop_tmin, chop_tmax,
3724 inplace=False)
3726 except pyrocko.trace.NoData:
3727 continue
3729 if ctrace.data_len() < 2:
3730 continue
3732 chopped_traces.append(ctrace)
3734 self.timer_cutout.stop()
3735 return chopped_traces
3737 def pre_process_hooks(self, traces):
3738 for snuffling in self.snufflings:
3739 if snuffling._pre_process_hook_enabled:
3740 traces = snuffling.pre_process_hook(traces)
3742 return traces
3744 def post_process_hooks(self, traces):
3745 for snuffling in self.snufflings:
3746 if snuffling._post_process_hook_enabled:
3747 traces = snuffling.post_process_hook(traces)
3749 return traces
3751 def visible_length_change(self, ignore=None):
3752 for menuitem, vlen in self.menuitems_visible_length:
3753 if menuitem.isChecked():
3754 self.visible_length = vlen
3756 def scaling_base_change(self, ignore=None):
3757 for menuitem, scaling_base in self.menuitems_scaling_base:
3758 if menuitem.isChecked():
3759 self.scaling_base = scaling_base
3761 def scalingmode_change(self, ignore=None):
3762 for menuitem, scaling_key in self.menuitems_scaling:
3763 if menuitem.isChecked():
3764 self.scaling_key = scaling_key
3765 self.update()
3767 def apply_scaling_hooks(self, data_ranges):
3768 for k in sorted(self.scaling_hooks.keys()):
3769 hook = self.scaling_hooks[k]
3770 hook(data_ranges)
3772 def viewmode_change(self, ignore=True):
3773 for item, mode in self.menuitems_viewmode:
3774 if item.isChecked():
3775 self.view_mode = mode
3776 break
3777 else:
3778 raise AttributeError('unknown view mode')
3780 items_waterfall_disabled = (
3781 self.menuitem_showscaleaxis,
3782 self.menuitem_showscalerange,
3783 self.menuitem_showzeroline,
3784 self.menuitem_colortraces,
3785 self.menuitem_cliptraces,
3786 *(itm[0] for itm in self.menuitems_visible_length)
3787 )
3789 if self.view_mode is ViewMode.Waterfall:
3790 self.parent().show_colorbar_ctrl(True)
3791 self.parent().show_gain_ctrl(False)
3793 for item in items_waterfall_disabled:
3794 item.setDisabled(True)
3796 self.visible_length = 180.
3797 else:
3798 self.parent().show_colorbar_ctrl(False)
3799 self.parent().show_gain_ctrl(True)
3801 for item in items_waterfall_disabled:
3802 item.setDisabled(False)
3804 self.visible_length_change()
3805 self.update()
3807 def set_scaling_hook(self, k, hook):
3808 self.scaling_hooks[k] = hook
3810 def remove_scaling_hook(self, k):
3811 del self.scaling_hooks[k]
3813 def remove_scaling_hooks(self):
3814 self.scaling_hooks = {}
3816 def s_sortingmode_change(self, ignore=None):
3817 for menuitem, valfunc in self.menuitems_ssorting:
3818 if menuitem.isChecked():
3819 self._ssort = valfunc
3821 self.sortingmode_change()
3823 def sortingmode_change(self, ignore=None):
3824 for menuitem, (gather, color) in self.menuitems_sorting:
3825 if menuitem.isChecked():
3826 self.set_gathering(gather, color)
3828 self.sortingmode_change_time = time.time()
3830 def lowpass_change(self, value, ignore=None):
3831 self.lowpass = value
3832 self.passband_check()
3833 self.tf_cache = {}
3834 self.update()
3836 def highpass_change(self, value, ignore=None):
3837 self.highpass = value
3838 self.passband_check()
3839 self.tf_cache = {}
3840 self.update()
3842 def passband_check(self):
3843 if self.highpass and self.lowpass \
3844 and self.highpass >= self.lowpass:
3846 self.message = 'Corner frequency of highpass larger than ' \
3847 'corner frequency of lowpass! I will now ' \
3848 'deactivate the highpass.'
3850 self.update_status()
3851 else:
3852 oldmess = self.message
3853 self.message = None
3854 if oldmess is not None:
3855 self.update_status()
3857 def gain_change(self, value, ignore):
3858 self.gain = value
3859 self.update()
3861 def rot_change(self, value, ignore):
3862 self.rotate = value
3863 self.update()
3865 def waterfall_cmap_change(self, cmap):
3866 self.waterfall_cmap = cmap
3867 self.update()
3869 def waterfall_clip_change(self, clip_min, clip_max):
3870 self.waterfall_clip_min = clip_min
3871 self.waterfall_clip_max = clip_max
3872 self.update()
3874 def waterfall_show_absolute_change(self, toggle):
3875 self.waterfall_show_absolute = toggle
3876 self.update()
3878 def waterfall_set_integrate(self, toggle):
3879 self.waterfall_integrate = toggle
3880 self.update()
3882 def set_selected_markers(self, markers):
3883 '''
3884 Set a list of markers selected
3886 :param markers: list of markers
3887 '''
3888 self.deselect_all()
3889 for m in markers:
3890 m.selected = True
3892 self.update()
3894 def deselect_all(self):
3895 for marker in self.markers:
3896 marker.selected = False
3898 def animate_picking(self):
3899 point = self.mapFromGlobal(qg.QCursor.pos())
3900 self.update_picking(point.x(), point.y(), doshift=True)
3902 def get_nslc_ids_for_track(self, ftrack):
3903 itrack = int(ftrack)
3904 return self.track_to_nslc_ids.get(itrack, [])
3906 def stop_picking(self, x, y, abort=False):
3907 if self.picking:
3908 self.update_picking(x, y, doshift=False)
3909 self.picking = None
3910 self.picking_down = None
3911 self.picking_timer.stop()
3912 self.picking_timer = None
3913 if not abort:
3914 self.add_marker(self.floating_marker)
3915 self.floating_marker.selected = True
3916 self.emit_selected_markers()
3918 self.floating_marker = None
3920 def start_picking(self, ignore):
3922 if not self.picking:
3923 self.deselect_all()
3924 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3925 point = self.mapFromGlobal(qg.QCursor.pos())
3927 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3928 self.picking.setGeometry(
3929 gpoint.x(), gpoint.y(), 1, self.height())
3930 t = self.time_projection.rev(point.x())
3932 ftrack = self.track_to_screen.rev(point.y())
3933 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3934 self.floating_marker = Marker(nslc_ids, t, t)
3935 self.floating_marker.selected = True
3937 self.picking_timer = qc.QTimer()
3938 self.picking_timer.timeout.connect(
3939 self.animate_picking)
3941 self.picking_timer.setInterval(50)
3942 self.picking_timer.start()
3944 def update_picking(self, x, y, doshift=False):
3945 if self.picking:
3946 mouset = self.time_projection.rev(x)
3947 dt = 0.0
3948 if mouset < self.tmin or mouset > self.tmax:
3949 if mouset < self.tmin:
3950 dt = -(self.tmin - mouset)
3951 else:
3952 dt = mouset - self.tmax
3953 ddt = self.tmax-self.tmin
3954 dt = max(dt, -ddt/10.)
3955 dt = min(dt, ddt/10.)
3957 x0 = x
3958 if self.picking_down is not None:
3959 x0 = self.time_projection(self.picking_down[0])
3961 w = abs(x-x0)
3962 x0 = min(x0, x)
3964 tmin, tmax = (
3965 self.time_projection.rev(x0),
3966 self.time_projection.rev(x0+w))
3968 tmin, tmax = (
3969 max(working_system_time_range[0], tmin),
3970 min(working_system_time_range[1], tmax))
3972 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3974 self.picking.setGeometry(
3975 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3977 ftrack = self.track_to_screen.rev(y)
3978 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3979 self.floating_marker.set(nslc_ids, tmin, tmax)
3981 if dt != 0.0 and doshift:
3982 self.interrupt_following()
3983 self.set_time_range(self.tmin+dt, self.tmax+dt)
3985 self.update()
3987 def update_status(self):
3989 if self.message is None:
3990 point = self.mapFromGlobal(qg.QCursor.pos())
3992 mouse_t = self.time_projection.rev(point.x())
3993 if not is_working_time(mouse_t):
3994 return
3996 if self.floating_marker:
3997 tmi, tma = (
3998 self.floating_marker.tmin,
3999 self.floating_marker.tmax)
4001 tt, ms = gmtime_x(tmi)
4003 if tmi == tma:
4004 message = mystrftime(
4005 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
4006 tt=tt, milliseconds=ms)
4007 else:
4008 srange = '%g s' % (tma-tmi)
4009 message = mystrftime(
4010 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
4011 tt=tt, milliseconds=ms)
4012 else:
4013 tt, ms = gmtime_x(mouse_t)
4015 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
4016 else:
4017 message = self.message
4019 sb = self.window().statusBar()
4020 sb.clearMessage()
4021 sb.showMessage(message)
4023 def set_sortingmode_change_delay_time(self, dt):
4024 self.sortingmode_change_delay_time = dt
4026 def sortingmode_change_delayed(self):
4027 now = time.time()
4028 return (
4029 self.sortingmode_change_delay_time is not None
4030 and now - self.sortingmode_change_time
4031 < self.sortingmode_change_delay_time)
4033 def set_visible_marker_kinds(self, kinds):
4034 self.deselect_all()
4035 self.visible_marker_kinds = tuple(kinds)
4036 self.emit_selected_markers()
4038 def following(self):
4039 return self.follow_timer is not None \
4040 and not self.following_interrupted()
4042 def interrupt_following(self):
4043 self.interactive_range_change_time = time.time()
4045 def following_interrupted(self, now=None):
4046 if now is None:
4047 now = time.time()
4048 return now - self.interactive_range_change_time \
4049 < self.interactive_range_change_delay_time
4051 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4052 if tmax_start is None:
4053 tmax_start = time.time()
4054 self.show_all = False
4055 self.follow_time = tlen
4056 self.follow_timer = qc.QTimer(self)
4057 self.follow_timer.timeout.connect(
4058 self.follow_update)
4059 self.follow_timer.setInterval(interval)
4060 self.follow_timer.start()
4061 self.follow_started = time.time()
4062 self.follow_lapse = lapse
4063 self.follow_tshift = self.follow_started - tmax_start
4064 self.interactive_range_change_time = 0.0
4066 def unfollow(self):
4067 if self.follow_timer is not None:
4068 self.follow_timer.stop()
4069 self.follow_timer = None
4070 self.interactive_range_change_time = 0.0
4072 def follow_update(self):
4073 rnow = time.time()
4074 if self.follow_lapse is None:
4075 now = rnow
4076 else:
4077 now = self.follow_started + (rnow - self.follow_started) \
4078 * self.follow_lapse
4080 if self.following_interrupted(rnow):
4081 return
4082 self.set_time_range(
4083 now-self.follow_time-self.follow_tshift,
4084 now-self.follow_tshift)
4086 self.update()
4088 def myclose(self, return_tag=''):
4089 self.return_tag = return_tag
4090 self.window().close()
4092 def cleanup(self):
4093 self.about_to_close.emit()
4094 self.timer.stop()
4095 if self.follow_timer is not None:
4096 self.follow_timer.stop()
4098 for snuffling in list(self.snufflings):
4099 self.remove_snuffling(snuffling)
4101 def set_error_message(self, key, value):
4102 if value is None:
4103 if key in self.error_messages:
4104 del self.error_messages[key]
4105 else:
4106 self.error_messages[key] = value
4108 def inputline_changed(self, text):
4109 pass
4111 def inputline_finished(self, text):
4112 line = str(text)
4114 toks = line.split()
4115 clearit, hideit, error = False, True, None
4116 if len(toks) >= 1:
4117 command = toks[0].lower()
4119 try:
4120 quick_filter_commands = {
4121 'n': '%s.*.*.*',
4122 's': '*.%s.*.*',
4123 'l': '*.*.%s.*',
4124 'c': '*.*.*.%s'}
4126 if command in quick_filter_commands:
4127 if len(toks) >= 2:
4128 patterns = [
4129 quick_filter_commands[toks[0]] % pat
4130 for pat in toks[1:]]
4131 self.set_quick_filter_patterns(patterns, line)
4132 else:
4133 self.set_quick_filter_patterns(None)
4135 self.update()
4137 elif command in ('hide', 'unhide'):
4138 if len(toks) >= 2:
4139 patterns = []
4140 if len(toks) == 2:
4141 patterns = [toks[1]]
4142 elif len(toks) >= 3:
4143 x = {
4144 'n': '%s.*.*.*',
4145 's': '*.%s.*.*',
4146 'l': '*.*.%s.*',
4147 'c': '*.*.*.%s'}
4149 if toks[1] in x:
4150 patterns.extend(
4151 x[toks[1]] % tok for tok in toks[2:])
4153 for pattern in patterns:
4154 if command == 'hide':
4155 self.add_blacklist_pattern(pattern)
4156 else:
4157 self.remove_blacklist_pattern(pattern)
4159 elif command == 'unhide' and len(toks) == 1:
4160 self.clear_blacklist()
4162 clearit = True
4164 self.update()
4166 elif command == 'markers':
4167 if len(toks) == 2:
4168 if toks[1] == 'all':
4169 kinds = self.all_marker_kinds
4170 else:
4171 kinds = []
4172 for x in toks[1]:
4173 try:
4174 kinds.append(int(x))
4175 except Exception:
4176 pass
4178 self.set_visible_marker_kinds(kinds)
4180 elif len(toks) == 1:
4181 self.set_visible_marker_kinds(())
4183 self.update()
4185 elif command == 'scaling':
4186 if len(toks) == 2:
4187 hideit = False
4188 error = 'wrong number of arguments'
4190 if len(toks) >= 3:
4191 vmin, vmax = [
4192 pyrocko.model.float_or_none(x)
4193 for x in toks[-2:]]
4195 def upd(d, k, vmin, vmax):
4196 if k in d:
4197 if vmin is not None:
4198 d[k] = vmin, d[k][1]
4199 if vmax is not None:
4200 d[k] = d[k][0], vmax
4202 if len(toks) == 1:
4203 self.remove_scaling_hooks()
4205 elif len(toks) == 3:
4206 def hook(data_ranges):
4207 for k in data_ranges:
4208 upd(data_ranges, k, vmin, vmax)
4210 self.set_scaling_hook('_', hook)
4212 elif len(toks) == 4:
4213 pattern = toks[1]
4215 def hook(data_ranges):
4216 for k in pyrocko.util.match_nslcs(
4217 pattern, list(data_ranges.keys())):
4219 upd(data_ranges, k, vmin, vmax)
4221 self.set_scaling_hook(pattern, hook)
4223 elif command == 'goto':
4224 toks2 = line.split(None, 1)
4225 if len(toks2) == 2:
4226 arg = toks2[1]
4227 m = re.match(
4228 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4229 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4230 if m:
4231 tlen = None
4232 if not m.group(1):
4233 tlen = 12*32*24*60*60
4234 elif not m.group(2):
4235 tlen = 32*24*60*60
4236 elif not m.group(3):
4237 tlen = 24*60*60
4238 elif not m.group(4):
4239 tlen = 60*60
4240 elif not m.group(5):
4241 tlen = 60
4243 supl = '1970-01-01 00:00:00'
4244 if len(supl) > len(arg):
4245 arg = arg + supl[-(len(supl)-len(arg)):]
4246 t = pyrocko.util.str_to_time(arg)
4247 self.go_to_time(t, tlen=tlen)
4249 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4250 supl = '00:00:00'
4251 if len(supl) > len(arg):
4252 arg = arg + supl[-(len(supl)-len(arg)):]
4253 tmin, tmax = self.get_time_range()
4254 sdate = pyrocko.util.time_to_str(
4255 tmin/2.+tmax/2., format='%Y-%m-%d')
4256 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4257 self.go_to_time(t)
4259 elif arg == 'today':
4260 self.go_to_time(
4261 day_start(
4262 time.time()), tlen=24*60*60)
4264 elif arg == 'yesterday':
4265 self.go_to_time(
4266 day_start(
4267 time.time()-24*60*60), tlen=24*60*60)
4269 else:
4270 self.go_to_event_by_name(arg)
4272 else:
4273 raise PileViewerMainException(
4274 'No such command: %s' % command)
4276 except PileViewerMainException as e:
4277 error = str(e)
4278 hideit = False
4280 return clearit, hideit, error
4282 return PileViewerMain
4285PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4286GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4289class LineEditWithAbort(qw.QLineEdit):
4291 aborted = qc.pyqtSignal()
4292 history_down = qc.pyqtSignal()
4293 history_up = qc.pyqtSignal()
4295 def keyPressEvent(self, key_event):
4296 if key_event.key() == qc.Qt.Key_Escape:
4297 self.aborted.emit()
4298 elif key_event.key() == qc.Qt.Key_Down:
4299 self.history_down.emit()
4300 elif key_event.key() == qc.Qt.Key_Up:
4301 self.history_up.emit()
4302 else:
4303 return qw.QLineEdit.keyPressEvent(self, key_event)
4306class PileViewer(qw.QFrame):
4307 '''
4308 PileViewerMain + Controls + Inputline
4309 '''
4311 def __init__(
4312 self, pile,
4313 ntracks_shown_max=20,
4314 marker_editor_sortable=True,
4315 use_opengl=None,
4316 panel_parent=None,
4317 *args):
4319 qw.QFrame.__init__(self, *args)
4321 layout = qw.QGridLayout()
4322 layout.setContentsMargins(0, 0, 0, 0)
4323 layout.setSpacing(0)
4325 self.menu = PileViewerMenuBar(self)
4327 if use_opengl is None:
4328 use_opengl = is_macos
4330 if use_opengl:
4331 self.viewer = GLPileViewerMain(
4332 pile,
4333 ntracks_shown_max=ntracks_shown_max,
4334 panel_parent=panel_parent,
4335 menu=self.menu)
4336 else:
4337 self.viewer = PileViewerMain(
4338 pile,
4339 ntracks_shown_max=ntracks_shown_max,
4340 panel_parent=panel_parent,
4341 menu=self.menu)
4343 self.marker_editor_sortable = marker_editor_sortable
4345 # self.setFrameShape(qw.QFrame.StyledPanel)
4346 # self.setFrameShadow(qw.QFrame.Sunken)
4348 self.input_area = qw.QFrame(self)
4349 ia_layout = qw.QGridLayout()
4350 ia_layout.setContentsMargins(11, 11, 11, 11)
4351 self.input_area.setLayout(ia_layout)
4353 self.inputline = LineEditWithAbort(self.input_area)
4354 self.inputline.returnPressed.connect(
4355 self.inputline_returnpressed)
4356 self.inputline.editingFinished.connect(
4357 self.inputline_finished)
4358 self.inputline.aborted.connect(
4359 self.inputline_aborted)
4361 self.inputline.history_down.connect(
4362 lambda: self.step_through_history(1))
4363 self.inputline.history_up.connect(
4364 lambda: self.step_through_history(-1))
4366 self.inputline.textEdited.connect(
4367 self.inputline_changed)
4369 self.inputline.setPlaceholderText(
4370 u'Quick commands: e.g. \'c HH?\' to select channels. '
4371 u'Use ↑ or ↓ to navigate.')
4372 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4373 self.input_area.hide()
4374 self.history = None
4376 self.inputline_error_str = None
4378 self.inputline_error = qw.QLabel()
4379 self.inputline_error.hide()
4381 ia_layout.addWidget(self.inputline, 0, 0)
4382 ia_layout.addWidget(self.inputline_error, 1, 0)
4383 layout.addWidget(self.input_area, 0, 0, 1, 2)
4384 layout.addWidget(self.viewer, 1, 0)
4386 pb = Progressbars(self)
4387 layout.addWidget(pb, 2, 0, 1, 2)
4388 self.progressbars = pb
4390 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4391 self.scrollbar = scrollbar
4392 layout.addWidget(scrollbar, 1, 1)
4393 self.scrollbar.valueChanged.connect(
4394 self.scrollbar_changed)
4396 self.block_scrollbar_changes = False
4398 self.viewer.want_input.connect(
4399 self.inputline_show)
4400 self.viewer.tracks_range_changed.connect(
4401 self.tracks_range_changed)
4402 self.viewer.pile_has_changed_signal.connect(
4403 self.adjust_controls)
4404 self.viewer.about_to_close.connect(
4405 self.save_inputline_history)
4407 self.setLayout(layout)
4409 def cleanup(self):
4410 self.viewer.cleanup()
4412 def get_progressbars(self):
4413 return self.progressbars
4415 def inputline_show(self):
4416 if not self.history:
4417 self.load_inputline_history()
4419 self.input_area.show()
4420 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4421 self.inputline.selectAll()
4423 def inputline_set_error(self, string):
4424 self.inputline_error_str = string
4425 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4426 self.inputline.selectAll()
4427 self.inputline_error.setText(string)
4428 self.input_area.show()
4429 self.inputline_error.show()
4431 def inputline_clear_error(self):
4432 if self.inputline_error_str:
4433 self.inputline.setPalette(qw.QApplication.palette())
4434 self.inputline_error_str = None
4435 self.inputline_error.clear()
4436 self.inputline_error.hide()
4438 def inputline_changed(self, line):
4439 self.viewer.inputline_changed(str(line))
4440 self.inputline_clear_error()
4442 def inputline_returnpressed(self):
4443 line = str(self.inputline.text())
4444 clearit, hideit, error = self.viewer.inputline_finished(line)
4446 if error:
4447 self.inputline_set_error(error)
4449 line = line.strip()
4451 if line != '' and not error:
4452 if not (len(self.history) >= 1 and line == self.history[-1]):
4453 self.history.append(line)
4455 if clearit:
4457 self.inputline.blockSignals(True)
4458 qpat, qinp = self.viewer.get_quick_filter_patterns()
4459 if qpat is None:
4460 self.inputline.clear()
4461 else:
4462 self.inputline.setText(qinp)
4463 self.inputline.blockSignals(False)
4465 if hideit and not error:
4466 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4467 self.input_area.hide()
4469 self.hist_ind = len(self.history)
4471 def inputline_aborted(self):
4472 '''
4473 Hide the input line.
4474 '''
4475 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4476 self.hist_ind = len(self.history)
4477 self.input_area.hide()
4479 def save_inputline_history(self):
4480 '''
4481 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4482 '''
4483 if not self.history:
4484 return
4486 conf = pyrocko.config
4487 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4488 with open(fn_hist, 'w') as f:
4489 i = min(100, len(self.history))
4490 for c in self.history[-i:]:
4491 f.write('%s\n' % c)
4493 def load_inputline_history(self):
4494 '''
4495 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4496 '''
4497 conf = pyrocko.config
4498 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4499 if not os.path.exists(fn_hist):
4500 with open(fn_hist, 'w+') as f:
4501 f.write('\n')
4503 with open(fn_hist, 'r') as f:
4504 self.history = [line.strip() for line in f.readlines()]
4506 self.hist_ind = len(self.history)
4508 def step_through_history(self, ud=1):
4509 '''
4510 Step through input line history and set the input line text.
4511 '''
4512 n = len(self.history)
4513 self.hist_ind += ud
4514 self.hist_ind %= (n + 1)
4515 if len(self.history) != 0 and self.hist_ind != n:
4516 self.inputline.setText(self.history[self.hist_ind])
4517 else:
4518 self.inputline.setText('')
4520 def inputline_finished(self):
4521 pass
4523 def tracks_range_changed(self, ntracks, ilo, ihi):
4524 if self.block_scrollbar_changes:
4525 return
4527 self.scrollbar.blockSignals(True)
4528 self.scrollbar.setPageStep(ihi-ilo)
4529 vmax = max(0, ntracks-(ihi-ilo))
4530 self.scrollbar.setRange(0, vmax)
4531 self.scrollbar.setValue(ilo)
4532 self.scrollbar.setHidden(vmax == 0)
4533 self.scrollbar.blockSignals(False)
4535 def scrollbar_changed(self, value):
4536 self.block_scrollbar_changes = True
4537 ilo = value
4538 ihi = ilo + self.scrollbar.pageStep()
4539 self.viewer.set_tracks_range((ilo, ihi))
4540 self.block_scrollbar_changes = False
4541 self.update_contents()
4543 def controls(self):
4544 frame = qw.QFrame(self)
4545 layout = qw.QGridLayout()
4546 frame.setLayout(layout)
4548 minfreq = 0.001
4549 maxfreq = 1000.0
4550 self.lowpass_control = ValControl(high_is_none=True)
4551 self.lowpass_control.setup(
4552 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4553 self.highpass_control = ValControl(low_is_none=True)
4554 self.highpass_control.setup(
4555 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4556 self.gain_control = ValControl()
4557 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4558 self.rot_control = LinValControl()
4559 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4560 self.colorbar_control = ColorbarControl(self)
4562 self.lowpass_control.valchange.connect(
4563 self.viewer.lowpass_change)
4564 self.highpass_control.valchange.connect(
4565 self.viewer.highpass_change)
4566 self.gain_control.valchange.connect(
4567 self.viewer.gain_change)
4568 self.rot_control.valchange.connect(
4569 self.viewer.rot_change)
4570 self.colorbar_control.cmap_changed.connect(
4571 self.viewer.waterfall_cmap_change
4572 )
4573 self.colorbar_control.clip_changed.connect(
4574 self.viewer.waterfall_clip_change
4575 )
4576 self.colorbar_control.show_absolute_toggled.connect(
4577 self.viewer.waterfall_show_absolute_change
4578 )
4579 self.colorbar_control.show_integrate_toggled.connect(
4580 self.viewer.waterfall_set_integrate
4581 )
4583 for icontrol, control in enumerate((
4584 self.highpass_control,
4585 self.lowpass_control,
4586 self.gain_control,
4587 self.rot_control,
4588 self.colorbar_control)):
4590 for iwidget, widget in enumerate(control.widgets()):
4591 layout.addWidget(widget, icontrol, iwidget)
4593 spacer = qw.QSpacerItem(
4594 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4595 layout.addItem(spacer, 4, 0, 1, 3)
4597 self.adjust_controls()
4598 self.viewer.viewmode_change(ViewMode.Wiggle)
4599 return frame
4601 def marker_editor(self):
4602 editor = pyrocko.gui.marker_editor.MarkerEditor(
4603 self, sortable=self.marker_editor_sortable)
4605 editor.set_viewer(self.get_view())
4606 editor.get_marker_model().dataChanged.connect(
4607 self.update_contents)
4608 return editor
4610 def adjust_controls(self):
4611 dtmin, dtmax = self.viewer.content_deltat_range()
4612 maxfreq = 0.5/dtmin
4613 minfreq = (0.5/dtmax)*0.0001
4614 self.lowpass_control.set_range(minfreq, maxfreq)
4615 self.highpass_control.set_range(minfreq, maxfreq)
4617 def setup_snufflings(self):
4618 self.viewer.setup_snufflings()
4620 def get_view(self):
4621 return self.viewer
4623 def update_contents(self):
4624 self.viewer.update()
4626 def get_pile(self):
4627 return self.viewer.get_pile()
4629 def show_colorbar_ctrl(self, show):
4630 for w in self.colorbar_control.widgets():
4631 w.setVisible(show)
4633 def show_gain_ctrl(self, show):
4634 for w in self.gain_control.widgets():
4635 w.setVisible(show)