1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import, print_function
7import os
8import time
9import calendar
10import datetime
11import re
12import math
13import logging
14import operator
15import copy
16import enum
17from itertools import groupby
19import numpy as num
20import pyrocko.model
21import pyrocko.pile
22import pyrocko.trace
23import pyrocko.response
24import pyrocko.util
25import pyrocko.plot
26import pyrocko.gui.snuffling
27import pyrocko.gui.snufflings
28import pyrocko.gui.marker_editor
30from pyrocko.util import hpfloat, gmtime_x, mystrftime
32from .marker import associate_phases_to_events, MarkerOneNSLCRequired
34from .util import (ValControl, LinValControl, Marker, EventMarker,
35 PhaseMarker, make_QPolygonF, draw_label, Label,
36 Progressbars, ColorbarControl)
38from .qt_compat import qc, qg, qw, qsvg
40from .pile_viewer_waterfall import TraceWaterfall
42import scipy.stats as sstats
43import platform
45MIN_LABEL_SIZE_PT = 6
48qc.QString = str
50qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
51 qw.QFileDialog.DontUseSheet
53is_macos = platform.uname()[0] == 'Darwin'
55logger = logging.getLogger('pyrocko.gui.pile_viewer')
58def detrend(x, y):
59 slope, offset, _, _, _ = sstats.linregress(x, y)
60 y_detrended = y - slope * x - offset
61 return y_detrended, slope, offset
64def retrend(x, y_detrended, slope, offset):
65 return x * slope + y_detrended + offset
68class Global(object):
69 appOnDemand = None
72class NSLC(object):
73 def __init__(self, n, s, l=None, c=None): # noqa
74 self.network = n
75 self.station = s
76 self.location = l
77 self.channel = c
80class m_float(float):
82 def __str__(self):
83 if abs(self) >= 10000.:
84 return '%g km' % round(self/1000., 0)
85 elif abs(self) >= 1000.:
86 return '%g km' % round(self/1000., 1)
87 else:
88 return '%.5g m' % self
90 def __lt__(self, other):
91 if other is None:
92 return True
93 return float(self) < float(other)
95 def __gt__(self, other):
96 if other is None:
97 return False
98 return float(self) > float(other)
101def m_float_or_none(x):
102 if x is None:
103 return None
104 else:
105 return m_float(x)
108def make_chunks(items):
109 '''
110 Split a list of integers into sublists of consecutive elements.
111 '''
112 return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
113 enumerate(items), (lambda x: x[1]-x[0]))]
116class deg_float(float):
118 def __str__(self):
119 return '%4.0f' % self
121 def __lt__(self, other):
122 if other is None:
123 return True
124 return float(self) < float(other)
126 def __gt__(self, other):
127 if other is None:
128 return False
129 return float(self) > float(other)
132def deg_float_or_none(x):
133 if x is None:
134 return None
135 else:
136 return deg_float(x)
139class sector_int(int):
141 def __str__(self):
142 return '[%i]' % self
144 def __lt__(self, other):
145 if other is None:
146 return True
147 return int(self) < int(other)
149 def __gt__(self, other):
150 if other is None:
151 return False
152 return int(self) > int(other)
155def num_to_html(num):
156 snum = '%g' % num
157 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
158 if m:
159 snum = m.group(1) + ' × 10<sup>%i</sup>' % int(m.group(2))
161 return snum
164gap_lap_tolerance = 5.
167class ViewMode(enum.Enum):
168 Wiggle = 1
169 Waterfall = 2
172class Timer(object):
173 def __init__(self):
174 self._start = None
175 self._stop = None
177 def start(self):
178 self._start = os.times()
180 def stop(self):
181 self._stop = os.times()
183 def get(self):
184 a = self._start
185 b = self._stop
186 if a is not None and b is not None:
187 return tuple([b[i] - a[i] for i in range(5)])
188 else:
189 return tuple([0.] * 5)
191 def __sub__(self, other):
192 a = self.get()
193 b = other.get()
194 return tuple([a[i] - b[i] for i in range(5)])
197class ObjectStyle(object):
198 def __init__(self, frame_pen, fill_brush):
199 self.frame_pen = frame_pen
200 self.fill_brush = fill_brush
203box_styles = []
204box_alpha = 100
205for color in 'orange skyblue butter chameleon chocolate plum ' \
206 'scarletred'.split():
208 box_styles.append(ObjectStyle(
209 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
210 qg.QBrush(qg.QColor(
211 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
212 ))
214box_styles_coverage = {}
216box_styles_coverage['waveform'] = [
217 ObjectStyle(
218 qg.QPen(
219 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']),
220 1, qc.Qt.DashLine),
221 qg.QBrush(qg.QColor(
222 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))),
223 ),
224 ObjectStyle(
225 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])),
226 qg.QBrush(qg.QColor(
227 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))),
228 ),
229 ObjectStyle(
230 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])),
231 qg.QBrush(qg.QColor(
232 *(pyrocko.plot.tango_colors['plum1'] + (50,)))),
233 )]
235box_styles_coverage['waveform_promise'] = [
236 ObjectStyle(
237 qg.QPen(
238 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']),
239 1, qc.Qt.DashLine),
240 qg.QBrush(qg.QColor(
241 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
242 ),
243 ObjectStyle(
244 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
245 qg.QBrush(qg.QColor(
246 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))),
247 ),
248 ObjectStyle(
249 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])),
250 qg.QBrush(qg.QColor(
251 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))),
252 )]
254sday = 60*60*24. # \
255smonth = 60*60*24*30. # | only used as approx. intervals...
256syear = 60*60*24*365. # /
258acceptable_tincs = num.array([
259 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
260 60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
263working_system_time_range = \
264 pyrocko.util.working_system_time_range()
266initial_time_range = []
268try:
269 initial_time_range.append(
270 calendar.timegm((1950, 1, 1, 0, 0, 0)))
271except Exception:
272 initial_time_range.append(working_system_time_range[0])
274try:
275 initial_time_range.append(
276 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
277except Exception:
278 initial_time_range.append(working_system_time_range[1])
281def is_working_time(t):
282 return working_system_time_range[0] <= t and \
283 t <= working_system_time_range[1]
286def fancy_time_ax_format(inc):
287 l0_fmt_brief = ''
288 l2_fmt = ''
289 l2_trig = 0
290 if inc < 0.000001:
291 l0_fmt = '.%n'
292 l0_center = False
293 l1_fmt = '%H:%M:%S'
294 l1_trig = 6
295 l2_fmt = '%b %d, %Y'
296 l2_trig = 3
297 elif inc < 0.001:
298 l0_fmt = '.%u'
299 l0_center = False
300 l1_fmt = '%H:%M:%S'
301 l1_trig = 6
302 l2_fmt = '%b %d, %Y'
303 l2_trig = 3
304 elif inc < 1:
305 l0_fmt = '.%r'
306 l0_center = False
307 l1_fmt = '%H:%M:%S'
308 l1_trig = 6
309 l2_fmt = '%b %d, %Y'
310 l2_trig = 3
311 elif inc < 60:
312 l0_fmt = '%H:%M:%S'
313 l0_center = False
314 l1_fmt = '%b %d, %Y'
315 l1_trig = 3
316 elif inc < 3600:
317 l0_fmt = '%H:%M'
318 l0_center = False
319 l1_fmt = '%b %d, %Y'
320 l1_trig = 3
321 elif inc < sday:
322 l0_fmt = '%H:%M'
323 l0_center = False
324 l1_fmt = '%b %d, %Y'
325 l1_trig = 3
326 elif inc < smonth:
327 l0_fmt = '%a %d'
328 l0_fmt_brief = '%d'
329 l0_center = True
330 l1_fmt = '%b, %Y'
331 l1_trig = 2
332 elif inc < syear:
333 l0_fmt = '%b'
334 l0_center = True
335 l1_fmt = '%Y'
336 l1_trig = 1
337 else:
338 l0_fmt = '%Y'
339 l0_center = False
340 l1_fmt = ''
341 l1_trig = 0
343 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
346def day_start(timestamp):
347 tt = time.gmtime(int(timestamp))
348 tts = tt[0:3] + (0, 0, 0) + tt[6:9]
349 return calendar.timegm(tts)
352def month_start(timestamp):
353 tt = time.gmtime(int(timestamp))
354 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
355 return calendar.timegm(tts)
358def year_start(timestamp):
359 tt = time.gmtime(int(timestamp))
360 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
361 return calendar.timegm(tts)
364def time_nice_value(inc0):
365 if inc0 < acceptable_tincs[0]:
366 return pyrocko.plot.nice_value(inc0)
367 elif inc0 > acceptable_tincs[-1]:
368 return pyrocko.plot.nice_value(inc0/syear)*syear
369 else:
370 i = num.argmin(num.abs(acceptable_tincs-inc0))
371 return acceptable_tincs[i]
374class TimeScaler(pyrocko.plot.AutoScaler):
375 def __init__(self):
376 pyrocko.plot.AutoScaler.__init__(self)
377 self.mode = 'min-max'
379 def make_scale(self, data_range):
380 assert self.mode in ('min-max', 'off'), \
381 'mode must be "min-max" or "off" for TimeScaler'
383 data_min = min(data_range)
384 data_max = max(data_range)
385 is_reverse = (data_range[0] > data_range[1])
387 mi, ma = data_min, data_max
388 nmi = mi
389 if self.mode != 'off':
390 nmi = mi - self.space*(ma-mi)
392 nma = ma
393 if self.mode != 'off':
394 nma = ma + self.space*(ma-mi)
396 mi, ma = nmi, nma
398 if mi == ma and self.mode != 'off':
399 mi -= 1.0
400 ma += 1.0
402 mi = max(working_system_time_range[0], mi)
403 ma = min(working_system_time_range[1], ma)
405 # make nice tick increment
406 if self.inc is not None:
407 inc = self.inc
408 else:
409 if self.approx_ticks > 0.:
410 inc = time_nice_value((ma-mi)/self.approx_ticks)
411 else:
412 inc = time_nice_value((ma-mi)*10.)
414 if inc == 0.0:
415 inc = 1.0
417 if is_reverse:
418 return ma, mi, -inc
419 else:
420 return mi, ma, inc
422 def make_ticks(self, data_range):
423 mi, ma, inc = self.make_scale(data_range)
425 is_reverse = False
426 if inc < 0:
427 mi, ma, inc = ma, mi, -inc
428 is_reverse = True
430 ticks = []
432 if inc < sday:
433 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
434 if inc < 0.001:
435 mi_day = hpfloat(mi_day)
437 base = mi_day+num.ceil((mi-mi_day)/inc)*inc
438 if inc < 0.001:
439 base = hpfloat(base)
441 base_day = mi_day
442 i = 0
443 while True:
444 tick = base+i*inc
445 if tick > ma:
446 break
448 tick_day = day_start(tick)
449 if tick_day > base_day:
450 base_day = tick_day
451 base = base_day
452 i = 0
453 else:
454 ticks.append(tick)
455 i += 1
457 elif inc < smonth:
458 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
459 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
460 delta = datetime.timedelta(days=int(round(inc/sday)))
461 if mi_day == mi:
462 dt_base += delta
463 i = 0
464 while True:
465 current = dt_base + i*delta
466 tick = calendar.timegm(current.timetuple())
467 if tick > ma:
468 break
469 ticks.append(tick)
470 i += 1
472 elif inc < syear:
473 mi_month = month_start(max(
474 mi, working_system_time_range[0]+smonth*1.5))
476 y, m = time.gmtime(mi_month)[:2]
477 while True:
478 tick = calendar.timegm((y, m, 1, 0, 0, 0))
479 m += 1
480 if m > 12:
481 y, m = y+1, 1
483 if tick > ma:
484 break
486 if tick >= mi:
487 ticks.append(tick)
489 else:
490 mi_year = year_start(max(
491 mi, working_system_time_range[0]+syear*1.5))
493 incy = int(round(inc/syear))
494 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
496 while True:
497 tick = calendar.timegm((y, 1, 1, 0, 0, 0))
498 y += incy
499 if tick > ma:
500 break
501 if tick >= mi:
502 ticks.append(tick)
504 if is_reverse:
505 ticks.reverse()
507 return ticks, inc
510def need_l1_tick(tt, ms, l1_trig):
511 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
514def tick_to_labels(tick, inc):
515 tt, ms = gmtime_x(tick)
516 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
517 fancy_time_ax_format(inc)
519 l0 = mystrftime(l0_fmt, tt, ms)
520 l0_brief = mystrftime(l0_fmt_brief, tt, ms)
521 l1, l2 = None, None
522 if need_l1_tick(tt, ms, l1_trig):
523 l1 = mystrftime(l1_fmt, tt, ms)
524 if need_l1_tick(tt, ms, l2_trig):
525 l2 = mystrftime(l2_fmt, tt, ms)
527 return l0, l0_brief, l0_center, l1, l2
530def l1_l2_tick(tick, inc):
531 tt, ms = gmtime_x(tick)
532 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
533 fancy_time_ax_format(inc)
535 l1 = mystrftime(l1_fmt, tt, ms)
536 l2 = mystrftime(l2_fmt, tt, ms)
537 return l1, l2
540class TimeAx(TimeScaler):
541 def __init__(self, *args):
542 TimeScaler.__init__(self, *args)
544 def drawit(self, p, xprojection, yprojection):
545 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
546 p.setPen(pen)
547 font = qg.QFont()
548 font.setBold(True)
549 p.setFont(font)
550 fm = p.fontMetrics()
551 ticklen = 10
552 pad = 10
553 tmin, tmax = xprojection.get_in_range()
554 ticks, inc = self.make_ticks((tmin, tmax))
555 l1_hits = 0
556 l2_hits = 0
558 vmin, vmax = yprojection(0), yprojection(ticklen)
559 uumin, uumax = xprojection.get_out_range()
560 first_tick_with_label = None
561 for tick in ticks:
562 umin = xprojection(tick)
564 umin_approx_next = xprojection(tick+inc)
565 umax = xprojection(tick)
567 pinc_approx = umin_approx_next - umin
569 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
570 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
572 if tick == 0.0 and tmax - tmin < 3600*24:
573 # hide year at epoch (we assume that synthetic data is shown)
574 if l2:
575 l2 = None
576 elif l1:
577 l1 = None
579 if l0_center:
580 ushift = (umin_approx_next-umin)/2.
581 else:
582 ushift = 0.
584 for l0x in (l0, l0_brief, ''):
585 label0 = l0x
586 rect0 = fm.boundingRect(label0)
587 if rect0.width() <= pinc_approx*0.9:
588 break
590 if uumin+pad < umin-rect0.width()/2.+ushift and \
591 umin+rect0.width()/2.+ushift < uumax-pad:
593 if first_tick_with_label is None:
594 first_tick_with_label = tick
595 p.drawText(qc.QPointF(
596 umin-rect0.width()/2.+ushift,
597 vmin+rect0.height()+ticklen), label0)
599 if l1:
600 label1 = l1
601 rect1 = fm.boundingRect(label1)
602 if uumin+pad < umin-rect1.width()/2. and \
603 umin+rect1.width()/2. < uumax-pad:
605 p.drawText(qc.QPointF(
606 umin-rect1.width()/2.,
607 vmin+rect0.height()+rect1.height()+ticklen),
608 label1)
610 l1_hits += 1
612 if l2:
613 label2 = l2
614 rect2 = fm.boundingRect(label2)
615 if uumin+pad < umin-rect2.width()/2. and \
616 umin+rect2.width()/2. < uumax-pad:
618 p.drawText(qc.QPointF(
619 umin-rect2.width()/2.,
620 vmin+rect0.height()+rect1.height()+rect2.height() +
621 ticklen), label2)
623 l2_hits += 1
625 if first_tick_with_label is None:
626 first_tick_with_label = tmin
628 l1, l2 = l1_l2_tick(first_tick_with_label, inc)
630 if -3600.*25 < first_tick_with_label <= 3600.*25 and \
631 tmax - tmin < 3600*24:
633 # hide year at epoch (we assume that synthetic data is shown)
634 if l2:
635 l2 = None
636 elif l1:
637 l1 = None
639 if l1_hits == 0 and l1:
640 label1 = l1
641 rect1 = fm.boundingRect(label1)
642 p.drawText(qc.QPointF(
643 uumin+pad,
644 vmin+rect0.height()+rect1.height()+ticklen),
645 label1)
647 l1_hits += 1
649 if l2_hits == 0 and l2:
650 label2 = l2
651 rect2 = fm.boundingRect(label2)
652 p.drawText(qc.QPointF(
653 uumin+pad,
654 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
655 label2)
657 v = yprojection(0)
658 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
661class Projection(object):
662 def __init__(self):
663 self.xr = 0., 1.
664 self.ur = 0., 1.
666 def set_in_range(self, xmin, xmax):
667 if xmax == xmin:
668 xmax = xmin + 1.
670 self.xr = xmin, xmax
672 def get_in_range(self):
673 return self.xr
675 def set_out_range(self, umin, umax):
676 if umax == umin:
677 umax = umin + 1.
679 self.ur = umin, umax
681 def get_out_range(self):
682 return self.ur
684 def __call__(self, x):
685 umin, umax = self.ur
686 xmin, xmax = self.xr
687 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
689 def clipped(self, x):
690 umin, umax = self.ur
691 xmin, xmax = self.xr
692 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
694 def rev(self, u):
695 umin, umax = self.ur
696 xmin, xmax = self.xr
697 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
699 def copy(self):
700 return copy.copy(self)
703def add_radiobuttongroup(menu, menudef, target, default=None):
704 group = qw.QActionGroup(menu)
705 group.setExclusive(True)
706 menuitems = []
708 for name, value, *shortcut in menudef:
709 action = menu.addAction(name)
710 action.setCheckable(True)
711 action.setActionGroup(group)
712 if shortcut:
713 action.setShortcut(shortcut[0])
715 menuitems.append((action, value))
716 if default is not None and (
717 name.lower().replace(' ', '_') == default or
718 value == default):
719 action.setChecked(True)
721 group.triggered.connect(target)
723 if default is None:
724 menuitems[0][0].setChecked(True)
726 return menuitems
729def sort_actions(menu):
730 actions = [act for act in menu.actions() if not act.menu()]
731 for action in actions:
732 menu.removeAction(action)
733 actions.sort(key=lambda x: str(x.text()))
735 help_action = [a for a in actions if a.text() == 'Snuffler Controls']
736 if help_action:
737 actions.insert(0, actions.pop(actions.index(help_action[0])))
738 for action in actions:
739 menu.addAction(action)
742fkey_map = dict(zip(
743 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
744 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10),
745 range(10)))
748class PileViewerMainException(Exception):
749 pass
752class PileViewerMenuBar(qw.QMenuBar):
753 ...
756class PileViewerMenu(qw.QMenu):
757 ...
760def MakePileViewerMainClass(base):
762 class PileViewerMain(base):
764 want_input = qc.pyqtSignal()
765 about_to_close = qc.pyqtSignal()
766 pile_has_changed_signal = qc.pyqtSignal()
767 tracks_range_changed = qc.pyqtSignal(int, int, int)
769 begin_markers_add = qc.pyqtSignal(int, int)
770 end_markers_add = qc.pyqtSignal()
771 begin_markers_remove = qc.pyqtSignal(int, int)
772 end_markers_remove = qc.pyqtSignal()
774 marker_selection_changed = qc.pyqtSignal(list)
775 active_event_marker_changed = qc.pyqtSignal()
777 def __init__(self, pile, ntracks_shown_max, panel_parent, *args,
778 menu=None):
779 base.__init__(self, *args)
781 self.pile = pile
782 self.ax_height = 80
783 self.panel_parent = panel_parent
785 self.click_tolerance = 5
787 self.ntracks_shown_max = ntracks_shown_max
788 self.initial_ntracks_shown_max = ntracks_shown_max
789 self.ntracks = 0
790 self.show_all = True
791 self.shown_tracks_range = None
792 self.track_start = None
793 self.track_trange = None
795 self.lowpass = None
796 self.highpass = None
797 self.gain = 1.0
798 self.rotate = 0.0
799 self.picking_down = None
800 self.picking = None
801 self.floating_marker = None
802 self.markers = pyrocko.pile.Sorted([], 'tmin')
803 self.markers_deltat_max = 0.
804 self.n_selected_markers = 0
805 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7)
806 self.visible_marker_kinds = self.all_marker_kinds
807 self.active_event_marker = None
808 self.ignore_releases = 0
809 self.message = None
810 self.reloaded = False
811 self.pile_has_changed = False
812 self.config = pyrocko.config.config('snuffler')
814 self.tax = TimeAx()
815 self.setBackgroundRole(qg.QPalette.Base)
816 self.setAutoFillBackground(True)
817 poli = qw.QSizePolicy(
818 qw.QSizePolicy.Expanding,
819 qw.QSizePolicy.Expanding)
821 self.setSizePolicy(poli)
822 self.setMinimumSize(300, 200)
823 self.setFocusPolicy(qc.Qt.ClickFocus)
825 self.menu = menu or PileViewerMenu(self)
827 file_menu = self.menu.addMenu('&File')
828 view_menu = self.menu.addMenu('&View')
829 options_menu = self.menu.addMenu('&Options')
830 scale_menu = self.menu.addMenu('&Scaling')
831 sort_menu = self.menu.addMenu('Sor&ting')
832 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings')
834 help_menu = self.menu.addMenu('&Help')
836 self.snufflings_menu = self.toggle_panel_menu.addMenu(
837 'Run Snuffling')
838 self.toggle_panel_menu.addSeparator()
839 self.snuffling_help = help_menu.addMenu('Snuffling Help')
840 help_menu.addSeparator()
842 file_menu.addAction(
843 qg.QIcon.fromTheme('document-open'),
844 'Open waveform files...',
845 self.open_waveforms,
846 qg.QKeySequence.Open)
848 file_menu.addAction(
849 qg.QIcon.fromTheme('document-open'),
850 'Open waveform directory...',
851 self.open_waveform_directory)
853 file_menu.addAction(
854 'Open station files...',
855 self.open_stations)
857 file_menu.addAction(
858 'Open StationXML files...',
859 self.open_stations_xml)
861 file_menu.addAction(
862 'Open event file...',
863 self.read_events)
865 file_menu.addSeparator()
866 file_menu.addAction(
867 'Open marker file...',
868 self.read_markers)
870 file_menu.addAction(
871 qg.QIcon.fromTheme('document-save'),
872 'Save markers...',
873 self.write_markers,
874 qg.QKeySequence.Save)
876 file_menu.addAction(
877 qg.QIcon.fromTheme('document-save-as'),
878 'Save selected markers...',
879 self.write_selected_markers,
880 qg.QKeySequence.SaveAs)
882 file_menu.addSeparator()
883 file_menu.addAction(
884 qg.QIcon.fromTheme('document-print'),
885 'Print',
886 self.printit,
887 qg.QKeySequence.Print)
889 file_menu.addAction(
890 qg.QIcon.fromTheme('insert-image'),
891 'Save as SVG or PNG',
892 self.savesvg,
893 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E))
895 file_menu.addSeparator()
896 close = file_menu.addAction(
897 qg.QIcon.fromTheme('window-close'),
898 'Close',
899 self.myclose)
900 close.setShortcuts(
901 (qg.QKeySequence(qc.Qt.Key_Q),
902 qg.QKeySequence(qc.Qt.Key_X)))
904 # Scale Menu
905 menudef = [
906 ('Individual Scale',
907 lambda tr: tr.nslc_id,
908 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)),
909 ('Common Scale',
910 lambda tr: None,
911 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)),
912 ('Common Scale per Station',
913 lambda tr: (tr.network, tr.station),
914 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)),
915 ('Common Scale per Station Location',
916 lambda tr: (tr.network, tr.station, tr.location)),
917 ('Common Scale per Component',
918 lambda tr: (tr.channel)),
919 ]
921 self.menuitems_scaling = add_radiobuttongroup(
922 scale_menu, menudef, self.scalingmode_change,
923 default=self.config.trace_scale)
924 scale_menu.addSeparator()
926 self.scaling_key = self.menuitems_scaling[0][1]
927 self.scaling_hooks = {}
928 self.scalingmode_change()
930 menudef = [
931 ('Scaling based on Minimum and Maximum',
932 ('minmax', 'minmax')),
933 ('Scaling based on Minimum and Maximum (Robust)',
934 ('minmax', 'robust')),
935 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')),
936 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')),
937 ]
939 self.menuitems_scaling_base = add_radiobuttongroup(
940 scale_menu, menudef, self.scaling_base_change)
942 self.scaling_base = self.menuitems_scaling_base[0][1]
943 scale_menu.addSeparator()
945 self.menuitem_fixscalerange = scale_menu.addAction(
946 'Fix Scale Ranges')
947 self.menuitem_fixscalerange.setCheckable(True)
949 # Sort Menu
950 def sector_dist(sta):
951 if sta.dist_m is None:
952 return None, None
953 else:
954 return (
955 sector_int(round((sta.azimuth+15.)/30.)),
956 m_float(sta.dist_m))
958 menudef = [
959 ('Sort by Names',
960 lambda tr: (),
961 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
962 ('Sort by Distance',
963 lambda tr: self.station_attrib(
964 tr,
965 lambda sta: (m_float_or_none(sta.dist_m),),
966 lambda tr: (None,)),
967 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
968 ('Sort by Azimuth',
969 lambda tr: self.station_attrib(
970 tr,
971 lambda sta: (deg_float_or_none(sta.azimuth),),
972 lambda tr: (None,))),
973 ('Sort by Distance in 12 Azimuthal Blocks',
974 lambda tr: self.station_attrib(
975 tr,
976 sector_dist,
977 lambda tr: (None, None))),
978 ('Sort by Backazimuth',
979 lambda tr: self.station_attrib(
980 tr,
981 lambda sta: (deg_float_or_none(sta.backazimuth),),
982 lambda tr: (None,))),
983 ]
984 self.menuitems_ssorting = add_radiobuttongroup(
985 sort_menu, menudef, self.s_sortingmode_change)
986 sort_menu.addSeparator()
988 self._ssort = lambda tr: ()
990 self.menu.addSeparator()
992 menudef = [
993 ('Subsort by Network, Station, Location, Channel',
994 ((0, 1, 2, 3), # gathering
995 lambda tr: tr.location)), # coloring
996 ('Subsort by Network, Station, Channel, Location',
997 ((0, 1, 3, 2),
998 lambda tr: tr.channel)),
999 ('Subsort by Station, Network, Channel, Location',
1000 ((1, 0, 3, 2),
1001 lambda tr: tr.channel)),
1002 ('Subsort by Location, Network, Station, Channel',
1003 ((2, 0, 1, 3),
1004 lambda tr: tr.channel)),
1005 ('Subsort by Channel, Network, Station, Location',
1006 ((3, 0, 1, 2),
1007 lambda tr: (tr.network, tr.station, tr.location))),
1008 ('Subsort by Network, Station, Channel (Grouped by Location)',
1009 ((0, 1, 3),
1010 lambda tr: tr.location)),
1011 ('Subsort by Station, Network, Channel (Grouped by Location)',
1012 ((1, 0, 3),
1013 lambda tr: tr.location)),
1014 ]
1016 self.menuitems_sorting = add_radiobuttongroup(
1017 sort_menu, menudef, self.sortingmode_change)
1019 menudef = [(x.key, x.value) for x in
1020 self.config.visible_length_setting]
1022 # View menu
1023 self.menuitems_visible_length = add_radiobuttongroup(
1024 view_menu, menudef,
1025 self.visible_length_change)
1026 view_menu.addSeparator()
1028 view_modes = [
1029 ('Wiggle Plot', ViewMode.Wiggle),
1030 ('Waterfall', ViewMode.Waterfall)
1031 ]
1033 self.menuitems_viewmode = add_radiobuttongroup(
1034 view_menu, view_modes,
1035 self.viewmode_change, default=ViewMode.Wiggle)
1036 view_menu.addSeparator()
1038 self.menuitem_cliptraces = view_menu.addAction(
1039 'Clip Traces')
1040 self.menuitem_cliptraces.setCheckable(True)
1041 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1043 self.menuitem_showboxes = view_menu.addAction(
1044 'Show Boxes')
1045 self.menuitem_showboxes.setCheckable(True)
1046 self.menuitem_showboxes.setChecked(
1047 self.config.show_boxes)
1049 self.menuitem_colortraces = view_menu.addAction(
1050 'Color Traces')
1051 self.menuitem_colortraces.setCheckable(True)
1052 self.menuitem_antialias = view_menu.addAction(
1053 'Antialiasing')
1054 self.menuitem_antialias.setCheckable(True)
1056 view_menu.addSeparator()
1057 self.menuitem_showscalerange = view_menu.addAction(
1058 'Show Scale Ranges')
1059 self.menuitem_showscalerange.setCheckable(True)
1060 self.menuitem_showscalerange.setChecked(
1061 self.config.show_scale_ranges)
1063 self.menuitem_showscaleaxis = view_menu.addAction(
1064 'Show Scale Axes')
1065 self.menuitem_showscaleaxis.setCheckable(True)
1066 self.menuitem_showscaleaxis.setChecked(
1067 self.config.show_scale_axes)
1069 self.menuitem_showzeroline = view_menu.addAction(
1070 'Show Zero Lines')
1071 self.menuitem_showzeroline.setCheckable(True)
1073 view_menu.addSeparator()
1074 view_menu.addAction(
1075 qg.QIcon.fromTheme('view-fullscreen'),
1076 'Fullscreen',
1077 self.toggle_fullscreen,
1078 qg.QKeySequence(qc.Qt.Key_F11))
1080 # Options Menu
1081 self.menuitem_demean = options_menu.addAction('Demean')
1082 self.menuitem_demean.setCheckable(True)
1083 self.menuitem_demean.setChecked(self.config.demean)
1084 self.menuitem_demean.setShortcut(
1085 qg.QKeySequence(qc.Qt.Key_Underscore))
1087 self.menuitem_distances_3d = options_menu.addAction(
1088 '3D distances',
1089 self.distances_3d_changed)
1090 self.menuitem_distances_3d.setCheckable(True)
1092 self.menuitem_allowdownsampling = options_menu.addAction(
1093 'Allow Downsampling')
1094 self.menuitem_allowdownsampling.setCheckable(True)
1095 self.menuitem_allowdownsampling.setChecked(True)
1097 self.menuitem_degap = options_menu.addAction(
1098 'Allow Degapping')
1099 self.menuitem_degap.setCheckable(True)
1100 self.menuitem_degap.setChecked(True)
1102 options_menu.addSeparator()
1104 self.menuitem_fft_filtering = options_menu.addAction(
1105 'FFT Filtering')
1106 self.menuitem_fft_filtering.setCheckable(True)
1108 self.menuitem_lphp = options_menu.addAction(
1109 'Bandpass is Low- + Highpass')
1110 self.menuitem_lphp.setCheckable(True)
1111 self.menuitem_lphp.setChecked(True)
1113 options_menu.addSeparator()
1114 self.menuitem_watch = options_menu.addAction(
1115 'Watch Files')
1116 self.menuitem_watch.setCheckable(True)
1118 self.menuitem_liberal_fetch = options_menu.addAction(
1119 'Liberal Fetch Optimization')
1120 self.menuitem_liberal_fetch.setCheckable(True)
1122 self.visible_length = menudef[0][1]
1124 self.snufflings_menu.addAction(
1125 'Reload Snufflings',
1126 self.setup_snufflings)
1128 # Disable ShadowPileTest
1129 if False:
1130 test_action = self.menu.addAction(
1131 'Test',
1132 self.toggletest)
1133 test_action.setCheckable(True)
1135 help_menu.addAction(
1136 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1137 'Snuffler Controls',
1138 self.help,
1139 qg.QKeySequence(qc.Qt.Key_Question))
1141 help_menu.addAction(
1142 'About',
1143 self.about)
1145 self.time_projection = Projection()
1146 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1147 self.time_projection.set_out_range(0., self.width())
1149 self.gather = None
1151 self.trace_filter = None
1152 self.quick_filter = None
1153 self.quick_filter_patterns = None, None
1154 self.blacklist = []
1156 self.track_to_screen = Projection()
1157 self.track_to_nslc_ids = {}
1159 self.cached_vec = None
1160 self.cached_processed_traces = None
1162 self.timer = qc.QTimer(self)
1163 self.timer.timeout.connect(self.periodical)
1164 self.timer.setInterval(1000)
1165 self.timer.start()
1166 self.pile.add_listener(self)
1167 self.trace_styles = {}
1168 if self.get_squirrel() is None:
1169 self.determine_box_styles()
1171 self.setMouseTracking(True)
1173 user_home_dir = os.path.expanduser('~')
1174 self.snuffling_modules = {}
1175 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1176 self.default_snufflings = None
1177 self.snufflings = []
1179 self.stations = {}
1181 self.timer_draw = Timer()
1182 self.timer_cutout = Timer()
1183 self.time_spent_painting = 0.0
1184 self.time_last_painted = time.time()
1186 self.interactive_range_change_time = 0.0
1187 self.interactive_range_change_delay_time = 10.0
1188 self.follow_timer = None
1190 self.sortingmode_change_time = 0.0
1191 self.sortingmode_change_delay_time = None
1193 self.old_data_ranges = {}
1195 self.error_messages = {}
1196 self.return_tag = None
1197 self.wheel_pos = 60
1199 self.setAcceptDrops(True)
1200 self._paths_to_load = []
1202 self.tf_cache = {}
1204 self.waterfall = TraceWaterfall()
1205 self.waterfall_cmap = 'viridis'
1206 self.waterfall_clip_min = 0.
1207 self.waterfall_clip_max = 1.
1208 self.waterfall_show_absolute = False
1209 self.waterfall_integrate = False
1210 self.view_mode = ViewMode.Wiggle
1212 self.automatic_updates = True
1214 self.closing = False
1215 self.in_paint_event = False
1217 def fail(self, reason):
1218 box = qw.QMessageBox(self)
1219 box.setText(reason)
1220 box.exec_()
1222 def set_trace_filter(self, filter_func):
1223 self.trace_filter = filter_func
1224 self.sortingmode_change()
1226 def update_trace_filter(self):
1227 if self.blacklist:
1229 def blacklist_func(tr):
1230 return not pyrocko.util.match_nslc(
1231 self.blacklist, tr.nslc_id)
1233 else:
1234 blacklist_func = None
1236 if self.quick_filter is None and blacklist_func is None:
1237 self.set_trace_filter(None)
1238 elif self.quick_filter is None:
1239 self.set_trace_filter(blacklist_func)
1240 elif blacklist_func is None:
1241 self.set_trace_filter(self.quick_filter)
1242 else:
1243 self.set_trace_filter(
1244 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1246 def set_quick_filter(self, filter_func):
1247 self.quick_filter = filter_func
1248 self.update_trace_filter()
1250 def set_quick_filter_patterns(self, patterns, inputline=None):
1251 if patterns is not None:
1252 self.set_quick_filter(
1253 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1254 else:
1255 self.set_quick_filter(None)
1257 self.quick_filter_patterns = patterns, inputline
1259 def get_quick_filter_patterns(self):
1260 return self.quick_filter_patterns
1262 def add_blacklist_pattern(self, pattern):
1263 if pattern == 'empty':
1264 keys = set(self.pile.nslc_ids)
1265 trs = self.pile.all(
1266 tmin=self.tmin,
1267 tmax=self.tmax,
1268 load_data=False,
1269 degap=False)
1271 for tr in trs:
1272 if tr.nslc_id in keys:
1273 keys.remove(tr.nslc_id)
1275 for key in keys:
1276 xpattern = '.'.join(key)
1277 if xpattern not in self.blacklist:
1278 self.blacklist.append(xpattern)
1280 else:
1281 if pattern in self.blacklist:
1282 self.blacklist.remove(pattern)
1284 self.blacklist.append(pattern)
1286 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1287 self.update_trace_filter()
1289 def remove_blacklist_pattern(self, pattern):
1290 if pattern in self.blacklist:
1291 self.blacklist.remove(pattern)
1292 else:
1293 raise PileViewerMainException(
1294 'Pattern not found in blacklist.')
1296 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1297 self.update_trace_filter()
1299 def clear_blacklist(self):
1300 self.blacklist = []
1301 self.update_trace_filter()
1303 def ssort(self, tr):
1304 return self._ssort(tr)
1306 def station_key(self, x):
1307 return x.network, x.station
1309 def station_keys(self, x):
1310 return [
1311 (x.network, x.station, x.location),
1312 (x.network, x.station)]
1314 def station_attrib(self, tr, getter, default_getter):
1315 for sk in self.station_keys(tr):
1316 if sk in self.stations:
1317 station = self.stations[sk]
1318 return getter(station)
1320 return default_getter(tr)
1322 def get_station(self, sk):
1323 return self.stations[sk]
1325 def has_station(self, station):
1326 for sk in self.station_keys(station):
1327 if sk in self.stations:
1328 return True
1330 return False
1332 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1333 return self.station_attrib(
1334 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1336 def set_stations(self, stations):
1337 self.stations = {}
1338 self.add_stations(stations)
1340 def add_stations(self, stations):
1341 for station in stations:
1342 for sk in self.station_keys(station):
1343 self.stations[sk] = station
1345 ev = self.get_active_event()
1346 if ev:
1347 self.set_origin(ev)
1349 def add_event(self, event):
1350 marker = EventMarker(event)
1351 self.add_marker(marker)
1353 def add_events(self, events):
1354 markers = [EventMarker(e) for e in events]
1355 self.add_markers(markers)
1357 def set_event_marker_as_origin(self, ignore=None):
1358 selected = self.selected_markers()
1359 if not selected:
1360 self.fail('An event marker must be selected.')
1361 return
1363 m = selected[0]
1364 if not isinstance(m, EventMarker):
1365 self.fail('Selected marker is not an event.')
1366 return
1368 self.set_active_event_marker(m)
1370 def deactivate_event_marker(self):
1371 if self.active_event_marker:
1372 self.active_event_marker.active = False
1374 self.active_event_marker_changed.emit()
1375 self.active_event_marker = None
1377 def set_active_event_marker(self, event_marker):
1378 if self.active_event_marker:
1379 self.active_event_marker.active = False
1381 self.active_event_marker = event_marker
1382 event_marker.active = True
1383 event = event_marker.get_event()
1384 self.set_origin(event)
1385 self.active_event_marker_changed.emit()
1387 def set_active_event(self, event):
1388 for marker in self.markers:
1389 if isinstance(marker, EventMarker):
1390 if marker.get_event() is event:
1391 self.set_active_event_marker(marker)
1393 def get_active_event_marker(self):
1394 return self.active_event_marker
1396 def get_active_event(self):
1397 m = self.get_active_event_marker()
1398 if m is not None:
1399 return m.get_event()
1400 else:
1401 return None
1403 def get_active_markers(self):
1404 emarker = self.get_active_event_marker()
1405 if emarker is None:
1406 return None, []
1408 else:
1409 ev = emarker.get_event()
1410 pmarkers = [
1411 m for m in self.markers
1412 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1414 return emarker, pmarkers
1416 def set_origin(self, location):
1417 for station in self.stations.values():
1418 station.set_event_relative_data(
1419 location,
1420 distance_3d=self.menuitem_distances_3d.isChecked())
1422 self.sortingmode_change()
1424 def distances_3d_changed(self):
1425 ignore = self.menuitem_distances_3d.isChecked()
1426 self.set_event_marker_as_origin(ignore)
1428 def iter_snuffling_modules(self):
1429 pjoin = os.path.join
1430 for path in self.snuffling_paths:
1432 if not os.path.isdir(path):
1433 os.mkdir(path)
1435 for entry in os.listdir(path):
1436 directory = path
1437 fn = entry
1438 d = pjoin(path, entry)
1439 if os.path.isdir(d):
1440 directory = d
1441 if os.path.isfile(
1442 os.path.join(directory, 'snuffling.py')):
1443 fn = 'snuffling.py'
1445 if not fn.endswith('.py'):
1446 continue
1448 name = fn[:-3]
1450 if (directory, name) not in self.snuffling_modules:
1451 self.snuffling_modules[directory, name] = \
1452 pyrocko.gui.snuffling.SnufflingModule(
1453 directory, name, self)
1455 yield self.snuffling_modules[directory, name]
1457 def setup_snufflings(self):
1458 # user snufflings
1459 for mod in self.iter_snuffling_modules():
1460 try:
1461 mod.load_if_needed()
1462 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1463 logger.warning('Snuffling module "%s" is broken' % e)
1465 # load the default snufflings on first run
1466 if self.default_snufflings is None:
1467 self.default_snufflings = pyrocko.gui\
1468 .snufflings.__snufflings__()
1469 for snuffling in self.default_snufflings:
1470 self.add_snuffling(snuffling)
1472 def set_panel_parent(self, panel_parent):
1473 self.panel_parent = panel_parent
1475 def get_panel_parent(self):
1476 return self.panel_parent
1478 def add_snuffling(self, snuffling, reloaded=False):
1479 logger.debug('Adding snuffling %s' % snuffling.get_name())
1480 snuffling.init_gui(
1481 self, self.get_panel_parent(), self, reloaded=reloaded)
1482 self.snufflings.append(snuffling)
1483 self.update()
1485 def remove_snuffling(self, snuffling):
1486 snuffling.delete_gui()
1487 self.update()
1488 self.snufflings.remove(snuffling)
1489 snuffling.pre_destroy()
1491 def add_snuffling_menuitem(self, item):
1492 self.snufflings_menu.addAction(item)
1493 item.setParent(self.snufflings_menu)
1494 sort_actions(self.snufflings_menu)
1496 def remove_snuffling_menuitem(self, item):
1497 self.snufflings_menu.removeAction(item)
1499 def add_snuffling_help_menuitem(self, item):
1500 self.snuffling_help.addAction(item)
1501 item.setParent(self.snuffling_help)
1502 sort_actions(self.snuffling_help)
1504 def remove_snuffling_help_menuitem(self, item):
1505 self.snuffling_help.removeAction(item)
1507 def add_panel_toggler(self, item):
1508 self.toggle_panel_menu.addAction(item)
1509 item.setParent(self.toggle_panel_menu)
1510 sort_actions(self.toggle_panel_menu)
1512 def remove_panel_toggler(self, item):
1513 self.toggle_panel_menu.removeAction(item)
1515 def load(self, paths, regex=None, format='detect',
1516 cache_dir=None, force_cache=False):
1518 if cache_dir is None:
1519 cache_dir = pyrocko.config.config().cache_dir
1520 if isinstance(paths, str):
1521 paths = [paths]
1523 fns = pyrocko.util.select_files(
1524 paths, selector=None, include=regex, show_progress=False)
1526 if not fns:
1527 return
1529 cache = pyrocko.pile.get_cache(cache_dir)
1531 t = [time.time()]
1533 def update_bar(label, value):
1534 pbs = self.parent().get_progressbars()
1535 if label.lower() == 'looking at files':
1536 label = 'Looking at %i files' % len(fns)
1537 else:
1538 label = 'Scanning %i files' % len(fns)
1540 return pbs.set_status(label, value)
1542 def update_progress(label, i, n):
1543 abort = False
1545 qw.qApp.processEvents()
1546 if n != 0:
1547 perc = i*100/n
1548 else:
1549 perc = 100
1550 abort |= update_bar(label, perc)
1551 abort |= self.window().is_closing()
1553 tnow = time.time()
1554 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1555 self.update()
1556 t[0] = tnow
1558 return abort
1560 self.automatic_updates = False
1562 self.pile.load_files(
1563 sorted(fns),
1564 filename_attributes=regex,
1565 cache=cache,
1566 fileformat=format,
1567 show_progress=False,
1568 update_progress=update_progress)
1570 self.automatic_updates = True
1571 self.update()
1573 def load_queued(self):
1574 if not self._paths_to_load:
1575 return
1576 paths = self._paths_to_load
1577 self._paths_to_load = []
1578 self.load(paths)
1580 def load_soon(self, paths):
1581 self._paths_to_load.extend(paths)
1582 qc.QTimer.singleShot(200, self.load_queued)
1584 def open_waveforms(self):
1585 caption = 'Select one or more files to open'
1587 fns, _ = qw.QFileDialog.getOpenFileNames(
1588 self, caption, options=qfiledialog_options)
1590 if fns:
1591 self.load(list(str(fn) for fn in fns))
1593 def open_waveform_directory(self):
1594 caption = 'Select directory to scan for waveform files'
1596 dn = qw.QFileDialog.getExistingDirectory(
1597 self, caption, options=qfiledialog_options)
1599 if dn:
1600 self.load([str(dn)])
1602 def open_stations(self, fns=None):
1603 caption = 'Select one or more Pyrocko station files to open'
1605 if not fns:
1606 fns, _ = qw.QFileDialog.getOpenFileNames(
1607 self, caption, options=qfiledialog_options)
1609 try:
1610 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1611 for stat in stations:
1612 self.add_stations(stat)
1614 except Exception as e:
1615 self.fail('Failed to read station file: %s' % str(e))
1617 def open_stations_xml(self, fns=None):
1618 from pyrocko.io import stationxml
1620 caption = 'Select one or more StationXML files'
1621 if not fns:
1622 fns, _ = qw.QFileDialog.getOpenFileNames(
1623 self, caption, options=qfiledialog_options,
1624 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1625 ';;All files (*)')
1627 try:
1628 stations = [
1629 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1630 for x in fns]
1632 for stat in stations:
1633 self.add_stations(stat)
1635 except Exception as e:
1636 self.fail('Failed to read StationXML file: %s' % str(e))
1638 def add_traces(self, traces):
1639 if traces:
1640 mtf = pyrocko.pile.MemTracesFile(None, traces)
1641 self.pile.add_file(mtf)
1642 ticket = (self.pile, mtf)
1643 return ticket
1644 else:
1645 return (None, None)
1647 def release_data(self, tickets):
1648 for ticket in tickets:
1649 pile, mtf = ticket
1650 if pile is not None:
1651 pile.remove_file(mtf)
1653 def periodical(self):
1654 if self.menuitem_watch.isChecked():
1655 if self.pile.reload_modified():
1656 self.update()
1658 def get_pile(self):
1659 return self.pile
1661 def pile_changed(self, what):
1662 self.pile_has_changed = True
1663 self.pile_has_changed_signal.emit()
1664 if self.automatic_updates:
1665 self.update()
1667 def set_gathering(self, gather=None, color=None):
1669 if gather is None:
1670 def gather_func(tr):
1671 return tr.nslc_id
1673 gather = (0, 1, 2, 3)
1675 else:
1676 def gather_func(tr):
1677 return (
1678 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1680 if color is None:
1681 def color(tr):
1682 return tr.location
1684 self.gather = gather_func
1685 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1687 self.color_gather = color
1688 self.color_keys = self.pile.gather_keys(color)
1689 previous_ntracks = self.ntracks
1690 self.set_ntracks(len(keys))
1692 if self.shown_tracks_range is None or \
1693 previous_ntracks == 0 or \
1694 self.show_all:
1696 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1697 key_at_top = None
1698 n = high-low
1700 else:
1701 low, high = self.shown_tracks_range
1702 key_at_top = self.track_keys[low]
1703 n = high-low
1705 self.track_keys = sorted(keys)
1707 track_patterns = []
1708 for k in self.track_keys:
1709 pat = ['*', '*', '*', '*']
1710 for i, j in enumerate(gather):
1711 pat[j] = k[-len(gather)+i]
1713 track_patterns.append(pat)
1715 self.track_patterns = track_patterns
1717 if key_at_top is not None:
1718 try:
1719 ind = self.track_keys.index(key_at_top)
1720 low = ind
1721 high = low+n
1722 except Exception:
1723 pass
1725 self.set_tracks_range((low, high))
1727 self.key_to_row = dict(
1728 [(key, i) for (i, key) in enumerate(self.track_keys)])
1730 def inrange(x, r):
1731 return r[0] <= x and x < r[1]
1733 def trace_selector(trace):
1734 gt = self.gather(trace)
1735 return (
1736 gt in self.key_to_row and
1737 inrange(self.key_to_row[gt], self.shown_tracks_range))
1739 self.trace_selector = lambda x: \
1740 (self.trace_filter is None or self.trace_filter(x)) \
1741 and trace_selector(x)
1743 if self.tmin == working_system_time_range[0] and \
1744 self.tmax == working_system_time_range[1] or \
1745 self.show_all:
1747 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1748 if tmin is not None and tmax is not None:
1749 tlen = (tmax - tmin)
1750 tpad = tlen * 5./self.width()
1751 self.set_time_range(tmin-tpad, tmax+tpad)
1753 def set_time_range(self, tmin, tmax):
1754 if tmin is None:
1755 tmin = initial_time_range[0]
1757 if tmax is None:
1758 tmax = initial_time_range[1]
1760 if tmin > tmax:
1761 tmin, tmax = tmax, tmin
1763 if tmin == tmax:
1764 tmin -= 1.
1765 tmax += 1.
1767 tmin = max(working_system_time_range[0], tmin)
1768 tmax = min(working_system_time_range[1], tmax)
1770 min_deltat = self.content_deltat_range()[0]
1771 if (tmax - tmin < min_deltat):
1772 m = (tmin + tmax) / 2.
1773 tmin = m - min_deltat/2.
1774 tmax = m + min_deltat/2.
1776 self.time_projection.set_in_range(tmin, tmax)
1777 self.tmin, self.tmax = tmin, tmax
1779 def get_time_range(self):
1780 return self.tmin, self.tmax
1782 def ypart(self, y):
1783 if y < self.ax_height:
1784 return -1
1785 elif y > self.height()-self.ax_height:
1786 return 1
1787 else:
1788 return 0
1790 def time_fractional_digits(self):
1791 min_deltat = self.content_deltat_range()[0]
1792 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1794 def write_markers(self, fn=None):
1795 caption = "Choose a file name to write markers"
1796 if not fn:
1797 fn, _ = qw.QFileDialog.getSaveFileName(
1798 self, caption, options=qfiledialog_options)
1799 if fn:
1800 try:
1801 Marker.save_markers(
1802 self.markers, fn,
1803 fdigits=self.time_fractional_digits())
1805 except Exception as e:
1806 self.fail('Failed to write marker file: %s' % str(e))
1808 def write_selected_markers(self, fn=None):
1809 caption = "Choose a file name to write selected markers"
1810 if not fn:
1811 fn, _ = qw.QFileDialog.getSaveFileName(
1812 self, caption, options=qfiledialog_options)
1813 if fn:
1814 try:
1815 Marker.save_markers(
1816 self.iter_selected_markers(),
1817 fn,
1818 fdigits=self.time_fractional_digits())
1820 except Exception as e:
1821 self.fail('Failed to write marker file: %s' % str(e))
1823 def read_events(self, fn=None):
1824 '''
1825 Open QFileDialog to open, read and add
1826 :py:class:`pyrocko.model.Event` instances and their marker
1827 representation to the pile viewer.
1828 '''
1829 caption = "Selet one or more files to open"
1830 if not fn:
1831 fn, _ = qw.QFileDialog.getOpenFileName(
1832 self, caption, options=qfiledialog_options)
1833 if fn:
1834 try:
1835 self.add_events(pyrocko.model.load_events(fn))
1836 self.associate_phases_to_events()
1838 except Exception as e:
1839 self.fail('Failed to read event file: %s' % str(e))
1841 def read_markers(self, fn=None):
1842 '''
1843 Open QFileDialog to open, read and add markers to the pile viewer.
1844 '''
1845 caption = "Selet one or more marker files to open"
1846 if not fn:
1847 fn, _ = qw.QFileDialog.getOpenFileName(
1848 self, caption, options=qfiledialog_options)
1849 if fn:
1850 try:
1851 self.add_markers(Marker.load_markers(fn))
1852 self.associate_phases_to_events()
1854 except Exception as e:
1855 self.fail('Failed to read marker file: %s' % str(e))
1857 def associate_phases_to_events(self):
1858 associate_phases_to_events(self.markers)
1860 def add_marker(self, marker):
1861 # need index to inform QAbstactTableModel about upcoming change,
1862 # but have to restore current state in order to not cause problems
1863 self.markers.insert(marker)
1864 i = self.markers.remove(marker)
1866 self.begin_markers_add.emit(i, i)
1867 self.markers.insert(marker)
1868 self.end_markers_add.emit()
1869 self.markers_deltat_max = max(
1870 self.markers_deltat_max, marker.tmax - marker.tmin)
1872 def add_markers(self, markers):
1873 if not self.markers:
1874 self.begin_markers_add.emit(0, len(markers) - 1)
1875 self.markers.insert_many(markers)
1876 self.end_markers_add.emit()
1877 self.update_markers_deltat_max()
1878 else:
1879 for marker in markers:
1880 self.add_marker(marker)
1882 def update_markers_deltat_max(self):
1883 if self.markers:
1884 self.markers_deltat_max = max(
1885 marker.tmax - marker.tmin for marker in self.markers)
1887 def remove_marker(self, marker):
1888 '''
1889 Remove a ``marker`` from the :py:class:`PileViewer`.
1891 :param marker: :py:class:`Marker` (or subclass) instance
1892 '''
1894 if marker is self.active_event_marker:
1895 self.deactivate_event_marker()
1897 try:
1898 i = self.markers.index(marker)
1899 self.begin_markers_remove.emit(i, i)
1900 self.markers.remove_at(i)
1901 self.end_markers_remove.emit()
1902 except ValueError:
1903 pass
1905 def remove_markers(self, markers):
1906 '''
1907 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1909 :param markers: list of :py:class:`Marker` (or subclass)
1910 instances
1911 '''
1913 if markers is self.markers:
1914 markers = list(markers)
1916 for marker in markers:
1917 self.remove_marker(marker)
1919 self.update_markers_deltat_max()
1921 def remove_selected_markers(self):
1922 def delete_segment(istart, iend):
1923 self.begin_markers_remove.emit(istart, iend-1)
1924 for _ in range(iend - istart):
1925 self.markers.remove_at(istart)
1927 self.end_markers_remove.emit()
1929 istart = None
1930 ipos = 0
1931 markers = self.markers
1932 nmarkers = len(self.markers)
1933 while ipos < nmarkers:
1934 marker = markers[ipos]
1935 if marker.is_selected():
1936 if marker is self.active_event_marker:
1937 self.deactivate_event_marker()
1939 if istart is None:
1940 istart = ipos
1941 else:
1942 if istart is not None:
1943 delete_segment(istart, ipos)
1944 nmarkers -= ipos - istart
1945 ipos = istart - 1
1946 istart = None
1948 ipos += 1
1950 if istart is not None:
1951 delete_segment(istart, ipos)
1953 self.update_markers_deltat_max()
1955 def selected_markers(self):
1956 return [marker for marker in self.markers if marker.is_selected()]
1958 def iter_selected_markers(self):
1959 for marker in self.markers:
1960 if marker.is_selected():
1961 yield marker
1963 def get_markers(self):
1964 return self.markers
1966 def mousePressEvent(self, mouse_ev):
1967 self.show_all = False
1968 point = self.mapFromGlobal(mouse_ev.globalPos())
1970 if mouse_ev.button() == qc.Qt.LeftButton:
1971 marker = self.marker_under_cursor(point.x(), point.y())
1972 if self.picking:
1973 if self.picking_down is None:
1974 self.picking_down = (
1975 self.time_projection.rev(mouse_ev.x()),
1976 mouse_ev.y())
1978 elif marker is not None:
1979 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1980 self.deselect_all()
1981 marker.selected = True
1982 self.emit_selected_markers()
1983 self.update()
1984 else:
1985 self.track_start = mouse_ev.x(), mouse_ev.y()
1986 self.track_trange = self.tmin, self.tmax
1988 if mouse_ev.button() == qc.Qt.RightButton \
1989 and isinstance(self.menu, qw.QMenu):
1990 self.menu.exec_(qg.QCursor.pos())
1991 self.update_status()
1993 def mouseReleaseEvent(self, mouse_ev):
1994 if self.ignore_releases:
1995 self.ignore_releases -= 1
1996 return
1998 if self.picking:
1999 self.stop_picking(mouse_ev.x(), mouse_ev.y())
2000 self.emit_selected_markers()
2002 if self.track_start:
2003 self.update()
2005 self.track_start = None
2006 self.track_trange = None
2007 self.update_status()
2009 def mouseDoubleClickEvent(self, mouse_ev):
2010 self.show_all = False
2011 self.start_picking(None)
2012 self.ignore_releases = 1
2014 def mouseMoveEvent(self, mouse_ev):
2015 point = self.mapFromGlobal(mouse_ev.globalPos())
2017 if self.picking:
2018 self.update_picking(point.x(), point.y())
2020 elif self.track_start is not None:
2021 x0, y0 = self.track_start
2022 dx = (point.x() - x0)/float(self.width())
2023 dy = (point.y() - y0)/float(self.height())
2024 if self.ypart(y0) == 1:
2025 dy = 0
2027 tmin0, tmax0 = self.track_trange
2029 scale = math.exp(-dy*5.)
2030 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2031 frac = x0/float(self.width())
2032 dt = dx*(tmax0-tmin0)*scale
2034 self.interrupt_following()
2035 self.set_time_range(
2036 tmin0 - dt - dtr*frac,
2037 tmax0 - dt + dtr*(1.-frac))
2039 self.update()
2040 else:
2041 self.hoovering(point.x(), point.y())
2043 self.update_status()
2045 def nslc_ids_under_cursor(self, x, y):
2046 ftrack = self.track_to_screen.rev(y)
2047 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2048 return nslc_ids
2050 def marker_under_cursor(self, x, y):
2051 mouset = self.time_projection.rev(x)
2052 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2053 relevant_nslc_ids = None
2054 for marker in self.markers:
2055 if marker.kind not in self.visible_marker_kinds:
2056 continue
2058 if (abs(mouset-marker.tmin) < deltat or
2059 abs(mouset-marker.tmax) < deltat):
2061 if relevant_nslc_ids is None:
2062 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2064 marker_nslc_ids = marker.get_nslc_ids()
2065 if not marker_nslc_ids:
2066 return marker
2068 for nslc_id in marker_nslc_ids:
2069 if nslc_id in relevant_nslc_ids:
2070 return marker
2072 def hoovering(self, x, y):
2073 mouset = self.time_projection.rev(x)
2074 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2075 needupdate = False
2076 haveone = False
2077 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2078 for marker in self.markers:
2079 if marker.kind not in self.visible_marker_kinds:
2080 continue
2082 state = abs(mouset-marker.tmin) < deltat or \
2083 abs(mouset-marker.tmax) < deltat and not haveone
2085 if state:
2086 xstate = False
2088 marker_nslc_ids = marker.get_nslc_ids()
2089 if not marker_nslc_ids:
2090 xstate = True
2092 for nslc in relevant_nslc_ids:
2093 if marker.match_nslc(nslc):
2094 xstate = True
2096 state = xstate
2098 if state:
2099 haveone = True
2100 oldstate = marker.is_alerted()
2101 if oldstate != state:
2102 needupdate = True
2103 marker.set_alerted(state)
2104 if state:
2105 self.message = marker.hoover_message()
2107 if not haveone:
2108 self.message = None
2110 if needupdate:
2111 self.update()
2113 def event(self, event):
2114 if event.type() == qc.QEvent.KeyPress:
2115 self.keyPressEvent(event)
2116 return True
2117 else:
2118 return base.event(self, event)
2120 def keyPressEvent(self, key_event):
2121 self.show_all = False
2122 dt = self.tmax - self.tmin
2123 tmid = (self.tmin + self.tmax) / 2.
2125 key = key_event.key()
2126 try:
2127 keytext = str(key_event.text())
2128 except UnicodeEncodeError:
2129 return
2131 if key == qc.Qt.Key_Space:
2132 self.interrupt_following()
2133 self.set_time_range(self.tmin+dt, self.tmax+dt)
2135 elif key == qc.Qt.Key_Up:
2136 for m in self.selected_markers():
2137 if isinstance(m, PhaseMarker):
2138 if key_event.modifiers() & qc.Qt.ShiftModifier:
2139 p = 0
2140 else:
2141 p = 1 if m.get_polarity() != 1 else None
2142 m.set_polarity(p)
2144 elif key == qc.Qt.Key_Down:
2145 for m in self.selected_markers():
2146 if isinstance(m, PhaseMarker):
2147 if key_event.modifiers() & qc.Qt.ShiftModifier:
2148 p = 0
2149 else:
2150 p = -1 if m.get_polarity() != -1 else None
2151 m.set_polarity(p)
2153 elif key == qc.Qt.Key_B:
2154 dt = self.tmax - self.tmin
2155 self.interrupt_following()
2156 self.set_time_range(self.tmin-dt, self.tmax-dt)
2158 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2159 self.interrupt_following()
2161 tgo = None
2163 class TraceDummy(object):
2164 def __init__(self, marker):
2165 self._marker = marker
2167 @property
2168 def nslc_id(self):
2169 return self._marker.one_nslc()
2171 def marker_to_itrack(marker):
2172 try:
2173 return self.key_to_row.get(
2174 self.gather(TraceDummy(marker)), -1)
2176 except MarkerOneNSLCRequired:
2177 return -1
2179 emarker, pmarkers = self.get_active_markers()
2180 pmarkers = [
2181 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2182 pmarkers.sort(key=lambda m: (
2183 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2185 if key == qc.Qt.Key_Backtab:
2186 pmarkers.reverse()
2188 smarkers = self.selected_markers()
2189 iselected = []
2190 for sm in smarkers:
2191 try:
2192 iselected.append(pmarkers.index(sm))
2193 except ValueError:
2194 pass
2196 if iselected:
2197 icurrent = max(iselected) + 1
2198 else:
2199 icurrent = 0
2201 if icurrent < len(pmarkers):
2202 self.deselect_all()
2203 cmarker = pmarkers[icurrent]
2204 cmarker.selected = True
2205 tgo = cmarker.tmin
2206 if not self.tmin < tgo < self.tmax:
2207 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2209 itrack = marker_to_itrack(cmarker)
2210 if itrack != -1:
2211 if itrack < self.shown_tracks_range[0]:
2212 self.scroll_tracks(
2213 - (self.shown_tracks_range[0] - itrack))
2214 elif self.shown_tracks_range[1] <= itrack:
2215 self.scroll_tracks(
2216 itrack - self.shown_tracks_range[1]+1)
2218 if itrack not in self.track_to_nslc_ids:
2219 self.go_to_selection()
2221 elif keytext in ('p', 'n', 'P', 'N'):
2222 smarkers = self.selected_markers()
2223 tgo = None
2224 dir = str(keytext)
2225 if smarkers:
2226 tmid = smarkers[0].tmin
2227 for smarker in smarkers:
2228 if dir == 'n':
2229 tmid = max(smarker.tmin, tmid)
2230 else:
2231 tmid = min(smarker.tmin, tmid)
2233 tgo = tmid
2235 if dir.lower() == 'n':
2236 for marker in sorted(
2237 self.markers,
2238 key=operator.attrgetter('tmin')):
2240 t = marker.tmin
2241 if t > tmid and \
2242 marker.kind in self.visible_marker_kinds and \
2243 (dir == 'n' or
2244 isinstance(marker, EventMarker)):
2246 self.deselect_all()
2247 marker.selected = True
2248 tgo = t
2249 break
2250 else:
2251 for marker in sorted(
2252 self.markers,
2253 key=operator.attrgetter('tmin'),
2254 reverse=True):
2256 t = marker.tmin
2257 if t < tmid and \
2258 marker.kind in self.visible_marker_kinds and \
2259 (dir == 'p' or
2260 isinstance(marker, EventMarker)):
2261 self.deselect_all()
2262 marker.selected = True
2263 tgo = t
2264 break
2266 if tgo is not None:
2267 self.interrupt_following()
2268 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2270 elif keytext == 'r':
2271 if self.pile.reload_modified():
2272 self.reloaded = True
2274 elif keytext == 'R':
2275 self.setup_snufflings()
2277 elif key == qc.Qt.Key_Backspace:
2278 self.remove_selected_markers()
2280 elif keytext == 'a':
2281 for marker in self.markers:
2282 if ((self.tmin <= marker.tmin <= self.tmax or
2283 self.tmin <= marker.tmax <= self.tmax) and
2284 marker.kind in self.visible_marker_kinds):
2285 marker.selected = True
2286 else:
2287 marker.selected = False
2289 elif keytext == 'A':
2290 for marker in self.markers:
2291 if marker.kind in self.visible_marker_kinds:
2292 marker.selected = True
2294 elif keytext == 'd':
2295 self.deselect_all()
2297 elif keytext == 'E':
2298 self.deactivate_event_marker()
2300 elif keytext == 'e':
2301 markers = self.selected_markers()
2302 event_markers_in_spe = [
2303 marker for marker in markers
2304 if not isinstance(marker, PhaseMarker)]
2306 phase_markers = [
2307 marker for marker in markers
2308 if isinstance(marker, PhaseMarker)]
2310 if len(event_markers_in_spe) == 1:
2311 event_marker = event_markers_in_spe[0]
2312 if not isinstance(event_marker, EventMarker):
2313 nslcs = list(event_marker.nslc_ids)
2314 lat, lon = 0.0, 0.0
2315 old = self.get_active_event()
2316 if len(nslcs) == 1:
2317 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2318 elif old is not None:
2319 lat, lon = old.lat, old.lon
2321 event_marker.convert_to_event_marker(lat, lon)
2323 self.set_active_event_marker(event_marker)
2324 event = event_marker.get_event()
2325 for marker in phase_markers:
2326 marker.set_event(event)
2328 else:
2329 for marker in event_markers_in_spe:
2330 marker.convert_to_event_marker()
2332 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'):
2333 for marker in self.selected_markers():
2334 marker.set_kind(int(keytext))
2335 self.emit_selected_markers()
2337 elif key in fkey_map:
2338 self.handle_fkeys(key)
2340 elif key == qc.Qt.Key_Escape:
2341 if self.picking:
2342 self.stop_picking(0, 0, abort=True)
2344 elif key == qc.Qt.Key_PageDown:
2345 self.scroll_tracks(
2346 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2348 elif key == qc.Qt.Key_PageUp:
2349 self.scroll_tracks(
2350 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2352 elif key == qc.Qt.Key_Plus:
2353 self.zoom_tracks(0., 1.)
2355 elif key == qc.Qt.Key_Minus:
2356 self.zoom_tracks(0., -1.)
2358 elif key == qc.Qt.Key_Equal:
2359 ntracks_shown = self.shown_tracks_range[1] - \
2360 self.shown_tracks_range[0]
2361 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2362 self.zoom_tracks(0., dtracks)
2364 elif key == qc.Qt.Key_Colon:
2365 self.want_input.emit()
2367 elif keytext == 'f':
2368 self.toggle_fullscreen()
2370 elif keytext == 'g':
2371 self.go_to_selection()
2373 elif keytext == 'G':
2374 self.go_to_selection(tight=True)
2376 elif keytext == 'm':
2377 self.toggle_marker_editor()
2379 elif keytext == 'c':
2380 self.toggle_main_controls()
2382 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2383 dir = 1
2384 amount = 1
2385 if key_event.key() == qc.Qt.Key_Left:
2386 dir = -1
2387 if key_event.modifiers() & qc.Qt.ShiftModifier:
2388 amount = 10
2389 self.nudge_selected_markers(dir*amount)
2390 else:
2391 super().keyPressEvent(key_event)
2393 if keytext != '' and keytext in 'degaApPnN':
2394 self.emit_selected_markers()
2396 self.update()
2397 self.update_status()
2399 def handle_fkeys(self, key):
2400 self.set_phase_kind(
2401 self.selected_markers(),
2402 fkey_map[key] + 1)
2403 self.emit_selected_markers()
2405 def emit_selected_markers(self):
2406 ibounds = []
2407 last_selected = False
2408 for imarker, marker in enumerate(self.markers):
2409 this_selected = marker.is_selected()
2410 if this_selected != last_selected:
2411 ibounds.append(imarker)
2413 last_selected = this_selected
2415 if last_selected:
2416 ibounds.append(len(self.markers))
2418 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2419 self.n_selected_markers = sum(
2420 chunk[1] - chunk[0] for chunk in chunks)
2421 self.marker_selection_changed.emit(chunks)
2423 def toggle_marker_editor(self):
2424 self.panel_parent.toggle_marker_editor()
2426 def toggle_main_controls(self):
2427 self.panel_parent.toggle_main_controls()
2429 def nudge_selected_markers(self, npixels):
2430 a, b = self.time_projection.ur
2431 c, d = self.time_projection.xr
2432 for marker in self.selected_markers():
2433 if not isinstance(marker, EventMarker):
2434 marker.tmin += npixels * (d-c)/b
2435 marker.tmax += npixels * (d-c)/b
2437 def toggle_fullscreen(self):
2438 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2439 self.window().windowState() & qc.Qt.WindowMaximized:
2440 self.window().showNormal()
2441 else:
2442 if is_macos:
2443 self.window().showMaximized()
2444 else:
2445 self.window().showFullScreen()
2447 def about(self):
2448 fn = pyrocko.util.data_file('snuffler.png')
2449 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2450 txt = f.read()
2451 label = qw.QLabel(txt % {'logo': fn})
2452 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2453 self.show_doc('About', [label], target='tab')
2455 def help(self):
2456 class MyScrollArea(qw.QScrollArea):
2458 def sizeHint(self):
2459 s = qc.QSize()
2460 s.setWidth(self.widget().sizeHint().width())
2461 s.setHeight(self.widget().sizeHint().height())
2462 return s
2464 with open(pyrocko.util.data_file(
2465 'snuffler_help.html')) as f:
2466 hcheat = qw.QLabel(f.read())
2468 with open(pyrocko.util.data_file(
2469 'snuffler_help_epilog.html')) as f:
2470 hepilog = qw.QLabel(f.read())
2472 for h in [hcheat, hepilog]:
2473 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2474 h.setWordWrap(True)
2476 self.show_doc('Help', [hcheat, hepilog], target='panel')
2478 def show_doc(self, name, labels, target='panel'):
2479 scroller = qw.QScrollArea()
2480 frame = qw.QFrame(scroller)
2481 frame.setLineWidth(0)
2482 layout = qw.QVBoxLayout()
2483 layout.setContentsMargins(0, 0, 0, 0)
2484 layout.setSpacing(0)
2485 frame.setLayout(layout)
2486 scroller.setWidget(frame)
2487 scroller.setWidgetResizable(True)
2488 frame.setBackgroundRole(qg.QPalette.Base)
2489 for h in labels:
2490 h.setParent(frame)
2491 h.setMargin(3)
2492 h.setTextInteractionFlags(
2493 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2494 h.setBackgroundRole(qg.QPalette.Base)
2495 layout.addWidget(h)
2496 h.linkActivated.connect(
2497 self.open_link)
2499 if self.panel_parent is not None:
2500 if target == 'panel':
2501 self.panel_parent.add_panel(
2502 name, scroller, True, volatile=False)
2503 else:
2504 self.panel_parent.add_tab(name, scroller)
2506 def open_link(self, link):
2507 qg.QDesktopServices.openUrl(qc.QUrl(link))
2509 def wheelEvent(self, wheel_event):
2510 self.wheel_pos += wheel_event.angleDelta().y()
2512 n = self.wheel_pos // 120
2513 self.wheel_pos = self.wheel_pos % 120
2514 if n == 0:
2515 return
2517 amount = max(
2518 1.,
2519 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2520 wdelta = amount * n
2522 trmin, trmax = self.track_to_screen.get_in_range()
2523 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2524 / (trmax-trmin)
2526 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2527 self.zoom_tracks(anchor, wdelta)
2528 else:
2529 self.scroll_tracks(-wdelta)
2531 def dragEnterEvent(self, event):
2532 if event.mimeData().hasUrls():
2533 if any(url.toLocalFile() for url in event.mimeData().urls()):
2534 event.setDropAction(qc.Qt.LinkAction)
2535 event.accept()
2537 def dropEvent(self, event):
2538 if event.mimeData().hasUrls():
2539 paths = list(
2540 str(url.toLocalFile()) for url in event.mimeData().urls())
2541 event.acceptProposedAction()
2542 self.load(paths)
2544 def get_phase_name(self, kind):
2545 return self.config.get_phase_name(kind)
2547 def set_phase_kind(self, markers, kind):
2548 phasename = self.get_phase_name(kind)
2550 for marker in markers:
2551 if isinstance(marker, PhaseMarker):
2552 if kind == 10:
2553 marker.convert_to_marker()
2554 else:
2555 marker.set_phasename(phasename)
2556 marker.set_event(self.get_active_event())
2558 elif isinstance(marker, EventMarker):
2559 pass
2561 else:
2562 if kind != 10:
2563 event = self.get_active_event()
2564 marker.convert_to_phase_marker(
2565 event, phasename, None, False)
2567 def set_ntracks(self, ntracks):
2568 if self.ntracks != ntracks:
2569 self.ntracks = ntracks
2570 if self.shown_tracks_range is not None:
2571 l, h = self.shown_tracks_range
2572 else:
2573 l, h = 0, self.ntracks
2575 self.tracks_range_changed.emit(self.ntracks, l, h)
2577 def set_tracks_range(self, range, start=None):
2579 low, high = range
2580 low = min(self.ntracks-1, low)
2581 high = min(self.ntracks, high)
2582 low = max(0, low)
2583 high = max(1, high)
2585 if start is None:
2586 start = float(low)
2588 if self.shown_tracks_range != (low, high):
2589 self.shown_tracks_range = low, high
2590 self.shown_tracks_start = start
2592 self.tracks_range_changed.emit(self.ntracks, low, high)
2594 def scroll_tracks(self, shift):
2595 shown = self.shown_tracks_range
2596 shiftmin = -shown[0]
2597 shiftmax = self.ntracks-shown[1]
2598 shift = max(shiftmin, shift)
2599 shift = min(shiftmax, shift)
2600 shown = shown[0] + shift, shown[1] + shift
2602 self.set_tracks_range((int(shown[0]), int(shown[1])))
2604 self.update()
2606 def zoom_tracks(self, anchor, delta):
2607 ntracks_shown = self.shown_tracks_range[1] \
2608 - self.shown_tracks_range[0]
2610 if (ntracks_shown == 1 and delta <= 0) or \
2611 (ntracks_shown == self.ntracks and delta >= 0):
2612 return
2614 ntracks_shown += int(round(delta))
2615 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2617 u = self.shown_tracks_start
2618 nu = max(0., u-anchor*delta)
2619 nv = nu + ntracks_shown
2620 if nv > self.ntracks:
2621 nu -= nv - self.ntracks
2622 nv -= nv - self.ntracks
2624 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2626 self.ntracks_shown_max = self.shown_tracks_range[1] \
2627 - self.shown_tracks_range[0]
2629 self.update()
2631 def content_time_range(self):
2632 pile = self.get_pile()
2633 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2634 if tmin is None:
2635 tmin = initial_time_range[0]
2636 if tmax is None:
2637 tmax = initial_time_range[1]
2639 return tmin, tmax
2641 def content_deltat_range(self):
2642 pile = self.get_pile()
2644 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2646 if deltatmin is None:
2647 deltatmin = 0.001
2649 if deltatmax is None:
2650 deltatmax = 1000.0
2652 return deltatmin, deltatmax
2654 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2655 if tmax < tmin:
2656 tmin, tmax = tmax, tmin
2658 deltatmin = self.content_deltat_range()[0]
2659 dt = deltatmin * self.visible_length * 0.95
2661 if dt == 0.0:
2662 dt = 1.0
2664 if tight:
2665 if tmax != tmin:
2666 dtm = tmax - tmin
2667 tmin -= dtm*0.1
2668 tmax += dtm*0.1
2669 return tmin, tmax
2670 else:
2671 tcenter = (tmin + tmax) / 2.
2672 tmin = tcenter - 0.5*dt
2673 tmax = tcenter + 0.5*dt
2674 return tmin, tmax
2676 if tmax-tmin < dt:
2677 vmin, vmax = self.get_time_range()
2678 dt = min(vmax - vmin, dt)
2680 tcenter = (tmin+tmax)/2.
2681 etmin, etmax = tmin, tmax
2682 tmin = min(etmin, tcenter - 0.5*dt)
2683 tmax = max(etmax, tcenter + 0.5*dt)
2684 dtm = tmax-tmin
2685 if etmin == tmin:
2686 tmin -= dtm*0.1
2687 if etmax == tmax:
2688 tmax += dtm*0.1
2690 else:
2691 dtm = tmax-tmin
2692 tmin -= dtm*0.1
2693 tmax += dtm*0.1
2695 return tmin, tmax
2697 def go_to_selection(self, tight=False):
2698 markers = self.selected_markers()
2699 if markers:
2700 tmax, tmin = self.content_time_range()
2701 for marker in markers:
2702 tmin = min(tmin, marker.tmin)
2703 tmax = max(tmax, marker.tmax)
2705 else:
2706 if tight:
2707 vmin, vmax = self.get_time_range()
2708 tmin = tmax = (vmin + vmax) / 2.
2709 else:
2710 tmin, tmax = self.content_time_range()
2712 tmin, tmax = self.make_good_looking_time_range(
2713 tmin, tmax, tight=tight)
2715 self.interrupt_following()
2716 self.set_time_range(tmin, tmax)
2717 self.update()
2719 def go_to_time(self, t, tlen=None):
2720 tmax = t
2721 if tlen is not None:
2722 tmax = t+tlen
2723 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2724 self.interrupt_following()
2725 self.set_time_range(tmin, tmax)
2726 self.update()
2728 def go_to_event_by_name(self, name):
2729 for marker in self.markers:
2730 if isinstance(marker, EventMarker):
2731 event = marker.get_event()
2732 if event.name and event.name.lower() == name.lower():
2733 tmin, tmax = self.make_good_looking_time_range(
2734 event.time, event.time)
2736 self.interrupt_following()
2737 self.set_time_range(tmin, tmax)
2739 def printit(self):
2740 from .qt_compat import qprint
2741 printer = qprint.QPrinter()
2742 printer.setOrientation(qprint.QPrinter.Landscape)
2744 dialog = qprint.QPrintDialog(printer, self)
2745 dialog.setWindowTitle('Print')
2747 if dialog.exec_() != qw.QDialog.Accepted:
2748 return
2750 painter = qg.QPainter()
2751 painter.begin(printer)
2752 page = printer.pageRect()
2753 self.drawit(
2754 painter, printmode=False, w=page.width(), h=page.height())
2756 painter.end()
2758 def savesvg(self, fn=None):
2760 if not fn:
2761 fn, _ = qw.QFileDialog.getSaveFileName(
2762 self,
2763 'Save as SVG|PNG',
2764 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2765 'SVG|PNG (*.svg *.png)',
2766 options=qfiledialog_options)
2768 if fn == '':
2769 return
2771 fn = str(fn)
2773 if fn.lower().endswith('.svg'):
2774 try:
2775 w, h = 842, 595
2776 margin = 0.025
2777 m = max(w, h)*margin
2779 generator = qsvg.QSvgGenerator()
2780 generator.setFileName(fn)
2781 generator.setSize(qc.QSize(w, h))
2782 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2784 painter = qg.QPainter()
2785 painter.begin(generator)
2786 self.drawit(painter, printmode=False, w=w, h=h)
2787 painter.end()
2789 except Exception as e:
2790 self.fail('Failed to write SVG file: %s' % str(e))
2792 elif fn.lower().endswith('.png'):
2793 pixmap = self.grab()
2795 try:
2796 pixmap.save(fn)
2798 except Exception as e:
2799 self.fail('Failed to write PNG file: %s' % str(e))
2801 else:
2802 self.fail(
2803 'Unsupported file type: filename must end with ".svg" or '
2804 '".png".')
2806 def paintEvent(self, paint_ev):
2807 '''
2808 Called by QT whenever widget needs to be painted.
2809 '''
2810 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2811 # was called twice (by different threads?), causing segfaults.
2812 if self.in_paint_event:
2813 logger.warning('Blocking reentrant call to paintEvent().')
2814 return
2816 self.in_paint_event = True
2818 painter = qg.QPainter(self)
2820 if self.menuitem_antialias.isChecked():
2821 painter.setRenderHint(qg.QPainter.Antialiasing)
2823 self.drawit(painter)
2825 logger.debug(
2826 'Time spent drawing: '
2827 ' user:%.3f sys:%.3f children_user:%.3f'
2828 ' childred_sys:%.3f elapsed:%.3f' %
2829 (self.timer_draw - self.timer_cutout))
2831 logger.debug(
2832 'Time spent processing:'
2833 ' user:%.3f sys:%.3f children_user:%.3f'
2834 ' childred_sys:%.3f elapsed:%.3f' %
2835 self.timer_cutout.get())
2837 self.time_spent_painting = self.timer_draw.get()[-1]
2838 self.time_last_painted = time.time()
2839 self.in_paint_event = False
2841 def determine_box_styles(self):
2843 traces = list(self.pile.iter_traces())
2844 traces.sort(key=operator.attrgetter('full_id'))
2845 istyle = 0
2846 trace_styles = {}
2847 for itr, tr in enumerate(traces):
2848 if itr > 0:
2849 other = traces[itr-1]
2850 if not (
2851 other.nslc_id == tr.nslc_id
2852 and other.deltat == tr.deltat
2853 and abs(other.tmax - tr.tmin)
2854 < gap_lap_tolerance*tr.deltat):
2856 istyle += 1
2858 trace_styles[tr.full_id, tr.deltat] = istyle
2860 self.trace_styles = trace_styles
2862 def draw_trace_boxes(self, p, time_projection, track_projections):
2864 for v_projection in track_projections.values():
2865 v_projection.set_in_range(0., 1.)
2867 def selector(x):
2868 return x.overlaps(*time_projection.get_in_range())
2870 if self.trace_filter is not None:
2871 def tselector(x):
2872 return selector(x) and self.trace_filter(x)
2874 else:
2875 tselector = selector
2877 traces = list(self.pile.iter_traces(
2878 group_selector=selector, trace_selector=tselector))
2880 traces.sort(key=operator.attrgetter('full_id'))
2882 def drawbox(itrack, istyle, traces):
2883 v_projection = track_projections[itrack]
2884 dvmin = v_projection(0.)
2885 dvmax = v_projection(1.)
2886 dtmin = time_projection.clipped(traces[0].tmin)
2887 dtmax = time_projection.clipped(traces[-1].tmax)
2889 style = box_styles[istyle % len(box_styles)]
2890 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2891 p.fillRect(rect, style.fill_brush)
2892 p.setPen(style.frame_pen)
2893 p.drawRect(rect)
2895 traces_by_style = {}
2896 for itr, tr in enumerate(traces):
2897 gt = self.gather(tr)
2898 if gt not in self.key_to_row:
2899 continue
2901 itrack = self.key_to_row[gt]
2902 if itrack not in track_projections:
2903 continue
2905 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2907 if len(traces) < 500:
2908 drawbox(itrack, istyle, [tr])
2909 else:
2910 if (itrack, istyle) not in traces_by_style:
2911 traces_by_style[itrack, istyle] = []
2912 traces_by_style[itrack, istyle].append(tr)
2914 for (itrack, istyle), traces in traces_by_style.items():
2915 drawbox(itrack, istyle, traces)
2917 def draw_visible_markers(
2918 self, p, vcenter_projection, primary_pen):
2920 try:
2921 markers = self.markers.with_key_in_limited(
2922 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2924 except pyrocko.pile.TooMany:
2925 tmin = self.markers[0].tmin
2926 tmax = self.markers[-1].tmax
2927 umin_view, umax_view = self.time_projection.get_out_range()
2928 umin = max(umin_view, self.time_projection(tmin))
2929 umax = min(umax_view, self.time_projection(tmax))
2930 v0, _ = vcenter_projection.get_out_range()
2931 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2933 p.save()
2935 pen = qg.QPen(primary_pen)
2936 pen.setWidth(2)
2937 pen.setStyle(qc.Qt.DotLine)
2938 # pat = [5., 3.]
2939 # pen.setDashPattern(pat)
2940 p.setPen(pen)
2942 if self.n_selected_markers == len(self.markers):
2943 s_selected = ' (all selected)'
2944 elif self.n_selected_markers > 0:
2945 s_selected = ' (%i selected)' % self.n_selected_markers
2946 else:
2947 s_selected = ''
2949 draw_label(
2950 p, umin+10., v0-10.,
2951 '%i Markers' % len(self.markers) + s_selected,
2952 label_bg, 'LB')
2954 line = qc.QLineF(umin, v0, umax, v0)
2955 p.drawLine(line)
2956 p.restore()
2958 return
2960 for marker in markers:
2961 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2962 and marker.kind in self.visible_marker_kinds:
2964 marker.draw(
2965 p, self.time_projection, vcenter_projection,
2966 with_label=True)
2968 def get_squirrel(self):
2969 try:
2970 return self.pile._squirrel
2971 except AttributeError:
2972 return None
2974 def draw_coverage(self, p, time_projection, track_projections):
2975 sq = self.get_squirrel()
2976 if sq is None:
2977 return
2979 def drawbox(itrack, tmin, tmax, style):
2980 v_projection = track_projections[itrack]
2981 dvmin = v_projection(0.)
2982 dvmax = v_projection(1.)
2983 dtmin = time_projection.clipped(tmin)
2984 dtmax = time_projection.clipped(tmax)
2986 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2987 p.fillRect(rect, style.fill_brush)
2988 p.setPen(style.frame_pen)
2989 p.drawRect(rect)
2991 pattern_list = []
2992 pattern_to_itrack = {}
2993 for key in self.track_keys:
2994 itrack = self.key_to_row[key]
2995 if itrack not in track_projections:
2996 continue
2998 pattern = self.track_patterns[itrack]
2999 pattern_to_itrack[tuple(pattern)] = itrack
3000 pattern_list.append(tuple(pattern))
3002 vmin, vmax = self.get_time_range()
3004 for kind in ['waveform', 'waveform_promise']:
3005 for coverage in sq.get_coverage(
3006 kind, vmin, vmax, pattern_list, limit=500):
3007 itrack = pattern_to_itrack[coverage.pattern.nslc]
3009 if coverage.changes is None:
3010 drawbox(
3011 itrack, coverage.tmin, coverage.tmax,
3012 box_styles_coverage[kind][0])
3013 else:
3014 t = None
3015 pcount = 0
3016 for tb, count in coverage.changes:
3017 if t is not None and tb > t:
3018 if pcount > 0:
3019 drawbox(
3020 itrack, t, tb,
3021 box_styles_coverage[kind][
3022 min(len(box_styles_coverage)-1,
3023 pcount)])
3025 t = tb
3026 pcount = count
3028 def drawit(self, p, printmode=False, w=None, h=None):
3029 '''
3030 This performs the actual drawing.
3031 '''
3033 self.timer_draw.start()
3034 show_boxes = self.menuitem_showboxes.isChecked()
3035 sq = self.get_squirrel()
3037 if self.gather is None:
3038 self.set_gathering()
3040 if self.pile_has_changed:
3042 if not self.sortingmode_change_delayed():
3043 self.sortingmode_change()
3045 if show_boxes and sq is None:
3046 self.determine_box_styles()
3048 self.pile_has_changed = False
3050 if h is None:
3051 h = float(self.height())
3052 if w is None:
3053 w = float(self.width())
3055 if printmode:
3056 primary_color = (0, 0, 0)
3057 else:
3058 primary_color = pyrocko.plot.tango_colors['aluminium5']
3060 primary_pen = qg.QPen(qg.QColor(*primary_color))
3062 ax_h = float(self.ax_height)
3064 vbottom_ax_projection = Projection()
3065 vtop_ax_projection = Projection()
3066 vcenter_projection = Projection()
3068 self.time_projection.set_out_range(0., w)
3069 vbottom_ax_projection.set_out_range(h-ax_h, h)
3070 vtop_ax_projection.set_out_range(0., ax_h)
3071 vcenter_projection.set_out_range(ax_h, h-ax_h)
3072 vcenter_projection.set_in_range(0., 1.)
3073 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3075 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3076 track_projections = {}
3077 for i in range(*self.shown_tracks_range):
3078 proj = Projection()
3079 proj.set_out_range(
3080 self.track_to_screen(i+0.05),
3081 self.track_to_screen(i+1.-0.05))
3083 track_projections[i] = proj
3085 if self.tmin > self.tmax:
3086 return
3088 self.time_projection.set_in_range(self.tmin, self.tmax)
3089 vbottom_ax_projection.set_in_range(0, ax_h)
3091 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3093 yscaler = pyrocko.plot.AutoScaler()
3095 p.setPen(primary_pen)
3097 font = qg.QFont()
3098 font.setBold(True)
3100 axannotfont = qg.QFont()
3101 axannotfont.setBold(True)
3102 axannotfont.setPointSize(8)
3104 processed_traces = self.prepare_cutout2(
3105 self.tmin, self.tmax,
3106 trace_selector=self.trace_selector,
3107 degap=self.menuitem_degap.isChecked(),
3108 demean=self.menuitem_demean.isChecked())
3110 if not printmode and show_boxes:
3111 if (self.view_mode is ViewMode.Wiggle) \
3112 or (self.view_mode is ViewMode.Waterfall
3113 and not processed_traces):
3115 if sq is None:
3116 self.draw_trace_boxes(
3117 p, self.time_projection, track_projections)
3119 else:
3120 self.draw_coverage(
3121 p, self.time_projection, track_projections)
3123 p.setFont(font)
3124 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3126 color_lookup = dict(
3127 [(k, i) for (i, k) in enumerate(self.color_keys)])
3129 self.track_to_nslc_ids = {}
3130 nticks = 0
3131 annot_labels = []
3133 if self.view_mode is ViewMode.Waterfall and processed_traces:
3134 waterfall = self.waterfall
3135 waterfall.set_time_range(self.tmin, self.tmax)
3136 waterfall.set_traces(processed_traces)
3137 waterfall.set_cmap(self.waterfall_cmap)
3138 waterfall.set_integrate(self.waterfall_integrate)
3139 waterfall.set_clip(
3140 self.waterfall_clip_min, self.waterfall_clip_max)
3141 waterfall.show_absolute_values(
3142 self.waterfall_show_absolute)
3144 rect = qc.QRectF(
3145 0, self.ax_height,
3146 self.width(), self.height() - self.ax_height*2
3147 )
3148 waterfall.draw_waterfall(p, rect=rect)
3150 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3151 show_scales = self.menuitem_showscalerange.isChecked() \
3152 or self.menuitem_showscaleaxis.isChecked()
3154 fm = qg.QFontMetrics(axannotfont, p.device())
3155 trackheight = self.track_to_screen(1.-0.05) \
3156 - self.track_to_screen(0.05)
3158 nlinesavail = trackheight/float(fm.lineSpacing())
3160 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3161 if self.menuitem_showscaleaxis.isChecked() \
3162 else 15
3164 yscaler = pyrocko.plot.AutoScaler(
3165 no_exp_interval=(-3, 2), approx_ticks=nticks,
3166 snap=show_scales
3167 and not self.menuitem_showscaleaxis.isChecked())
3169 data_ranges = pyrocko.trace.minmax(
3170 processed_traces,
3171 key=self.scaling_key,
3172 mode=self.scaling_base[0],
3173 outer_mode=self.scaling_base[1])
3175 if not self.menuitem_fixscalerange.isChecked():
3176 self.old_data_ranges = data_ranges
3177 else:
3178 data_ranges.update(self.old_data_ranges)
3180 self.apply_scaling_hooks(data_ranges)
3182 trace_to_itrack = {}
3183 track_scaling_keys = {}
3184 track_scaling_colors = {}
3185 for trace in processed_traces:
3186 gt = self.gather(trace)
3187 if gt not in self.key_to_row:
3188 continue
3190 itrack = self.key_to_row[gt]
3191 if itrack not in track_projections:
3192 continue
3194 trace_to_itrack[trace] = itrack
3196 if itrack not in self.track_to_nslc_ids:
3197 self.track_to_nslc_ids[itrack] = set()
3199 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3201 if itrack not in track_scaling_keys:
3202 track_scaling_keys[itrack] = set()
3204 scaling_key = self.scaling_key(trace)
3205 track_scaling_keys[itrack].add(scaling_key)
3207 color = pyrocko.plot.color(
3208 color_lookup[self.color_gather(trace)])
3210 k = itrack, scaling_key
3211 if k not in track_scaling_colors \
3212 and self.menuitem_colortraces.isChecked():
3213 track_scaling_colors[k] = color
3214 else:
3215 track_scaling_colors[k] = primary_color
3217 # y axes, zero lines
3218 trace_projections = {}
3219 for itrack in list(track_projections.keys()):
3220 if itrack not in track_scaling_keys:
3221 continue
3222 uoff = 0
3223 for scaling_key in track_scaling_keys[itrack]:
3224 data_range = data_ranges[scaling_key]
3225 dymin, dymax = data_range
3226 ymin, ymax, yinc = yscaler.make_scale(
3227 (dymin/self.gain, dymax/self.gain))
3228 iexp = yscaler.make_exp(yinc)
3229 factor = 10**iexp
3230 trace_projection = track_projections[itrack].copy()
3231 trace_projection.set_in_range(ymax, ymin)
3232 trace_projections[itrack, scaling_key] = \
3233 trace_projection
3234 umin, umax = self.time_projection.get_out_range()
3235 vmin, vmax = trace_projection.get_out_range()
3236 umax_zeroline = umax
3237 uoffnext = uoff
3239 if show_scales:
3240 pen = qg.QPen(primary_pen)
3241 k = itrack, scaling_key
3242 if k in track_scaling_colors:
3243 c = qg.QColor(*track_scaling_colors[
3244 itrack, scaling_key])
3246 pen.setColor(c)
3248 p.setPen(pen)
3249 if nlinesavail > 3:
3250 if self.menuitem_showscaleaxis.isChecked():
3251 ymin_annot = math.ceil(ymin/yinc)*yinc
3252 ny_annot = int(
3253 math.floor(ymax/yinc)
3254 - math.ceil(ymin/yinc)) + 1
3256 for iy_annot in range(ny_annot):
3257 y = ymin_annot + iy_annot*yinc
3258 v = trace_projection(y)
3259 line = qc.QLineF(
3260 umax-10-uoff, v, umax-uoff, v)
3262 p.drawLine(line)
3263 if iy_annot == ny_annot - 1 \
3264 and iexp != 0:
3265 sexp = ' × ' \
3266 '10<sup>%i</sup>' % iexp
3267 else:
3268 sexp = ''
3270 snum = num_to_html(y/factor)
3271 lab = Label(
3272 p,
3273 umax-20-uoff,
3274 v, '%s%s' % (snum, sexp),
3275 label_bg=None,
3276 anchor='MR',
3277 font=axannotfont,
3278 color=c)
3280 uoffnext = max(
3281 lab.rect.width()+30., uoffnext)
3283 annot_labels.append(lab)
3284 if y == 0.:
3285 umax_zeroline = \
3286 umax - 20 \
3287 - lab.rect.width() - 10 \
3288 - uoff
3289 else:
3290 if not show_boxes:
3291 qpoints = make_QPolygonF(
3292 [umax-20-uoff,
3293 umax-10-uoff,
3294 umax-10-uoff,
3295 umax-20-uoff],
3296 [vmax, vmax, vmin, vmin])
3297 p.drawPolyline(qpoints)
3299 snum = num_to_html(ymin)
3300 labmin = Label(
3301 p, umax-15-uoff, vmax, snum,
3302 label_bg=None,
3303 anchor='BR',
3304 font=axannotfont,
3305 color=c)
3307 annot_labels.append(labmin)
3308 snum = num_to_html(ymax)
3309 labmax = Label(
3310 p, umax-15-uoff, vmin, snum,
3311 label_bg=None,
3312 anchor='TR',
3313 font=axannotfont,
3314 color=c)
3316 annot_labels.append(labmax)
3318 for lab in (labmin, labmax):
3319 uoffnext = max(
3320 lab.rect.width()+10., uoffnext)
3322 if self.menuitem_showzeroline.isChecked():
3323 v = trace_projection(0.)
3324 if vmin <= v <= vmax:
3325 line = qc.QLineF(umin, v, umax_zeroline, v)
3326 p.drawLine(line)
3328 uoff = uoffnext
3330 p.setFont(font)
3331 p.setPen(primary_pen)
3332 for trace in processed_traces:
3333 if self.view_mode is not ViewMode.Wiggle:
3334 break
3336 if trace not in trace_to_itrack:
3337 continue
3339 itrack = trace_to_itrack[trace]
3340 scaling_key = self.scaling_key(trace)
3341 trace_projection = trace_projections[
3342 itrack, scaling_key]
3344 vdata = trace_projection(trace.get_ydata())
3346 udata_min = float(self.time_projection(trace.tmin))
3347 udata_max = float(self.time_projection(
3348 trace.tmin+trace.deltat*(vdata.size-1)))
3349 udata = num.linspace(udata_min, udata_max, vdata.size)
3351 qpoints = make_QPolygonF(udata, vdata)
3353 umin, umax = self.time_projection.get_out_range()
3354 vmin, vmax = trace_projection.get_out_range()
3356 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3358 if self.menuitem_cliptraces.isChecked():
3359 p.setClipRect(trackrect)
3361 if self.menuitem_colortraces.isChecked():
3362 color = pyrocko.plot.color(
3363 color_lookup[self.color_gather(trace)])
3364 pen = qg.QPen(qg.QColor(*color), 1)
3365 p.setPen(pen)
3367 p.drawPolyline(qpoints)
3369 if self.floating_marker:
3370 self.floating_marker.draw_trace(
3371 self, p, trace,
3372 self.time_projection, trace_projection, 1.0)
3374 for marker in self.markers.with_key_in(
3375 self.tmin - self.markers_deltat_max,
3376 self.tmax):
3378 if marker.tmin < self.tmax \
3379 and self.tmin < marker.tmax \
3380 and marker.kind \
3381 in self.visible_marker_kinds:
3382 marker.draw_trace(
3383 self, p, trace, self.time_projection,
3384 trace_projection, 1.0)
3386 p.setPen(primary_pen)
3388 if self.menuitem_cliptraces.isChecked():
3389 p.setClipRect(0, 0, int(w), int(h))
3391 if self.floating_marker:
3392 self.floating_marker.draw(
3393 p, self.time_projection, vcenter_projection)
3395 self.draw_visible_markers(
3396 p, vcenter_projection, primary_pen)
3398 p.setPen(primary_pen)
3399 while font.pointSize() > 2:
3400 fm = qg.QFontMetrics(font, p.device())
3401 trackheight = self.track_to_screen(1.-0.05) \
3402 - self.track_to_screen(0.05)
3403 nlinesavail = trackheight/float(fm.lineSpacing())
3404 if nlinesavail > 1:
3405 break
3407 font.setPointSize(font.pointSize()-1)
3409 p.setFont(font)
3410 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3412 for key in self.track_keys:
3413 itrack = self.key_to_row[key]
3414 if itrack in track_projections:
3415 plabel = ' '.join(
3416 [str(x) for x in key if x is not None])
3417 lx = 10
3418 ly = self.track_to_screen(itrack+0.5)
3420 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3421 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3422 continue
3424 contains_cursor = \
3425 self.track_to_screen(itrack) \
3426 < mouse_pos.y() \
3427 < self.track_to_screen(itrack+1)
3429 if not contains_cursor:
3430 continue
3432 font_large = p.font()
3433 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3434 p.setFont(font_large)
3435 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3436 p.setFont(font)
3438 for lab in annot_labels:
3439 lab.draw()
3441 self.timer_draw.stop()
3443 def see_data_params(self):
3445 min_deltat = self.content_deltat_range()[0]
3447 # determine padding and downampling requirements
3448 if self.lowpass is not None:
3449 deltat_target = 1./self.lowpass * 0.25
3450 ndecimate = min(
3451 50,
3452 max(1, int(round(deltat_target / min_deltat))))
3453 tpad = 1./self.lowpass * 2.
3454 else:
3455 ndecimate = 1
3456 tpad = min_deltat*5.
3458 if self.highpass is not None:
3459 tpad = max(1./self.highpass * 2., tpad)
3461 nsee_points_per_trace = 5000*10
3462 tsee = ndecimate*nsee_points_per_trace*min_deltat
3464 return ndecimate, tpad, tsee
3466 def clean_update(self):
3467 self.cached_processed_traces = None
3468 self.update()
3470 def get_adequate_tpad(self):
3471 tpad = 0.
3472 for f in [self.highpass, self.lowpass]:
3473 if f is not None:
3474 tpad = max(tpad, 1.0/f)
3476 for snuffling in self.snufflings:
3477 if snuffling._post_process_hook_enabled \
3478 or snuffling._pre_process_hook_enabled:
3480 tpad = max(tpad, snuffling.get_tpad())
3482 return tpad
3484 def prepare_cutout2(
3485 self, tmin, tmax, trace_selector=None, degap=True,
3486 demean=True, nmax=6000):
3488 if self.pile.is_empty():
3489 return []
3491 nmax = self.visible_length
3493 self.timer_cutout.start()
3495 tsee = tmax-tmin
3496 min_deltat_wo_decimate = tsee/nmax
3497 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3499 min_deltat_allow = min_deltat_wo_decimate
3500 if self.lowpass is not None:
3501 target_deltat_lp = 0.25/self.lowpass
3502 if target_deltat_lp > min_deltat_wo_decimate:
3503 min_deltat_allow = min_deltat_w_decimate
3505 min_deltat_allow = math.exp(
3506 int(math.floor(math.log(min_deltat_allow))))
3508 tmin_ = tmin
3509 tmax_ = tmax
3511 # fetch more than needed?
3512 if self.menuitem_liberal_fetch.isChecked():
3513 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3514 tmin = math.floor(tmin/tlen) * tlen
3515 tmax = math.ceil(tmax/tlen) * tlen
3517 fft_filtering = self.menuitem_fft_filtering.isChecked()
3518 lphp = self.menuitem_lphp.isChecked()
3519 ads = self.menuitem_allowdownsampling.isChecked()
3521 tpad = self.get_adequate_tpad()
3522 tpad = max(tpad, tsee)
3524 # state vector to decide if cached traces can be used
3525 vec = (
3526 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3527 self.highpass, fft_filtering, lphp,
3528 min_deltat_allow, self.rotate, self.shown_tracks_range,
3529 ads, self.pile.get_update_count())
3531 if (self.cached_vec
3532 and self.cached_vec[0] <= vec[0]
3533 and vec[1] <= self.cached_vec[1]
3534 and vec[2:] == self.cached_vec[2:]
3535 and not (self.reloaded or self.menuitem_watch.isChecked())
3536 and self.cached_processed_traces is not None):
3538 logger.debug('Using cached traces')
3539 processed_traces = self.cached_processed_traces
3541 else:
3542 processed_traces = []
3543 if self.pile.deltatmax >= min_deltat_allow:
3545 if isinstance(self.pile, pyrocko.pile.Pile):
3546 def group_selector(gr):
3547 return gr.deltatmax >= min_deltat_allow
3549 kwargs = dict(group_selector=group_selector)
3550 else:
3551 kwargs = {}
3553 if trace_selector is not None:
3554 def trace_selectorx(tr):
3555 return tr.deltat >= min_deltat_allow \
3556 and trace_selector(tr)
3557 else:
3558 def trace_selectorx(tr):
3559 return tr.deltat >= min_deltat_allow
3561 for traces in self.pile.chopper(
3562 tmin=tmin, tmax=tmax, tpad=tpad,
3563 want_incomplete=True,
3564 degap=degap,
3565 maxgap=gap_lap_tolerance,
3566 maxlap=gap_lap_tolerance,
3567 keep_current_files_open=True,
3568 trace_selector=trace_selectorx,
3569 accessor_id=id(self),
3570 snap=(math.floor, math.ceil),
3571 include_last=True, **kwargs):
3573 if demean:
3574 for tr in traces:
3575 if (tr.meta and tr.meta.get('tabu', False)):
3576 continue
3577 y = tr.get_ydata()
3578 tr.set_ydata(y - num.mean(y))
3580 traces = self.pre_process_hooks(traces)
3582 for trace in traces:
3584 if not (trace.meta
3585 and trace.meta.get('tabu', False)):
3587 if fft_filtering:
3588 but = pyrocko.response.ButterworthResponse
3589 multres = pyrocko.response.MultiplyResponse
3590 if self.lowpass is not None \
3591 or self.highpass is not None:
3593 it = num.arange(
3594 trace.data_len(), dtype=float)
3595 detr_data, m, b = detrend(
3596 it, trace.get_ydata())
3598 trace.set_ydata(detr_data)
3600 freqs, fdata = trace.spectrum(
3601 pad_to_pow2=True, tfade=None)
3603 nfreqs = fdata.size
3605 key = (trace.deltat, nfreqs)
3607 if key not in self.tf_cache:
3608 resps = []
3609 if self.lowpass is not None:
3610 resps.append(but(
3611 order=4,
3612 corner=self.lowpass,
3613 type='low'))
3615 if self.highpass is not None:
3616 resps.append(but(
3617 order=4,
3618 corner=self.highpass,
3619 type='high'))
3621 resp = multres(resps)
3622 self.tf_cache[key] = \
3623 resp.evaluate(freqs)
3625 filtered_data = num.fft.irfft(
3626 fdata*self.tf_cache[key]
3627 )[:trace.data_len()]
3629 retrended_data = retrend(
3630 it, filtered_data, m, b)
3632 trace.set_ydata(retrended_data)
3634 else:
3636 if ads and self.lowpass is not None:
3637 while trace.deltat \
3638 < min_deltat_wo_decimate:
3640 trace.downsample(2, demean=False)
3642 fmax = 0.5/trace.deltat
3643 if not lphp and (
3644 self.lowpass is not None
3645 and self.highpass is not None
3646 and self.lowpass < fmax
3647 and self.highpass < fmax
3648 and self.highpass < self.lowpass):
3650 trace.bandpass(
3651 2, self.highpass, self.lowpass)
3652 else:
3653 if self.lowpass is not None:
3654 if self.lowpass < 0.5/trace.deltat:
3655 trace.lowpass(
3656 4, self.lowpass,
3657 demean=False)
3659 if self.highpass is not None:
3660 if self.lowpass is None \
3661 or self.highpass \
3662 < self.lowpass:
3664 if self.highpass < \
3665 0.5/trace.deltat:
3666 trace.highpass(
3667 4, self.highpass,
3668 demean=False)
3670 processed_traces.append(trace)
3672 if self.rotate != 0.0:
3673 phi = self.rotate/180.*math.pi
3674 cphi = math.cos(phi)
3675 sphi = math.sin(phi)
3676 for a in processed_traces:
3677 for b in processed_traces:
3678 if (a.network == b.network
3679 and a.station == b.station
3680 and a.location == b.location
3681 and ((a.channel.lower().endswith('n')
3682 and b.channel.lower().endswith('e'))
3683 or (a.channel.endswith('1')
3684 and b.channel.endswith('2')))
3685 and abs(a.deltat-b.deltat) < a.deltat*0.001
3686 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3687 len(a.get_ydata()) == len(b.get_ydata())):
3689 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3690 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3691 a.set_ydata(aydata)
3692 b.set_ydata(bydata)
3694 processed_traces = self.post_process_hooks(processed_traces)
3696 self.cached_processed_traces = processed_traces
3697 self.cached_vec = vec
3699 chopped_traces = []
3700 for trace in processed_traces:
3701 chop_tmin = tmin_ - trace.deltat*4
3702 chop_tmax = tmax_ + trace.deltat*4
3704 try:
3705 ctrace = trace.chop(
3706 chop_tmin, chop_tmax,
3707 inplace=False)
3709 except pyrocko.trace.NoData:
3710 continue
3712 if ctrace.data_len() < 2:
3713 continue
3715 chopped_traces.append(ctrace)
3717 self.timer_cutout.stop()
3718 return chopped_traces
3720 def pre_process_hooks(self, traces):
3721 for snuffling in self.snufflings:
3722 if snuffling._pre_process_hook_enabled:
3723 traces = snuffling.pre_process_hook(traces)
3725 return traces
3727 def post_process_hooks(self, traces):
3728 for snuffling in self.snufflings:
3729 if snuffling._post_process_hook_enabled:
3730 traces = snuffling.post_process_hook(traces)
3732 return traces
3734 def visible_length_change(self, ignore=None):
3735 for menuitem, vlen in self.menuitems_visible_length:
3736 if menuitem.isChecked():
3737 self.visible_length = vlen
3739 def scaling_base_change(self, ignore=None):
3740 for menuitem, scaling_base in self.menuitems_scaling_base:
3741 if menuitem.isChecked():
3742 self.scaling_base = scaling_base
3744 def scalingmode_change(self, ignore=None):
3745 for menuitem, scaling_key in self.menuitems_scaling:
3746 if menuitem.isChecked():
3747 self.scaling_key = scaling_key
3748 self.update()
3750 def apply_scaling_hooks(self, data_ranges):
3751 for k in sorted(self.scaling_hooks.keys()):
3752 hook = self.scaling_hooks[k]
3753 hook(data_ranges)
3755 def viewmode_change(self, ignore=True):
3756 for item, mode in self.menuitems_viewmode:
3757 if item.isChecked():
3758 self.view_mode = mode
3759 break
3760 else:
3761 raise AttributeError('unknown view mode')
3763 items_waterfall_disabled = (
3764 self.menuitem_showscaleaxis,
3765 self.menuitem_showscalerange,
3766 self.menuitem_showzeroline,
3767 self.menuitem_colortraces,
3768 self.menuitem_cliptraces,
3769 *(itm[0] for itm in self.menuitems_visible_length)
3770 )
3772 if self.view_mode is ViewMode.Waterfall:
3773 self.parent().show_colorbar_ctrl(True)
3774 self.parent().show_gain_ctrl(False)
3776 for item in items_waterfall_disabled:
3777 item.setDisabled(True)
3779 self.visible_length = 180.
3780 else:
3781 self.parent().show_colorbar_ctrl(False)
3782 self.parent().show_gain_ctrl(True)
3784 for item in items_waterfall_disabled:
3785 item.setDisabled(False)
3787 self.visible_length_change()
3788 self.update()
3790 def set_scaling_hook(self, k, hook):
3791 self.scaling_hooks[k] = hook
3793 def remove_scaling_hook(self, k):
3794 del self.scaling_hooks[k]
3796 def remove_scaling_hooks(self):
3797 self.scaling_hooks = {}
3799 def s_sortingmode_change(self, ignore=None):
3800 for menuitem, valfunc in self.menuitems_ssorting:
3801 if menuitem.isChecked():
3802 self._ssort = valfunc
3804 self.sortingmode_change()
3806 def sortingmode_change(self, ignore=None):
3807 for menuitem, (gather, color) in self.menuitems_sorting:
3808 if menuitem.isChecked():
3809 self.set_gathering(gather, color)
3811 self.sortingmode_change_time = time.time()
3813 def lowpass_change(self, value, ignore=None):
3814 self.lowpass = value
3815 self.passband_check()
3816 self.tf_cache = {}
3817 self.update()
3819 def highpass_change(self, value, ignore=None):
3820 self.highpass = value
3821 self.passband_check()
3822 self.tf_cache = {}
3823 self.update()
3825 def passband_check(self):
3826 if self.highpass and self.lowpass \
3827 and self.highpass >= self.lowpass:
3829 self.message = 'Corner frequency of highpass larger than ' \
3830 'corner frequency of lowpass! I will now ' \
3831 'deactivate the highpass.'
3833 self.update_status()
3834 else:
3835 oldmess = self.message
3836 self.message = None
3837 if oldmess is not None:
3838 self.update_status()
3840 def gain_change(self, value, ignore):
3841 self.gain = value
3842 self.update()
3844 def rot_change(self, value, ignore):
3845 self.rotate = value
3846 self.update()
3848 def waterfall_cmap_change(self, cmap):
3849 self.waterfall_cmap = cmap
3850 self.update()
3852 def waterfall_clip_change(self, clip_min, clip_max):
3853 self.waterfall_clip_min = clip_min
3854 self.waterfall_clip_max = clip_max
3855 self.update()
3857 def waterfall_show_absolute_change(self, toggle):
3858 self.waterfall_show_absolute = toggle
3859 self.update()
3861 def waterfall_set_integrate(self, toggle):
3862 self.waterfall_integrate = toggle
3863 self.update()
3865 def set_selected_markers(self, markers):
3866 '''
3867 Set a list of markers selected
3869 :param markers: list of markers
3870 '''
3871 self.deselect_all()
3872 for m in markers:
3873 m.selected = True
3875 self.update()
3877 def deselect_all(self):
3878 for marker in self.markers:
3879 marker.selected = False
3881 def animate_picking(self):
3882 point = self.mapFromGlobal(qg.QCursor.pos())
3883 self.update_picking(point.x(), point.y(), doshift=True)
3885 def get_nslc_ids_for_track(self, ftrack):
3886 itrack = int(ftrack)
3887 return self.track_to_nslc_ids.get(itrack, [])
3889 def stop_picking(self, x, y, abort=False):
3890 if self.picking:
3891 self.update_picking(x, y, doshift=False)
3892 self.picking = None
3893 self.picking_down = None
3894 self.picking_timer.stop()
3895 self.picking_timer = None
3896 if not abort:
3897 self.add_marker(self.floating_marker)
3898 self.floating_marker.selected = True
3899 self.emit_selected_markers()
3901 self.floating_marker = None
3903 def start_picking(self, ignore):
3905 if not self.picking:
3906 self.deselect_all()
3907 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3908 point = self.mapFromGlobal(qg.QCursor.pos())
3910 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3911 self.picking.setGeometry(
3912 gpoint.x(), gpoint.y(), 1, self.height())
3913 t = self.time_projection.rev(point.x())
3915 ftrack = self.track_to_screen.rev(point.y())
3916 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3917 self.floating_marker = Marker(nslc_ids, t, t)
3918 self.floating_marker.selected = True
3920 self.picking_timer = qc.QTimer()
3921 self.picking_timer.timeout.connect(
3922 self.animate_picking)
3924 self.picking_timer.setInterval(50)
3925 self.picking_timer.start()
3927 def update_picking(self, x, y, doshift=False):
3928 if self.picking:
3929 mouset = self.time_projection.rev(x)
3930 dt = 0.0
3931 if mouset < self.tmin or mouset > self.tmax:
3932 if mouset < self.tmin:
3933 dt = -(self.tmin - mouset)
3934 else:
3935 dt = mouset - self.tmax
3936 ddt = self.tmax-self.tmin
3937 dt = max(dt, -ddt/10.)
3938 dt = min(dt, ddt/10.)
3940 x0 = x
3941 if self.picking_down is not None:
3942 x0 = self.time_projection(self.picking_down[0])
3944 w = abs(x-x0)
3945 x0 = min(x0, x)
3947 tmin, tmax = (
3948 self.time_projection.rev(x0),
3949 self.time_projection.rev(x0+w))
3951 tmin, tmax = (
3952 max(working_system_time_range[0], tmin),
3953 min(working_system_time_range[1], tmax))
3955 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3957 self.picking.setGeometry(
3958 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3960 ftrack = self.track_to_screen.rev(y)
3961 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3962 self.floating_marker.set(nslc_ids, tmin, tmax)
3964 if dt != 0.0 and doshift:
3965 self.interrupt_following()
3966 self.set_time_range(self.tmin+dt, self.tmax+dt)
3968 self.update()
3970 def update_status(self):
3972 if self.message is None:
3973 point = self.mapFromGlobal(qg.QCursor.pos())
3975 mouse_t = self.time_projection.rev(point.x())
3976 if not is_working_time(mouse_t):
3977 return
3979 if self.floating_marker:
3980 tmi, tma = (
3981 self.floating_marker.tmin,
3982 self.floating_marker.tmax)
3984 tt, ms = gmtime_x(tmi)
3986 if tmi == tma:
3987 message = mystrftime(
3988 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
3989 tt=tt, milliseconds=ms)
3990 else:
3991 srange = '%g s' % (tma-tmi)
3992 message = mystrftime(
3993 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
3994 tt=tt, milliseconds=ms)
3995 else:
3996 tt, ms = gmtime_x(mouse_t)
3998 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
3999 else:
4000 message = self.message
4002 sb = self.window().statusBar()
4003 sb.clearMessage()
4004 sb.showMessage(message)
4006 def set_sortingmode_change_delay_time(self, dt):
4007 self.sortingmode_change_delay_time = dt
4009 def sortingmode_change_delayed(self):
4010 now = time.time()
4011 return (
4012 self.sortingmode_change_delay_time is not None
4013 and now - self.sortingmode_change_time
4014 < self.sortingmode_change_delay_time)
4016 def set_visible_marker_kinds(self, kinds):
4017 self.deselect_all()
4018 self.visible_marker_kinds = tuple(kinds)
4019 self.emit_selected_markers()
4021 def following(self):
4022 return self.follow_timer is not None \
4023 and not self.following_interrupted()
4025 def interrupt_following(self):
4026 self.interactive_range_change_time = time.time()
4028 def following_interrupted(self, now=None):
4029 if now is None:
4030 now = time.time()
4031 return now - self.interactive_range_change_time \
4032 < self.interactive_range_change_delay_time
4034 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4035 if tmax_start is None:
4036 tmax_start = time.time()
4037 self.show_all = False
4038 self.follow_time = tlen
4039 self.follow_timer = qc.QTimer(self)
4040 self.follow_timer.timeout.connect(
4041 self.follow_update)
4042 self.follow_timer.setInterval(interval)
4043 self.follow_timer.start()
4044 self.follow_started = time.time()
4045 self.follow_lapse = lapse
4046 self.follow_tshift = self.follow_started - tmax_start
4047 self.interactive_range_change_time = 0.0
4049 def unfollow(self):
4050 if self.follow_timer is not None:
4051 self.follow_timer.stop()
4052 self.follow_timer = None
4053 self.interactive_range_change_time = 0.0
4055 def follow_update(self):
4056 rnow = time.time()
4057 if self.follow_lapse is None:
4058 now = rnow
4059 else:
4060 now = self.follow_started + (rnow - self.follow_started) \
4061 * self.follow_lapse
4063 if self.following_interrupted(rnow):
4064 return
4065 self.set_time_range(
4066 now-self.follow_time-self.follow_tshift,
4067 now-self.follow_tshift)
4069 self.update()
4071 def myclose(self, return_tag=''):
4072 self.return_tag = return_tag
4073 self.window().close()
4075 def cleanup(self):
4076 self.about_to_close.emit()
4077 self.timer.stop()
4078 if self.follow_timer is not None:
4079 self.follow_timer.stop()
4081 for snuffling in list(self.snufflings):
4082 self.remove_snuffling(snuffling)
4084 def set_error_message(self, key, value):
4085 if value is None:
4086 if key in self.error_messages:
4087 del self.error_messages[key]
4088 else:
4089 self.error_messages[key] = value
4091 def inputline_changed(self, text):
4092 pass
4094 def inputline_finished(self, text):
4095 line = str(text)
4097 toks = line.split()
4098 clearit, hideit, error = False, True, None
4099 if len(toks) >= 1:
4100 command = toks[0].lower()
4102 try:
4103 quick_filter_commands = {
4104 'n': '%s.*.*.*',
4105 's': '*.%s.*.*',
4106 'l': '*.*.%s.*',
4107 'c': '*.*.*.%s'}
4109 if command in quick_filter_commands:
4110 if len(toks) >= 2:
4111 patterns = [
4112 quick_filter_commands[toks[0]] % pat
4113 for pat in toks[1:]]
4114 self.set_quick_filter_patterns(patterns, line)
4115 else:
4116 self.set_quick_filter_patterns(None)
4118 self.update()
4120 elif command in ('hide', 'unhide'):
4121 if len(toks) >= 2:
4122 patterns = []
4123 if len(toks) == 2:
4124 patterns = [toks[1]]
4125 elif len(toks) >= 3:
4126 x = {
4127 'n': '%s.*.*.*',
4128 's': '*.%s.*.*',
4129 'l': '*.*.%s.*',
4130 'c': '*.*.*.%s'}
4132 if toks[1] in x:
4133 patterns.extend(
4134 x[toks[1]] % tok for tok in toks[2:])
4136 for pattern in patterns:
4137 if command == 'hide':
4138 self.add_blacklist_pattern(pattern)
4139 else:
4140 self.remove_blacklist_pattern(pattern)
4142 elif command == 'unhide' and len(toks) == 1:
4143 self.clear_blacklist()
4145 clearit = True
4147 self.update()
4149 elif command == 'markers':
4150 if len(toks) == 2:
4151 if toks[1] == 'all':
4152 kinds = self.all_marker_kinds
4153 else:
4154 kinds = []
4155 for x in toks[1]:
4156 try:
4157 kinds.append(int(x))
4158 except Exception:
4159 pass
4161 self.set_visible_marker_kinds(kinds)
4163 elif len(toks) == 1:
4164 self.set_visible_marker_kinds(())
4166 self.update()
4168 elif command == 'scaling':
4169 if len(toks) == 2:
4170 hideit = False
4171 error = 'wrong number of arguments'
4173 if len(toks) >= 3:
4174 vmin, vmax = [
4175 pyrocko.model.float_or_none(x)
4176 for x in toks[-2:]]
4178 def upd(d, k, vmin, vmax):
4179 if k in d:
4180 if vmin is not None:
4181 d[k] = vmin, d[k][1]
4182 if vmax is not None:
4183 d[k] = d[k][0], vmax
4185 if len(toks) == 1:
4186 self.remove_scaling_hooks()
4188 elif len(toks) == 3:
4189 def hook(data_ranges):
4190 for k in data_ranges:
4191 upd(data_ranges, k, vmin, vmax)
4193 self.set_scaling_hook('_', hook)
4195 elif len(toks) == 4:
4196 pattern = toks[1]
4198 def hook(data_ranges):
4199 for k in pyrocko.util.match_nslcs(
4200 pattern, list(data_ranges.keys())):
4202 upd(data_ranges, k, vmin, vmax)
4204 self.set_scaling_hook(pattern, hook)
4206 elif command == 'goto':
4207 toks2 = line.split(None, 1)
4208 if len(toks2) == 2:
4209 arg = toks2[1]
4210 m = re.match(
4211 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4212 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4213 if m:
4214 tlen = None
4215 if not m.group(1):
4216 tlen = 12*32*24*60*60
4217 elif not m.group(2):
4218 tlen = 32*24*60*60
4219 elif not m.group(3):
4220 tlen = 24*60*60
4221 elif not m.group(4):
4222 tlen = 60*60
4223 elif not m.group(5):
4224 tlen = 60
4226 supl = '1970-01-01 00:00:00'
4227 if len(supl) > len(arg):
4228 arg = arg + supl[-(len(supl)-len(arg)):]
4229 t = pyrocko.util.str_to_time(arg)
4230 self.go_to_time(t, tlen=tlen)
4232 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4233 supl = '00:00:00'
4234 if len(supl) > len(arg):
4235 arg = arg + supl[-(len(supl)-len(arg)):]
4236 tmin, tmax = self.get_time_range()
4237 sdate = pyrocko.util.time_to_str(
4238 tmin/2.+tmax/2., format='%Y-%m-%d')
4239 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4240 self.go_to_time(t)
4242 elif arg == 'today':
4243 self.go_to_time(
4244 day_start(
4245 time.time()), tlen=24*60*60)
4247 elif arg == 'yesterday':
4248 self.go_to_time(
4249 day_start(
4250 time.time()-24*60*60), tlen=24*60*60)
4252 else:
4253 self.go_to_event_by_name(arg)
4255 else:
4256 raise PileViewerMainException(
4257 'No such command: %s' % command)
4259 except PileViewerMainException as e:
4260 error = str(e)
4261 hideit = False
4263 return clearit, hideit, error
4265 return PileViewerMain
4268PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4269GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4272class LineEditWithAbort(qw.QLineEdit):
4274 aborted = qc.pyqtSignal()
4275 history_down = qc.pyqtSignal()
4276 history_up = qc.pyqtSignal()
4278 def keyPressEvent(self, key_event):
4279 if key_event.key() == qc.Qt.Key_Escape:
4280 self.aborted.emit()
4281 elif key_event.key() == qc.Qt.Key_Down:
4282 self.history_down.emit()
4283 elif key_event.key() == qc.Qt.Key_Up:
4284 self.history_up.emit()
4285 else:
4286 return qw.QLineEdit.keyPressEvent(self, key_event)
4289class PileViewer(qw.QFrame):
4290 '''
4291 PileViewerMain + Controls + Inputline
4292 '''
4294 def __init__(
4295 self, pile,
4296 ntracks_shown_max=20,
4297 marker_editor_sortable=True,
4298 use_opengl=None,
4299 panel_parent=None,
4300 *args):
4302 qw.QFrame.__init__(self, *args)
4304 layout = qw.QGridLayout()
4305 layout.setContentsMargins(0, 0, 0, 0)
4306 layout.setSpacing(0)
4308 self.menu = PileViewerMenuBar(self)
4310 if use_opengl is None:
4311 use_opengl = is_macos
4313 if use_opengl:
4314 self.viewer = GLPileViewerMain(
4315 pile,
4316 ntracks_shown_max=ntracks_shown_max,
4317 panel_parent=panel_parent,
4318 menu=self.menu)
4319 else:
4320 self.viewer = PileViewerMain(
4321 pile,
4322 ntracks_shown_max=ntracks_shown_max,
4323 panel_parent=panel_parent,
4324 menu=self.menu)
4326 self.marker_editor_sortable = marker_editor_sortable
4328 self.setFrameShape(qw.QFrame.StyledPanel)
4329 self.setFrameShadow(qw.QFrame.Sunken)
4331 self.input_area = qw.QFrame(self)
4332 ia_layout = qw.QGridLayout()
4333 ia_layout.setContentsMargins(11, 11, 11, 11)
4334 self.input_area.setLayout(ia_layout)
4336 self.inputline = LineEditWithAbort(self.input_area)
4337 self.inputline.returnPressed.connect(
4338 self.inputline_returnpressed)
4339 self.inputline.editingFinished.connect(
4340 self.inputline_finished)
4341 self.inputline.aborted.connect(
4342 self.inputline_aborted)
4344 self.inputline.history_down.connect(
4345 lambda: self.step_through_history(1))
4346 self.inputline.history_up.connect(
4347 lambda: self.step_through_history(-1))
4349 self.inputline.textEdited.connect(
4350 self.inputline_changed)
4352 self.inputline.setPlaceholderText(
4353 u'Quick commands: e.g. \'c HH?\' to select channels. '
4354 u'Use ↑ or ↓ to navigate.')
4355 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4356 self.input_area.hide()
4357 self.history = None
4359 self.inputline_error_str = None
4361 self.inputline_error = qw.QLabel()
4362 self.inputline_error.hide()
4364 ia_layout.addWidget(self.inputline, 0, 0)
4365 ia_layout.addWidget(self.inputline_error, 1, 0)
4366 layout.addWidget(self.input_area, 0, 0, 1, 2)
4367 layout.addWidget(self.viewer, 1, 0)
4369 pb = Progressbars(self)
4370 layout.addWidget(pb, 2, 0, 1, 2)
4371 self.progressbars = pb
4373 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4374 self.scrollbar = scrollbar
4375 layout.addWidget(scrollbar, 1, 1)
4376 self.scrollbar.valueChanged.connect(
4377 self.scrollbar_changed)
4379 self.block_scrollbar_changes = False
4381 self.viewer.want_input.connect(
4382 self.inputline_show)
4383 self.viewer.tracks_range_changed.connect(
4384 self.tracks_range_changed)
4385 self.viewer.pile_has_changed_signal.connect(
4386 self.adjust_controls)
4387 self.viewer.about_to_close.connect(
4388 self.save_inputline_history)
4390 self.setLayout(layout)
4392 def cleanup(self):
4393 self.viewer.cleanup()
4395 def get_progressbars(self):
4396 return self.progressbars
4398 def inputline_show(self):
4399 if not self.history:
4400 self.load_inputline_history()
4402 self.input_area.show()
4403 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4404 self.inputline.selectAll()
4406 def inputline_set_error(self, string):
4407 self.inputline_error_str = string
4408 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4409 self.inputline.selectAll()
4410 self.inputline_error.setText(string)
4411 self.input_area.show()
4412 self.inputline_error.show()
4414 def inputline_clear_error(self):
4415 if self.inputline_error_str:
4416 self.inputline.setPalette(qw.QApplication.palette())
4417 self.inputline_error_str = None
4418 self.inputline_error.clear()
4419 self.inputline_error.hide()
4421 def inputline_changed(self, line):
4422 self.viewer.inputline_changed(str(line))
4423 self.inputline_clear_error()
4425 def inputline_returnpressed(self):
4426 line = str(self.inputline.text())
4427 clearit, hideit, error = self.viewer.inputline_finished(line)
4429 if error:
4430 self.inputline_set_error(error)
4432 line = line.strip()
4434 if line != '' and not error:
4435 if not (len(self.history) >= 1 and line == self.history[-1]):
4436 self.history.append(line)
4438 if clearit:
4440 self.inputline.blockSignals(True)
4441 qpat, qinp = self.viewer.get_quick_filter_patterns()
4442 if qpat is None:
4443 self.inputline.clear()
4444 else:
4445 self.inputline.setText(qinp)
4446 self.inputline.blockSignals(False)
4448 if hideit and not error:
4449 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4450 self.input_area.hide()
4452 self.hist_ind = len(self.history)
4454 def inputline_aborted(self):
4455 '''
4456 Hide the input line.
4457 '''
4458 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4459 self.hist_ind = len(self.history)
4460 self.input_area.hide()
4462 def save_inputline_history(self):
4463 '''
4464 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4465 '''
4466 if not self.history:
4467 return
4469 conf = pyrocko.config
4470 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4471 with open(fn_hist, 'w') as f:
4472 i = min(100, len(self.history))
4473 for c in self.history[-i:]:
4474 f.write('%s\n' % c)
4476 def load_inputline_history(self):
4477 '''
4478 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4479 '''
4480 conf = pyrocko.config
4481 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4482 if not os.path.exists(fn_hist):
4483 with open(fn_hist, 'w+') as f:
4484 f.write('\n')
4486 with open(fn_hist, 'r') as f:
4487 self.history = [line.strip() for line in f.readlines()]
4489 self.hist_ind = len(self.history)
4491 def step_through_history(self, ud=1):
4492 '''
4493 Step through input line history and set the input line text.
4494 '''
4495 n = len(self.history)
4496 self.hist_ind += ud
4497 self.hist_ind %= (n + 1)
4498 if len(self.history) != 0 and self.hist_ind != n:
4499 self.inputline.setText(self.history[self.hist_ind])
4500 else:
4501 self.inputline.setText('')
4503 def inputline_finished(self):
4504 pass
4506 def tracks_range_changed(self, ntracks, ilo, ihi):
4507 if self.block_scrollbar_changes:
4508 return
4510 self.scrollbar.blockSignals(True)
4511 self.scrollbar.setPageStep(ihi-ilo)
4512 vmax = max(0, ntracks-(ihi-ilo))
4513 self.scrollbar.setRange(0, vmax)
4514 self.scrollbar.setValue(ilo)
4515 self.scrollbar.setHidden(vmax == 0)
4516 self.scrollbar.blockSignals(False)
4518 def scrollbar_changed(self, value):
4519 self.block_scrollbar_changes = True
4520 ilo = value
4521 ihi = ilo + self.scrollbar.pageStep()
4522 self.viewer.set_tracks_range((ilo, ihi))
4523 self.block_scrollbar_changes = False
4524 self.update_contents()
4526 def controls(self):
4527 frame = qw.QFrame(self)
4528 layout = qw.QGridLayout()
4529 frame.setLayout(layout)
4531 minfreq = 0.001
4532 maxfreq = 1000.0
4533 self.lowpass_control = ValControl(high_is_none=True)
4534 self.lowpass_control.setup(
4535 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4536 self.highpass_control = ValControl(low_is_none=True)
4537 self.highpass_control.setup(
4538 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4539 self.gain_control = ValControl()
4540 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4541 self.rot_control = LinValControl()
4542 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4543 self.colorbar_control = ColorbarControl(self)
4545 self.lowpass_control.valchange.connect(
4546 self.viewer.lowpass_change)
4547 self.highpass_control.valchange.connect(
4548 self.viewer.highpass_change)
4549 self.gain_control.valchange.connect(
4550 self.viewer.gain_change)
4551 self.rot_control.valchange.connect(
4552 self.viewer.rot_change)
4553 self.colorbar_control.cmap_changed.connect(
4554 self.viewer.waterfall_cmap_change
4555 )
4556 self.colorbar_control.clip_changed.connect(
4557 self.viewer.waterfall_clip_change
4558 )
4559 self.colorbar_control.show_absolute_toggled.connect(
4560 self.viewer.waterfall_show_absolute_change
4561 )
4562 self.colorbar_control.show_integrate_toggled.connect(
4563 self.viewer.waterfall_set_integrate
4564 )
4566 for icontrol, control in enumerate((
4567 self.highpass_control,
4568 self.lowpass_control,
4569 self.gain_control,
4570 self.rot_control,
4571 self.colorbar_control)):
4573 for iwidget, widget in enumerate(control.widgets()):
4574 layout.addWidget(widget, icontrol, iwidget)
4576 spacer = qw.QSpacerItem(
4577 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4578 layout.addItem(spacer, 4, 0, 1, 3)
4580 self.adjust_controls()
4581 self.viewer.viewmode_change(ViewMode.Wiggle)
4582 return frame
4584 def marker_editor(self):
4585 editor = pyrocko.gui.marker_editor.MarkerEditor(
4586 self, sortable=self.marker_editor_sortable)
4588 editor.set_viewer(self.get_view())
4589 editor.get_marker_model().dataChanged.connect(
4590 self.update_contents)
4591 return editor
4593 def adjust_controls(self):
4594 dtmin, dtmax = self.viewer.content_deltat_range()
4595 maxfreq = 0.5/dtmin
4596 minfreq = (0.5/dtmax)*0.001
4597 self.lowpass_control.set_range(minfreq, maxfreq)
4598 self.highpass_control.set_range(minfreq, maxfreq)
4600 def setup_snufflings(self):
4601 self.viewer.setup_snufflings()
4603 def get_view(self):
4604 return self.viewer
4606 def update_contents(self):
4607 self.viewer.update()
4609 def get_pile(self):
4610 return self.viewer.get_pile()
4612 def show_colorbar_ctrl(self, show):
4613 for w in self.colorbar_control.widgets():
4614 w.setVisible(show)
4616 def show_gain_ctrl(self, show):
4617 for w in self.gain_control.widgets():
4618 w.setVisible(show)