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)
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', 'minmax'),
932 ('Scaling based on Mean ± 2x Std. Deviation', 2),
933 ('Scaling based on Mean ± 4x Std. Deviation', 4),
934 ]
936 self.menuitems_scaling_base = add_radiobuttongroup(
937 scale_menu, menudef, self.scaling_base_change)
939 self.scaling_base = self.menuitems_scaling_base[0][1]
940 scale_menu.addSeparator()
942 self.menuitem_fixscalerange = scale_menu.addAction(
943 'Fix Scale Ranges')
944 self.menuitem_fixscalerange.setCheckable(True)
946 # Sort Menu
947 def sector_dist(sta):
948 if sta.dist_m is None:
949 return None, None
950 else:
951 return (
952 sector_int(round((sta.azimuth+15.)/30.)),
953 m_float(sta.dist_m))
955 menudef = [
956 ('Sort by Names',
957 lambda tr: (),
958 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)),
959 ('Sort by Distance',
960 lambda tr: self.station_attrib(
961 tr,
962 lambda sta: (m_float_or_none(sta.dist_m),),
963 lambda tr: (None,)),
964 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)),
965 ('Sort by Azimuth',
966 lambda tr: self.station_attrib(
967 tr,
968 lambda sta: (deg_float_or_none(sta.azimuth),),
969 lambda tr: (None,))),
970 ('Sort by Distance in 12 Azimuthal Blocks',
971 lambda tr: self.station_attrib(
972 tr,
973 sector_dist,
974 lambda tr: (None, None))),
975 ('Sort by Backazimuth',
976 lambda tr: self.station_attrib(
977 tr,
978 lambda sta: (deg_float_or_none(sta.backazimuth),),
979 lambda tr: (None,))),
980 ]
981 self.menuitems_ssorting = add_radiobuttongroup(
982 sort_menu, menudef, self.s_sortingmode_change)
983 sort_menu.addSeparator()
985 self._ssort = lambda tr: ()
987 self.menu.addSeparator()
989 menudef = [
990 ('Subsort by Network, Station, Location, Channel',
991 ((0, 1, 2, 3), # gathering
992 lambda tr: tr.location)), # coloring
993 ('Subsort by Network, Station, Channel, Location',
994 ((0, 1, 3, 2),
995 lambda tr: tr.channel)),
996 ('Subsort by Station, Network, Channel, Location',
997 ((1, 0, 3, 2),
998 lambda tr: tr.channel)),
999 ('Subsort by Location, Network, Station, Channel',
1000 ((2, 0, 1, 3),
1001 lambda tr: tr.channel)),
1002 ('Subsort by Channel, Network, Station, Location',
1003 ((3, 0, 1, 2),
1004 lambda tr: (tr.network, tr.station, tr.location))),
1005 ('Subsort by Network, Station, Channel (Grouped by Location)',
1006 ((0, 1, 3),
1007 lambda tr: tr.location)),
1008 ('Subsort by Station, Network, Channel (Grouped by Location)',
1009 ((1, 0, 3),
1010 lambda tr: tr.location)),
1011 ]
1013 self.menuitems_sorting = add_radiobuttongroup(
1014 sort_menu, menudef, self.sortingmode_change)
1016 menudef = [(x.key, x.value) for x in
1017 self.config.visible_length_setting]
1019 # View menu
1020 self.menuitems_visible_length = add_radiobuttongroup(
1021 view_menu, menudef,
1022 self.visible_length_change)
1023 view_menu.addSeparator()
1025 view_modes = [
1026 ('Wiggle Plot', ViewMode.Wiggle),
1027 ('Waterfall', ViewMode.Waterfall)
1028 ]
1030 self.menuitems_viewmode = add_radiobuttongroup(
1031 view_menu, view_modes,
1032 self.viewmode_change, default=ViewMode.Wiggle)
1033 view_menu.addSeparator()
1035 self.menuitem_cliptraces = view_menu.addAction(
1036 'Clip Traces')
1037 self.menuitem_cliptraces.setCheckable(True)
1038 self.menuitem_cliptraces.setChecked(self.config.clip_traces)
1040 self.menuitem_showboxes = view_menu.addAction(
1041 'Show Boxes')
1042 self.menuitem_showboxes.setCheckable(True)
1043 self.menuitem_showboxes.setChecked(
1044 self.config.show_boxes)
1046 self.menuitem_colortraces = view_menu.addAction(
1047 'Color Traces')
1048 self.menuitem_colortraces.setCheckable(True)
1049 self.menuitem_antialias = view_menu.addAction(
1050 'Antialiasing')
1051 self.menuitem_antialias.setCheckable(True)
1053 view_menu.addSeparator()
1054 self.menuitem_showscalerange = view_menu.addAction(
1055 'Show Scale Ranges')
1056 self.menuitem_showscalerange.setCheckable(True)
1057 self.menuitem_showscalerange.setChecked(
1058 self.config.show_scale_ranges)
1060 self.menuitem_showscaleaxis = view_menu.addAction(
1061 'Show Scale Axes')
1062 self.menuitem_showscaleaxis.setCheckable(True)
1063 self.menuitem_showscaleaxis.setChecked(
1064 self.config.show_scale_axes)
1066 self.menuitem_showzeroline = view_menu.addAction(
1067 'Show Zero Lines')
1068 self.menuitem_showzeroline.setCheckable(True)
1070 view_menu.addSeparator()
1071 view_menu.addAction(
1072 qg.QIcon.fromTheme('view-fullscreen'),
1073 'Fullscreen',
1074 self.toggle_fullscreen,
1075 qg.QKeySequence(qc.Qt.Key_F11))
1077 # Options Menu
1078 self.menuitem_demean = options_menu.addAction('Demean')
1079 self.menuitem_demean.setCheckable(True)
1080 self.menuitem_demean.setChecked(self.config.demean)
1081 self.menuitem_demean.setShortcut(
1082 qg.QKeySequence(qc.Qt.Key_Underscore))
1084 self.menuitem_distances_3d = options_menu.addAction(
1085 '3D distances',
1086 self.distances_3d_changed)
1087 self.menuitem_distances_3d.setCheckable(True)
1089 self.menuitem_allowdownsampling = options_menu.addAction(
1090 'Allow Downsampling')
1091 self.menuitem_allowdownsampling.setCheckable(True)
1092 self.menuitem_allowdownsampling.setChecked(True)
1094 self.menuitem_degap = options_menu.addAction(
1095 'Allow Degapping')
1096 self.menuitem_degap.setCheckable(True)
1097 self.menuitem_degap.setChecked(True)
1099 options_menu.addSeparator()
1101 self.menuitem_fft_filtering = options_menu.addAction(
1102 'FFT Filtering')
1103 self.menuitem_fft_filtering.setCheckable(True)
1105 self.menuitem_lphp = options_menu.addAction(
1106 'Bandpass is Low- + Highpass')
1107 self.menuitem_lphp.setCheckable(True)
1108 self.menuitem_lphp.setChecked(True)
1110 options_menu.addSeparator()
1111 self.menuitem_watch = options_menu.addAction(
1112 'Watch Files')
1113 self.menuitem_watch.setCheckable(True)
1115 self.menuitem_liberal_fetch = options_menu.addAction(
1116 'Liberal Fetch Optimization')
1117 self.menuitem_liberal_fetch.setCheckable(True)
1119 self.visible_length = menudef[0][1]
1121 self.snufflings_menu.addAction(
1122 'Reload Snufflings',
1123 self.setup_snufflings)
1125 # Disable ShadowPileTest
1126 if False:
1127 test_action = self.menu.addAction(
1128 'Test',
1129 self.toggletest)
1130 test_action.setCheckable(True)
1132 help_menu.addAction(
1133 qg.QIcon.fromTheme('preferences-desktop-keyboard'),
1134 'Snuffler Controls',
1135 self.help,
1136 qg.QKeySequence(qc.Qt.Key_Question))
1138 help_menu.addAction(
1139 'About',
1140 self.about)
1142 self.time_projection = Projection()
1143 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
1144 self.time_projection.set_out_range(0., self.width())
1146 self.gather = None
1148 self.trace_filter = None
1149 self.quick_filter = None
1150 self.quick_filter_patterns = None, None
1151 self.blacklist = []
1153 self.track_to_screen = Projection()
1154 self.track_to_nslc_ids = {}
1156 self.cached_vec = None
1157 self.cached_processed_traces = None
1159 self.timer = qc.QTimer(self)
1160 self.timer.timeout.connect(self.periodical)
1161 self.timer.setInterval(1000)
1162 self.timer.start()
1163 self.pile.add_listener(self)
1164 self.trace_styles = {}
1165 if self.get_squirrel() is None:
1166 self.determine_box_styles()
1168 self.setMouseTracking(True)
1170 user_home_dir = os.path.expanduser('~')
1171 self.snuffling_modules = {}
1172 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
1173 self.default_snufflings = None
1174 self.snufflings = []
1176 self.stations = {}
1178 self.timer_draw = Timer()
1179 self.timer_cutout = Timer()
1180 self.time_spent_painting = 0.0
1181 self.time_last_painted = time.time()
1183 self.interactive_range_change_time = 0.0
1184 self.interactive_range_change_delay_time = 10.0
1185 self.follow_timer = None
1187 self.sortingmode_change_time = 0.0
1188 self.sortingmode_change_delay_time = None
1190 self.old_data_ranges = {}
1192 self.error_messages = {}
1193 self.return_tag = None
1194 self.wheel_pos = 60
1196 self.setAcceptDrops(True)
1197 self._paths_to_load = []
1199 self.tf_cache = {}
1201 self.waterfall = TraceWaterfall()
1202 self.waterfall_cmap = 'viridis'
1203 self.waterfall_clip_min = 0.
1204 self.waterfall_clip_max = 1.
1205 self.waterfall_show_absolute = False
1206 self.waterfall_integrate = False
1207 self.view_mode = ViewMode.Wiggle
1209 self.automatic_updates = True
1211 self.closing = False
1212 self.in_paint_event = False
1214 def fail(self, reason):
1215 box = qw.QMessageBox(self)
1216 box.setText(reason)
1217 box.exec_()
1219 def set_trace_filter(self, filter_func):
1220 self.trace_filter = filter_func
1221 self.sortingmode_change()
1223 def update_trace_filter(self):
1224 if self.blacklist:
1226 def blacklist_func(tr):
1227 return not pyrocko.util.match_nslc(
1228 self.blacklist, tr.nslc_id)
1230 else:
1231 blacklist_func = None
1233 if self.quick_filter is None and blacklist_func is None:
1234 self.set_trace_filter(None)
1235 elif self.quick_filter is None:
1236 self.set_trace_filter(blacklist_func)
1237 elif blacklist_func is None:
1238 self.set_trace_filter(self.quick_filter)
1239 else:
1240 self.set_trace_filter(
1241 lambda tr: blacklist_func(tr) and self.quick_filter(tr))
1243 def set_quick_filter(self, filter_func):
1244 self.quick_filter = filter_func
1245 self.update_trace_filter()
1247 def set_quick_filter_patterns(self, patterns, inputline=None):
1248 if patterns is not None:
1249 self.set_quick_filter(
1250 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
1251 else:
1252 self.set_quick_filter(None)
1254 self.quick_filter_patterns = patterns, inputline
1256 def get_quick_filter_patterns(self):
1257 return self.quick_filter_patterns
1259 def add_blacklist_pattern(self, pattern):
1260 if pattern == 'empty':
1261 keys = set(self.pile.nslc_ids)
1262 trs = self.pile.all(
1263 tmin=self.tmin,
1264 tmax=self.tmax,
1265 load_data=False,
1266 degap=False)
1268 for tr in trs:
1269 if tr.nslc_id in keys:
1270 keys.remove(tr.nslc_id)
1272 for key in keys:
1273 xpattern = '.'.join(key)
1274 if xpattern not in self.blacklist:
1275 self.blacklist.append(xpattern)
1277 else:
1278 if pattern in self.blacklist:
1279 self.blacklist.remove(pattern)
1281 self.blacklist.append(pattern)
1283 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1284 self.update_trace_filter()
1286 def remove_blacklist_pattern(self, pattern):
1287 if pattern in self.blacklist:
1288 self.blacklist.remove(pattern)
1289 else:
1290 raise PileViewerMainException(
1291 'Pattern not found in blacklist.')
1293 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
1294 self.update_trace_filter()
1296 def clear_blacklist(self):
1297 self.blacklist = []
1298 self.update_trace_filter()
1300 def ssort(self, tr):
1301 return self._ssort(tr)
1303 def station_key(self, x):
1304 return x.network, x.station
1306 def station_keys(self, x):
1307 return [
1308 (x.network, x.station, x.location),
1309 (x.network, x.station)]
1311 def station_attrib(self, tr, getter, default_getter):
1312 for sk in self.station_keys(tr):
1313 if sk in self.stations:
1314 station = self.stations[sk]
1315 return getter(station)
1317 return default_getter(tr)
1319 def get_station(self, sk):
1320 return self.stations[sk]
1322 def has_station(self, station):
1323 for sk in self.station_keys(station):
1324 if sk in self.stations:
1325 return True
1327 return False
1329 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
1330 return self.station_attrib(
1331 tr, lambda sta: (sta.lat, sta.lon), default_getter)
1333 def set_stations(self, stations):
1334 self.stations = {}
1335 self.add_stations(stations)
1337 def add_stations(self, stations):
1338 for station in stations:
1339 for sk in self.station_keys(station):
1340 self.stations[sk] = station
1342 ev = self.get_active_event()
1343 if ev:
1344 self.set_origin(ev)
1346 def add_event(self, event):
1347 marker = EventMarker(event)
1348 self.add_marker(marker)
1350 def add_events(self, events):
1351 markers = [EventMarker(e) for e in events]
1352 self.add_markers(markers)
1354 def set_event_marker_as_origin(self, ignore=None):
1355 selected = self.selected_markers()
1356 if not selected:
1357 self.fail('An event marker must be selected.')
1358 return
1360 m = selected[0]
1361 if not isinstance(m, EventMarker):
1362 self.fail('Selected marker is not an event.')
1363 return
1365 self.set_active_event_marker(m)
1367 def deactivate_event_marker(self):
1368 if self.active_event_marker:
1369 self.active_event_marker.active = False
1371 self.active_event_marker_changed.emit()
1372 self.active_event_marker = None
1374 def set_active_event_marker(self, event_marker):
1375 if self.active_event_marker:
1376 self.active_event_marker.active = False
1378 self.active_event_marker = event_marker
1379 event_marker.active = True
1380 event = event_marker.get_event()
1381 self.set_origin(event)
1382 self.active_event_marker_changed.emit()
1384 def set_active_event(self, event):
1385 for marker in self.markers:
1386 if isinstance(marker, EventMarker):
1387 if marker.get_event() is event:
1388 self.set_active_event_marker(marker)
1390 def get_active_event_marker(self):
1391 return self.active_event_marker
1393 def get_active_event(self):
1394 m = self.get_active_event_marker()
1395 if m is not None:
1396 return m.get_event()
1397 else:
1398 return None
1400 def get_active_markers(self):
1401 emarker = self.get_active_event_marker()
1402 if emarker is None:
1403 return None, []
1405 else:
1406 ev = emarker.get_event()
1407 pmarkers = [
1408 m for m in self.markers
1409 if isinstance(m, PhaseMarker) and m.get_event() is ev]
1411 return emarker, pmarkers
1413 def set_origin(self, location):
1414 for station in self.stations.values():
1415 station.set_event_relative_data(
1416 location,
1417 distance_3d=self.menuitem_distances_3d.isChecked())
1419 self.sortingmode_change()
1421 def distances_3d_changed(self):
1422 ignore = self.menuitem_distances_3d.isChecked()
1423 self.set_event_marker_as_origin(ignore)
1425 def iter_snuffling_modules(self):
1426 pjoin = os.path.join
1427 for path in self.snuffling_paths:
1429 if not os.path.isdir(path):
1430 os.mkdir(path)
1432 for entry in os.listdir(path):
1433 directory = path
1434 fn = entry
1435 d = pjoin(path, entry)
1436 if os.path.isdir(d):
1437 directory = d
1438 if os.path.isfile(
1439 os.path.join(directory, 'snuffling.py')):
1440 fn = 'snuffling.py'
1442 if not fn.endswith('.py'):
1443 continue
1445 name = fn[:-3]
1447 if (directory, name) not in self.snuffling_modules:
1448 self.snuffling_modules[directory, name] = \
1449 pyrocko.gui.snuffling.SnufflingModule(
1450 directory, name, self)
1452 yield self.snuffling_modules[directory, name]
1454 def setup_snufflings(self):
1455 # user snufflings
1456 for mod in self.iter_snuffling_modules():
1457 try:
1458 mod.load_if_needed()
1459 except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
1460 logger.warning('Snuffling module "%s" is broken' % e)
1462 # load the default snufflings on first run
1463 if self.default_snufflings is None:
1464 self.default_snufflings = pyrocko.gui\
1465 .snufflings.__snufflings__()
1466 for snuffling in self.default_snufflings:
1467 self.add_snuffling(snuffling)
1469 def set_panel_parent(self, panel_parent):
1470 self.panel_parent = panel_parent
1472 def get_panel_parent(self):
1473 return self.panel_parent
1475 def add_snuffling(self, snuffling, reloaded=False):
1476 logger.debug('Adding snuffling %s' % snuffling.get_name())
1477 snuffling.init_gui(
1478 self, self.get_panel_parent(), self, reloaded=reloaded)
1479 self.snufflings.append(snuffling)
1480 self.update()
1482 def remove_snuffling(self, snuffling):
1483 snuffling.delete_gui()
1484 self.update()
1485 self.snufflings.remove(snuffling)
1486 snuffling.pre_destroy()
1488 def add_snuffling_menuitem(self, item):
1489 self.snufflings_menu.addAction(item)
1490 item.setParent(self.snufflings_menu)
1491 sort_actions(self.snufflings_menu)
1493 def remove_snuffling_menuitem(self, item):
1494 self.snufflings_menu.removeAction(item)
1496 def add_snuffling_help_menuitem(self, item):
1497 self.snuffling_help.addAction(item)
1498 item.setParent(self.snuffling_help)
1499 sort_actions(self.snuffling_help)
1501 def remove_snuffling_help_menuitem(self, item):
1502 self.snuffling_help.removeAction(item)
1504 def add_panel_toggler(self, item):
1505 self.toggle_panel_menu.addAction(item)
1506 item.setParent(self.toggle_panel_menu)
1507 sort_actions(self.toggle_panel_menu)
1509 def remove_panel_toggler(self, item):
1510 self.toggle_panel_menu.removeAction(item)
1512 def load(self, paths, regex=None, format='detect',
1513 cache_dir=None, force_cache=False):
1515 if cache_dir is None:
1516 cache_dir = pyrocko.config.config().cache_dir
1517 if isinstance(paths, str):
1518 paths = [paths]
1520 fns = pyrocko.util.select_files(
1521 paths, selector=None, include=regex, show_progress=False)
1523 if not fns:
1524 return
1526 cache = pyrocko.pile.get_cache(cache_dir)
1528 t = [time.time()]
1530 def update_bar(label, value):
1531 pbs = self.parent().get_progressbars()
1532 if label.lower() == 'looking at files':
1533 label = 'Looking at %i files' % len(fns)
1534 else:
1535 label = 'Scanning %i files' % len(fns)
1537 return pbs.set_status(label, value)
1539 def update_progress(label, i, n):
1540 abort = False
1542 qw.qApp.processEvents()
1543 if n != 0:
1544 perc = i*100/n
1545 else:
1546 perc = 100
1547 abort |= update_bar(label, perc)
1548 abort |= self.window().is_closing()
1550 tnow = time.time()
1551 if t[0] + 1. + self.time_spent_painting * 10. < tnow:
1552 self.update()
1553 t[0] = tnow
1555 return abort
1557 self.automatic_updates = False
1559 self.pile.load_files(
1560 sorted(fns),
1561 filename_attributes=regex,
1562 cache=cache,
1563 fileformat=format,
1564 show_progress=False,
1565 update_progress=update_progress)
1567 self.automatic_updates = True
1568 self.update()
1570 def load_queued(self):
1571 if not self._paths_to_load:
1572 return
1573 paths = self._paths_to_load
1574 self._paths_to_load = []
1575 self.load(paths)
1577 def load_soon(self, paths):
1578 self._paths_to_load.extend(paths)
1579 qc.QTimer.singleShot(200, self.load_queued)
1581 def open_waveforms(self):
1582 caption = 'Select one or more files to open'
1584 fns, _ = qw.QFileDialog.getOpenFileNames(
1585 self, caption, options=qfiledialog_options)
1587 if fns:
1588 self.load(list(str(fn) for fn in fns))
1590 def open_waveform_directory(self):
1591 caption = 'Select directory to scan for waveform files'
1593 dn = qw.QFileDialog.getExistingDirectory(
1594 self, caption, options=qfiledialog_options)
1596 if dn:
1597 self.load([str(dn)])
1599 def open_stations(self, fns=None):
1600 caption = 'Select one or more Pyrocko station files to open'
1602 if not fns:
1603 fns, _ = qw.QFileDialog.getOpenFileNames(
1604 self, caption, options=qfiledialog_options)
1606 try:
1607 stations = [pyrocko.model.load_stations(str(x)) for x in fns]
1608 for stat in stations:
1609 self.add_stations(stat)
1611 except Exception as e:
1612 self.fail('Failed to read station file: %s' % str(e))
1614 def open_stations_xml(self, fns=None):
1615 from pyrocko.io import stationxml
1617 caption = 'Select one or more StationXML files'
1618 if not fns:
1619 fns, _ = qw.QFileDialog.getOpenFileNames(
1620 self, caption, options=qfiledialog_options,
1621 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)'
1622 ';;All files (*)')
1624 try:
1625 stations = [
1626 stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
1627 for x in fns]
1629 for stat in stations:
1630 self.add_stations(stat)
1632 except Exception as e:
1633 self.fail('Failed to read StationXML file: %s' % str(e))
1635 def add_traces(self, traces):
1636 if traces:
1637 mtf = pyrocko.pile.MemTracesFile(None, traces)
1638 self.pile.add_file(mtf)
1639 ticket = (self.pile, mtf)
1640 return ticket
1641 else:
1642 return (None, None)
1644 def release_data(self, tickets):
1645 for ticket in tickets:
1646 pile, mtf = ticket
1647 if pile is not None:
1648 pile.remove_file(mtf)
1650 def periodical(self):
1651 if self.menuitem_watch.isChecked():
1652 if self.pile.reload_modified():
1653 self.update()
1655 def get_pile(self):
1656 return self.pile
1658 def pile_changed(self, what):
1659 self.pile_has_changed = True
1660 self.pile_has_changed_signal.emit()
1661 if self.automatic_updates:
1662 self.update()
1664 def set_gathering(self, gather=None, color=None):
1666 if gather is None:
1667 def gather_func(tr):
1668 return tr.nslc_id
1670 gather = (0, 1, 2, 3)
1672 else:
1673 def gather_func(tr):
1674 return (
1675 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather))
1677 if color is None:
1678 def color(tr):
1679 return tr.location
1681 self.gather = gather_func
1682 keys = self.pile.gather_keys(gather_func, self.trace_filter)
1684 self.color_gather = color
1685 self.color_keys = self.pile.gather_keys(color)
1686 previous_ntracks = self.ntracks
1687 self.set_ntracks(len(keys))
1689 if self.shown_tracks_range is None or \
1690 previous_ntracks == 0 or \
1691 self.show_all:
1693 low, high = 0, min(self.ntracks_shown_max, self.ntracks)
1694 key_at_top = None
1695 n = high-low
1697 else:
1698 low, high = self.shown_tracks_range
1699 key_at_top = self.track_keys[low]
1700 n = high-low
1702 self.track_keys = sorted(keys)
1704 track_patterns = []
1705 for k in self.track_keys:
1706 pat = ['*', '*', '*', '*']
1707 for i, j in enumerate(gather):
1708 pat[j] = k[-len(gather)+i]
1710 track_patterns.append(pat)
1712 self.track_patterns = track_patterns
1714 if key_at_top is not None:
1715 try:
1716 ind = self.track_keys.index(key_at_top)
1717 low = ind
1718 high = low+n
1719 except Exception:
1720 pass
1722 self.set_tracks_range((low, high))
1724 self.key_to_row = dict(
1725 [(key, i) for (i, key) in enumerate(self.track_keys)])
1727 def inrange(x, r):
1728 return r[0] <= x and x < r[1]
1730 def trace_selector(trace):
1731 gt = self.gather(trace)
1732 return (
1733 gt in self.key_to_row and
1734 inrange(self.key_to_row[gt], self.shown_tracks_range))
1736 self.trace_selector = lambda x: \
1737 (self.trace_filter is None or self.trace_filter(x)) \
1738 and trace_selector(x)
1740 if self.tmin == working_system_time_range[0] and \
1741 self.tmax == working_system_time_range[1] or \
1742 self.show_all:
1744 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
1745 if tmin is not None and tmax is not None:
1746 tlen = (tmax - tmin)
1747 tpad = tlen * 5./self.width()
1748 self.set_time_range(tmin-tpad, tmax+tpad)
1750 def set_time_range(self, tmin, tmax):
1751 if tmin is None:
1752 tmin = initial_time_range[0]
1754 if tmax is None:
1755 tmax = initial_time_range[1]
1757 if tmin > tmax:
1758 tmin, tmax = tmax, tmin
1760 if tmin == tmax:
1761 tmin -= 1.
1762 tmax += 1.
1764 tmin = max(working_system_time_range[0], tmin)
1765 tmax = min(working_system_time_range[1], tmax)
1767 min_deltat = self.content_deltat_range()[0]
1768 if (tmax - tmin < min_deltat):
1769 m = (tmin + tmax) / 2.
1770 tmin = m - min_deltat/2.
1771 tmax = m + min_deltat/2.
1773 self.time_projection.set_in_range(tmin, tmax)
1774 self.tmin, self.tmax = tmin, tmax
1776 def get_time_range(self):
1777 return self.tmin, self.tmax
1779 def ypart(self, y):
1780 if y < self.ax_height:
1781 return -1
1782 elif y > self.height()-self.ax_height:
1783 return 1
1784 else:
1785 return 0
1787 def time_fractional_digits(self):
1788 min_deltat = self.content_deltat_range()[0]
1789 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
1791 def write_markers(self, fn=None):
1792 caption = "Choose a file name to write markers"
1793 if not fn:
1794 fn, _ = qw.QFileDialog.getSaveFileName(
1795 self, caption, options=qfiledialog_options)
1796 if fn:
1797 try:
1798 Marker.save_markers(
1799 self.markers, fn,
1800 fdigits=self.time_fractional_digits())
1802 except Exception as e:
1803 self.fail('Failed to write marker file: %s' % str(e))
1805 def write_selected_markers(self, fn=None):
1806 caption = "Choose a file name to write selected markers"
1807 if not fn:
1808 fn, _ = qw.QFileDialog.getSaveFileName(
1809 self, caption, options=qfiledialog_options)
1810 if fn:
1811 try:
1812 Marker.save_markers(
1813 self.iter_selected_markers(),
1814 fn,
1815 fdigits=self.time_fractional_digits())
1817 except Exception as e:
1818 self.fail('Failed to write marker file: %s' % str(e))
1820 def read_events(self, fn=None):
1821 '''
1822 Open QFileDialog to open, read and add
1823 :py:class:`pyrocko.model.Event` instances and their marker
1824 representation to the pile viewer.
1825 '''
1826 caption = "Selet one or more files to open"
1827 if not fn:
1828 fn, _ = qw.QFileDialog.getOpenFileName(
1829 self, caption, options=qfiledialog_options)
1830 if fn:
1831 try:
1832 self.add_events(pyrocko.model.load_events(fn))
1833 self.associate_phases_to_events()
1835 except Exception as e:
1836 self.fail('Failed to read event file: %s' % str(e))
1838 def read_markers(self, fn=None):
1839 '''
1840 Open QFileDialog to open, read and add markers to the pile viewer.
1841 '''
1842 caption = "Selet one or more marker files to open"
1843 if not fn:
1844 fn, _ = qw.QFileDialog.getOpenFileName(
1845 self, caption, options=qfiledialog_options)
1846 if fn:
1847 try:
1848 self.add_markers(Marker.load_markers(fn))
1849 self.associate_phases_to_events()
1851 except Exception as e:
1852 self.fail('Failed to read marker file: %s' % str(e))
1854 def associate_phases_to_events(self):
1855 associate_phases_to_events(self.markers)
1857 def add_marker(self, marker):
1858 # need index to inform QAbstactTableModel about upcoming change,
1859 # but have to restore current state in order to not cause problems
1860 self.markers.insert(marker)
1861 i = self.markers.remove(marker)
1863 self.begin_markers_add.emit(i, i)
1864 self.markers.insert(marker)
1865 self.end_markers_add.emit()
1866 self.markers_deltat_max = max(
1867 self.markers_deltat_max, marker.tmax - marker.tmin)
1869 def add_markers(self, markers):
1870 if not self.markers:
1871 self.begin_markers_add.emit(0, len(markers) - 1)
1872 self.markers.insert_many(markers)
1873 self.end_markers_add.emit()
1874 self.update_markers_deltat_max()
1875 else:
1876 for marker in markers:
1877 self.add_marker(marker)
1879 def update_markers_deltat_max(self):
1880 if self.markers:
1881 self.markers_deltat_max = max(
1882 marker.tmax - marker.tmin for marker in self.markers)
1884 def remove_marker(self, marker):
1885 '''
1886 Remove a ``marker`` from the :py:class:`PileViewer`.
1888 :param marker: :py:class:`Marker` (or subclass) instance
1889 '''
1891 if marker is self.active_event_marker:
1892 self.deactivate_event_marker()
1894 try:
1895 i = self.markers.index(marker)
1896 self.begin_markers_remove.emit(i, i)
1897 self.markers.remove_at(i)
1898 self.end_markers_remove.emit()
1899 except ValueError:
1900 pass
1902 def remove_markers(self, markers):
1903 '''
1904 Remove a list of ``markers`` from the :py:class:`PileViewer`.
1906 :param markers: list of :py:class:`Marker` (or subclass)
1907 instances
1908 '''
1910 if markers is self.markers:
1911 markers = list(markers)
1913 for marker in markers:
1914 self.remove_marker(marker)
1916 self.update_markers_deltat_max()
1918 def remove_selected_markers(self):
1919 def delete_segment(istart, iend):
1920 self.begin_markers_remove.emit(istart, iend-1)
1921 for _ in range(iend - istart):
1922 self.markers.remove_at(istart)
1924 self.end_markers_remove.emit()
1926 istart = None
1927 ipos = 0
1928 markers = self.markers
1929 nmarkers = len(self.markers)
1930 while ipos < nmarkers:
1931 marker = markers[ipos]
1932 if marker.is_selected():
1933 if marker is self.active_event_marker:
1934 self.deactivate_event_marker()
1936 if istart is None:
1937 istart = ipos
1938 else:
1939 if istart is not None:
1940 delete_segment(istart, ipos)
1941 nmarkers -= ipos - istart
1942 ipos = istart - 1
1943 istart = None
1945 ipos += 1
1947 if istart is not None:
1948 delete_segment(istart, ipos)
1950 self.update_markers_deltat_max()
1952 def selected_markers(self):
1953 return [marker for marker in self.markers if marker.is_selected()]
1955 def iter_selected_markers(self):
1956 for marker in self.markers:
1957 if marker.is_selected():
1958 yield marker
1960 def get_markers(self):
1961 return self.markers
1963 def mousePressEvent(self, mouse_ev):
1964 self.show_all = False
1965 point = self.mapFromGlobal(mouse_ev.globalPos())
1967 if mouse_ev.button() == qc.Qt.LeftButton:
1968 marker = self.marker_under_cursor(point.x(), point.y())
1969 if self.picking:
1970 if self.picking_down is None:
1971 self.picking_down = (
1972 self.time_projection.rev(mouse_ev.x()),
1973 mouse_ev.y())
1975 elif marker is not None:
1976 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
1977 self.deselect_all()
1978 marker.selected = True
1979 self.emit_selected_markers()
1980 self.update()
1981 else:
1982 self.track_start = mouse_ev.x(), mouse_ev.y()
1983 self.track_trange = self.tmin, self.tmax
1985 if mouse_ev.button() == qc.Qt.RightButton \
1986 and isinstance(self.menu, qw.QMenu):
1987 self.menu.exec_(qg.QCursor.pos())
1988 self.update_status()
1990 def mouseReleaseEvent(self, mouse_ev):
1991 if self.ignore_releases:
1992 self.ignore_releases -= 1
1993 return
1995 if self.picking:
1996 self.stop_picking(mouse_ev.x(), mouse_ev.y())
1997 self.emit_selected_markers()
1999 if self.track_start:
2000 self.update()
2002 self.track_start = None
2003 self.track_trange = None
2004 self.update_status()
2006 def mouseDoubleClickEvent(self, mouse_ev):
2007 self.show_all = False
2008 self.start_picking(None)
2009 self.ignore_releases = 1
2011 def mouseMoveEvent(self, mouse_ev):
2012 point = self.mapFromGlobal(mouse_ev.globalPos())
2014 if self.picking:
2015 self.update_picking(point.x(), point.y())
2017 elif self.track_start is not None:
2018 x0, y0 = self.track_start
2019 dx = (point.x() - x0)/float(self.width())
2020 dy = (point.y() - y0)/float(self.height())
2021 if self.ypart(y0) == 1:
2022 dy = 0
2024 tmin0, tmax0 = self.track_trange
2026 scale = math.exp(-dy*5.)
2027 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
2028 frac = x0/float(self.width())
2029 dt = dx*(tmax0-tmin0)*scale
2031 self.interrupt_following()
2032 self.set_time_range(
2033 tmin0 - dt - dtr*frac,
2034 tmax0 - dt + dtr*(1.-frac))
2036 self.update()
2037 else:
2038 self.hoovering(point.x(), point.y())
2040 self.update_status()
2042 def nslc_ids_under_cursor(self, x, y):
2043 ftrack = self.track_to_screen.rev(y)
2044 nslc_ids = self.get_nslc_ids_for_track(ftrack)
2045 return nslc_ids
2047 def marker_under_cursor(self, x, y):
2048 mouset = self.time_projection.rev(x)
2049 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2050 relevant_nslc_ids = None
2051 for marker in self.markers:
2052 if marker.kind not in self.visible_marker_kinds:
2053 continue
2055 if (abs(mouset-marker.tmin) < deltat or
2056 abs(mouset-marker.tmax) < deltat):
2058 if relevant_nslc_ids is None:
2059 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2061 marker_nslc_ids = marker.get_nslc_ids()
2062 if not marker_nslc_ids:
2063 return marker
2065 for nslc_id in marker_nslc_ids:
2066 if nslc_id in relevant_nslc_ids:
2067 return marker
2069 def hoovering(self, x, y):
2070 mouset = self.time_projection.rev(x)
2071 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
2072 needupdate = False
2073 haveone = False
2074 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
2075 for marker in self.markers:
2076 if marker.kind not in self.visible_marker_kinds:
2077 continue
2079 state = abs(mouset-marker.tmin) < deltat or \
2080 abs(mouset-marker.tmax) < deltat and not haveone
2082 if state:
2083 xstate = False
2085 marker_nslc_ids = marker.get_nslc_ids()
2086 if not marker_nslc_ids:
2087 xstate = True
2089 for nslc in relevant_nslc_ids:
2090 if marker.match_nslc(nslc):
2091 xstate = True
2093 state = xstate
2095 if state:
2096 haveone = True
2097 oldstate = marker.is_alerted()
2098 if oldstate != state:
2099 needupdate = True
2100 marker.set_alerted(state)
2101 if state:
2102 self.message = marker.hoover_message()
2104 if not haveone:
2105 self.message = None
2107 if needupdate:
2108 self.update()
2110 def event(self, event):
2111 if event.type() == qc.QEvent.KeyPress:
2112 self.keyPressEvent(event)
2113 return True
2114 else:
2115 return base.event(self, event)
2117 def keyPressEvent(self, key_event):
2118 self.show_all = False
2119 dt = self.tmax - self.tmin
2120 tmid = (self.tmin + self.tmax) / 2.
2122 key = key_event.key()
2123 try:
2124 keytext = str(key_event.text())
2125 except UnicodeEncodeError:
2126 return
2128 if key == qc.Qt.Key_Space:
2129 self.interrupt_following()
2130 self.set_time_range(self.tmin+dt, self.tmax+dt)
2132 elif key == qc.Qt.Key_Up:
2133 for m in self.selected_markers():
2134 if isinstance(m, PhaseMarker):
2135 if key_event.modifiers() & qc.Qt.ShiftModifier:
2136 p = 0
2137 else:
2138 p = 1 if m.get_polarity() != 1 else None
2139 m.set_polarity(p)
2141 elif key == qc.Qt.Key_Down:
2142 for m in self.selected_markers():
2143 if isinstance(m, PhaseMarker):
2144 if key_event.modifiers() & qc.Qt.ShiftModifier:
2145 p = 0
2146 else:
2147 p = -1 if m.get_polarity() != -1 else None
2148 m.set_polarity(p)
2150 elif key == qc.Qt.Key_B:
2151 dt = self.tmax - self.tmin
2152 self.interrupt_following()
2153 self.set_time_range(self.tmin-dt, self.tmax-dt)
2155 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
2156 self.interrupt_following()
2158 tgo = None
2160 class TraceDummy(object):
2161 def __init__(self, marker):
2162 self._marker = marker
2164 @property
2165 def nslc_id(self):
2166 return self._marker.one_nslc()
2168 def marker_to_itrack(marker):
2169 try:
2170 return self.key_to_row.get(
2171 self.gather(TraceDummy(marker)), -1)
2173 except MarkerOneNSLCRequired:
2174 return -1
2176 emarker, pmarkers = self.get_active_markers()
2177 pmarkers = [
2178 m for m in pmarkers if m.kind in self.visible_marker_kinds]
2179 pmarkers.sort(key=lambda m: (
2180 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
2182 if key == qc.Qt.Key_Backtab:
2183 pmarkers.reverse()
2185 smarkers = self.selected_markers()
2186 iselected = []
2187 for sm in smarkers:
2188 try:
2189 iselected.append(pmarkers.index(sm))
2190 except ValueError:
2191 pass
2193 if iselected:
2194 icurrent = max(iselected) + 1
2195 else:
2196 icurrent = 0
2198 if icurrent < len(pmarkers):
2199 self.deselect_all()
2200 cmarker = pmarkers[icurrent]
2201 cmarker.selected = True
2202 tgo = cmarker.tmin
2203 if not self.tmin < tgo < self.tmax:
2204 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2206 itrack = marker_to_itrack(cmarker)
2207 if itrack != -1:
2208 if itrack < self.shown_tracks_range[0]:
2209 self.scroll_tracks(
2210 - (self.shown_tracks_range[0] - itrack))
2211 elif self.shown_tracks_range[1] <= itrack:
2212 self.scroll_tracks(
2213 itrack - self.shown_tracks_range[1]+1)
2215 if itrack not in self.track_to_nslc_ids:
2216 self.go_to_selection()
2218 elif keytext in ('p', 'n', 'P', 'N'):
2219 smarkers = self.selected_markers()
2220 tgo = None
2221 dir = str(keytext)
2222 if smarkers:
2223 tmid = smarkers[0].tmin
2224 for smarker in smarkers:
2225 if dir == 'n':
2226 tmid = max(smarker.tmin, tmid)
2227 else:
2228 tmid = min(smarker.tmin, tmid)
2230 tgo = tmid
2232 if dir.lower() == 'n':
2233 for marker in sorted(
2234 self.markers,
2235 key=operator.attrgetter('tmin')):
2237 t = marker.tmin
2238 if t > tmid and \
2239 marker.kind in self.visible_marker_kinds and \
2240 (dir == 'n' or
2241 isinstance(marker, EventMarker)):
2243 self.deselect_all()
2244 marker.selected = True
2245 tgo = t
2246 break
2247 else:
2248 for marker in sorted(
2249 self.markers,
2250 key=operator.attrgetter('tmin'),
2251 reverse=True):
2253 t = marker.tmin
2254 if t < tmid and \
2255 marker.kind in self.visible_marker_kinds and \
2256 (dir == 'p' or
2257 isinstance(marker, EventMarker)):
2258 self.deselect_all()
2259 marker.selected = True
2260 tgo = t
2261 break
2263 if tgo is not None:
2264 self.interrupt_following()
2265 self.set_time_range(tgo-dt/2., tgo+dt/2.)
2267 elif keytext == 'r':
2268 if self.pile.reload_modified():
2269 self.reloaded = True
2271 elif keytext == 'R':
2272 self.setup_snufflings()
2274 elif key == qc.Qt.Key_Backspace:
2275 self.remove_selected_markers()
2277 elif keytext == 'a':
2278 for marker in self.markers:
2279 if ((self.tmin <= marker.tmin <= self.tmax or
2280 self.tmin <= marker.tmax <= self.tmax) and
2281 marker.kind in self.visible_marker_kinds):
2282 marker.selected = True
2283 else:
2284 marker.selected = False
2286 elif keytext == 'A':
2287 for marker in self.markers:
2288 if marker.kind in self.visible_marker_kinds:
2289 marker.selected = True
2291 elif keytext == 'd':
2292 self.deselect_all()
2294 elif keytext == 'E':
2295 self.deactivate_event_marker()
2297 elif keytext == 'e':
2298 markers = self.selected_markers()
2299 event_markers_in_spe = [
2300 marker for marker in markers
2301 if not isinstance(marker, PhaseMarker)]
2303 phase_markers = [
2304 marker for marker in markers
2305 if isinstance(marker, PhaseMarker)]
2307 if len(event_markers_in_spe) == 1:
2308 event_marker = event_markers_in_spe[0]
2309 if not isinstance(event_marker, EventMarker):
2310 nslcs = list(event_marker.nslc_ids)
2311 lat, lon = 0.0, 0.0
2312 old = self.get_active_event()
2313 if len(nslcs) == 1:
2314 lat, lon = self.station_latlon(NSLC(*nslcs[0]))
2315 elif old is not None:
2316 lat, lon = old.lat, old.lon
2318 event_marker.convert_to_event_marker(lat, lon)
2320 self.set_active_event_marker(event_marker)
2321 event = event_marker.get_event()
2322 for marker in phase_markers:
2323 marker.set_event(event)
2325 else:
2326 for marker in event_markers_in_spe:
2327 marker.convert_to_event_marker()
2329 elif keytext in ('0', '1', '2', '3', '4', '5'):
2330 for marker in self.selected_markers():
2331 marker.set_kind(int(keytext))
2332 self.emit_selected_markers()
2334 elif key in fkey_map:
2335 self.handle_fkeys(key)
2337 elif key == qc.Qt.Key_Escape:
2338 if self.picking:
2339 self.stop_picking(0, 0, abort=True)
2341 elif key == qc.Qt.Key_PageDown:
2342 self.scroll_tracks(
2343 self.shown_tracks_range[1]-self.shown_tracks_range[0])
2345 elif key == qc.Qt.Key_PageUp:
2346 self.scroll_tracks(
2347 self.shown_tracks_range[0]-self.shown_tracks_range[1])
2349 elif key == qc.Qt.Key_Plus:
2350 self.zoom_tracks(0., 1.)
2352 elif key == qc.Qt.Key_Minus:
2353 self.zoom_tracks(0., -1.)
2355 elif key == qc.Qt.Key_Equal:
2356 ntracks_shown = self.shown_tracks_range[1] - \
2357 self.shown_tracks_range[0]
2358 dtracks = self.initial_ntracks_shown_max - ntracks_shown
2359 self.zoom_tracks(0., dtracks)
2361 elif key == qc.Qt.Key_Colon:
2362 self.want_input.emit()
2364 elif keytext == 'f':
2365 self.toggle_fullscreen()
2367 elif keytext == 'g':
2368 self.go_to_selection()
2370 elif keytext == 'G':
2371 self.go_to_selection(tight=True)
2373 elif keytext == 'm':
2374 self.toggle_marker_editor()
2376 elif keytext == 'c':
2377 self.toggle_main_controls()
2379 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
2380 dir = 1
2381 amount = 1
2382 if key_event.key() == qc.Qt.Key_Left:
2383 dir = -1
2384 if key_event.modifiers() & qc.Qt.ShiftModifier:
2385 amount = 10
2386 self.nudge_selected_markers(dir*amount)
2387 else:
2388 super().keyPressEvent(key_event)
2390 if keytext != '' and keytext in 'degaApPnN':
2391 self.emit_selected_markers()
2393 self.update()
2394 self.update_status()
2396 def handle_fkeys(self, key):
2397 self.set_phase_kind(
2398 self.selected_markers(),
2399 fkey_map[key] + 1)
2400 self.emit_selected_markers()
2402 def emit_selected_markers(self):
2403 ibounds = []
2404 last_selected = False
2405 for imarker, marker in enumerate(self.markers):
2406 this_selected = marker.is_selected()
2407 if this_selected != last_selected:
2408 ibounds.append(imarker)
2410 last_selected = this_selected
2412 if last_selected:
2413 ibounds.append(len(self.markers))
2415 chunks = list(zip(ibounds[::2], ibounds[1::2]))
2416 self.n_selected_markers = sum(
2417 chunk[1] - chunk[0] for chunk in chunks)
2418 self.marker_selection_changed.emit(chunks)
2420 def toggle_marker_editor(self):
2421 self.panel_parent.toggle_marker_editor()
2423 def toggle_main_controls(self):
2424 self.panel_parent.toggle_main_controls()
2426 def nudge_selected_markers(self, npixels):
2427 a, b = self.time_projection.ur
2428 c, d = self.time_projection.xr
2429 for marker in self.selected_markers():
2430 if not isinstance(marker, EventMarker):
2431 marker.tmin += npixels * (d-c)/b
2432 marker.tmax += npixels * (d-c)/b
2434 def toggle_fullscreen(self):
2435 if self.window().windowState() & qc.Qt.WindowFullScreen or \
2436 self.window().windowState() & qc.Qt.WindowMaximized:
2437 self.window().showNormal()
2438 else:
2439 if is_macos:
2440 self.window().showMaximized()
2441 else:
2442 self.window().showFullScreen()
2444 def about(self):
2445 fn = pyrocko.util.data_file('snuffler.png')
2446 with open(pyrocko.util.data_file('snuffler_about.html')) as f:
2447 txt = f.read()
2448 label = qw.QLabel(txt % {'logo': fn})
2449 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
2450 self.show_doc('About', [label], target='tab')
2452 def help(self):
2453 class MyScrollArea(qw.QScrollArea):
2455 def sizeHint(self):
2456 s = qc.QSize()
2457 s.setWidth(self.widget().sizeHint().width())
2458 s.setHeight(self.widget().sizeHint().height())
2459 return s
2461 with open(pyrocko.util.data_file(
2462 'snuffler_help.html')) as f:
2463 hcheat = qw.QLabel(f.read())
2465 with open(pyrocko.util.data_file(
2466 'snuffler_help_epilog.html')) as f:
2467 hepilog = qw.QLabel(f.read())
2469 for h in [hcheat, hepilog]:
2470 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
2471 h.setWordWrap(True)
2473 self.show_doc('Help', [hcheat, hepilog], target='panel')
2475 def show_doc(self, name, labels, target='panel'):
2476 scroller = qw.QScrollArea()
2477 frame = qw.QFrame(scroller)
2478 frame.setLineWidth(0)
2479 layout = qw.QVBoxLayout()
2480 layout.setContentsMargins(0, 0, 0, 0)
2481 layout.setSpacing(0)
2482 frame.setLayout(layout)
2483 scroller.setWidget(frame)
2484 scroller.setWidgetResizable(True)
2485 frame.setBackgroundRole(qg.QPalette.Base)
2486 for h in labels:
2487 h.setParent(frame)
2488 h.setMargin(3)
2489 h.setTextInteractionFlags(
2490 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
2491 h.setBackgroundRole(qg.QPalette.Base)
2492 layout.addWidget(h)
2493 h.linkActivated.connect(
2494 self.open_link)
2496 if self.panel_parent is not None:
2497 if target == 'panel':
2498 self.panel_parent.add_panel(
2499 name, scroller, True, volatile=False)
2500 else:
2501 self.panel_parent.add_tab(name, scroller)
2503 def open_link(self, link):
2504 qg.QDesktopServices.openUrl(qc.QUrl(link))
2506 def wheelEvent(self, wheel_event):
2507 self.wheel_pos += wheel_event.angleDelta().y()
2509 n = self.wheel_pos // 120
2510 self.wheel_pos = self.wheel_pos % 120
2511 if n == 0:
2512 return
2514 amount = max(
2515 1.,
2516 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
2517 wdelta = amount * n
2519 trmin, trmax = self.track_to_screen.get_in_range()
2520 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
2521 / (trmax-trmin)
2523 if wheel_event.modifiers() & qc.Qt.ControlModifier:
2524 self.zoom_tracks(anchor, wdelta)
2525 else:
2526 self.scroll_tracks(-wdelta)
2528 def dragEnterEvent(self, event):
2529 if event.mimeData().hasUrls():
2530 if any(url.toLocalFile() for url in event.mimeData().urls()):
2531 event.setDropAction(qc.Qt.LinkAction)
2532 event.accept()
2534 def dropEvent(self, event):
2535 if event.mimeData().hasUrls():
2536 paths = list(
2537 str(url.toLocalFile()) for url in event.mimeData().urls())
2538 event.acceptProposedAction()
2539 self.load(paths)
2541 def get_phase_name(self, kind):
2542 return self.config.get_phase_name(kind)
2544 def set_phase_kind(self, markers, kind):
2545 phasename = self.get_phase_name(kind)
2547 for marker in markers:
2548 if isinstance(marker, PhaseMarker):
2549 if kind == 10:
2550 marker.convert_to_marker()
2551 else:
2552 marker.set_phasename(phasename)
2553 marker.set_event(self.get_active_event())
2555 elif isinstance(marker, EventMarker):
2556 pass
2558 else:
2559 if kind != 10:
2560 event = self.get_active_event()
2561 marker.convert_to_phase_marker(
2562 event, phasename, None, False)
2564 def set_ntracks(self, ntracks):
2565 if self.ntracks != ntracks:
2566 self.ntracks = ntracks
2567 if self.shown_tracks_range is not None:
2568 l, h = self.shown_tracks_range
2569 else:
2570 l, h = 0, self.ntracks
2572 self.tracks_range_changed.emit(self.ntracks, l, h)
2574 def set_tracks_range(self, range, start=None):
2576 low, high = range
2577 low = min(self.ntracks-1, low)
2578 high = min(self.ntracks, high)
2579 low = max(0, low)
2580 high = max(1, high)
2582 if start is None:
2583 start = float(low)
2585 if self.shown_tracks_range != (low, high):
2586 self.shown_tracks_range = low, high
2587 self.shown_tracks_start = start
2589 self.tracks_range_changed.emit(self.ntracks, low, high)
2591 def scroll_tracks(self, shift):
2592 shown = self.shown_tracks_range
2593 shiftmin = -shown[0]
2594 shiftmax = self.ntracks-shown[1]
2595 shift = max(shiftmin, shift)
2596 shift = min(shiftmax, shift)
2597 shown = shown[0] + shift, shown[1] + shift
2599 self.set_tracks_range((int(shown[0]), int(shown[1])))
2601 self.update()
2603 def zoom_tracks(self, anchor, delta):
2604 ntracks_shown = self.shown_tracks_range[1] \
2605 - self.shown_tracks_range[0]
2607 if (ntracks_shown == 1 and delta <= 0) or \
2608 (ntracks_shown == self.ntracks and delta >= 0):
2609 return
2611 ntracks_shown += int(round(delta))
2612 ntracks_shown = min(max(1, ntracks_shown), self.ntracks)
2614 u = self.shown_tracks_start
2615 nu = max(0., u-anchor*delta)
2616 nv = nu + ntracks_shown
2617 if nv > self.ntracks:
2618 nu -= nv - self.ntracks
2619 nv -= nv - self.ntracks
2621 self.set_tracks_range((int(round(nu)), int(round(nv))), nu)
2623 self.ntracks_shown_max = self.shown_tracks_range[1] \
2624 - self.shown_tracks_range[0]
2626 self.update()
2628 def content_time_range(self):
2629 pile = self.get_pile()
2630 tmin, tmax = pile.get_tmin(), pile.get_tmax()
2631 if tmin is None:
2632 tmin = initial_time_range[0]
2633 if tmax is None:
2634 tmax = initial_time_range[1]
2636 return tmin, tmax
2638 def content_deltat_range(self):
2639 pile = self.get_pile()
2641 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax()
2643 if deltatmin is None:
2644 deltatmin = 0.001
2646 if deltatmax is None:
2647 deltatmax = 1000.0
2649 return deltatmin, deltatmax
2651 def make_good_looking_time_range(self, tmin, tmax, tight=False):
2652 if tmax < tmin:
2653 tmin, tmax = tmax, tmin
2655 deltatmin = self.content_deltat_range()[0]
2656 dt = deltatmin * self.visible_length * 0.95
2658 if dt == 0.0:
2659 dt = 1.0
2661 if tight:
2662 if tmax != tmin:
2663 dtm = tmax - tmin
2664 tmin -= dtm*0.1
2665 tmax += dtm*0.1
2666 return tmin, tmax
2667 else:
2668 tcenter = (tmin + tmax) / 2.
2669 tmin = tcenter - 0.5*dt
2670 tmax = tcenter + 0.5*dt
2671 return tmin, tmax
2673 if tmax-tmin < dt:
2674 vmin, vmax = self.get_time_range()
2675 dt = min(vmax - vmin, dt)
2677 tcenter = (tmin+tmax)/2.
2678 etmin, etmax = tmin, tmax
2679 tmin = min(etmin, tcenter - 0.5*dt)
2680 tmax = max(etmax, tcenter + 0.5*dt)
2681 dtm = tmax-tmin
2682 if etmin == tmin:
2683 tmin -= dtm*0.1
2684 if etmax == tmax:
2685 tmax += dtm*0.1
2687 else:
2688 dtm = tmax-tmin
2689 tmin -= dtm*0.1
2690 tmax += dtm*0.1
2692 return tmin, tmax
2694 def go_to_selection(self, tight=False):
2695 markers = self.selected_markers()
2696 if markers:
2697 tmax, tmin = self.content_time_range()
2698 for marker in markers:
2699 tmin = min(tmin, marker.tmin)
2700 tmax = max(tmax, marker.tmax)
2702 else:
2703 if tight:
2704 vmin, vmax = self.get_time_range()
2705 tmin = tmax = (vmin + vmax) / 2.
2706 else:
2707 tmin, tmax = self.content_time_range()
2709 tmin, tmax = self.make_good_looking_time_range(
2710 tmin, tmax, tight=tight)
2712 self.interrupt_following()
2713 self.set_time_range(tmin, tmax)
2714 self.update()
2716 def go_to_time(self, t, tlen=None):
2717 tmax = t
2718 if tlen is not None:
2719 tmax = t+tlen
2720 tmin, tmax = self.make_good_looking_time_range(t, tmax)
2721 self.interrupt_following()
2722 self.set_time_range(tmin, tmax)
2723 self.update()
2725 def go_to_event_by_name(self, name):
2726 for marker in self.markers:
2727 if isinstance(marker, EventMarker):
2728 event = marker.get_event()
2729 if event.name and event.name.lower() == name.lower():
2730 tmin, tmax = self.make_good_looking_time_range(
2731 event.time, event.time)
2733 self.interrupt_following()
2734 self.set_time_range(tmin, tmax)
2736 def printit(self):
2737 from .qt_compat import qprint
2738 printer = qprint.QPrinter()
2739 printer.setOrientation(qprint.QPrinter.Landscape)
2741 dialog = qprint.QPrintDialog(printer, self)
2742 dialog.setWindowTitle('Print')
2744 if dialog.exec_() != qw.QDialog.Accepted:
2745 return
2747 painter = qg.QPainter()
2748 painter.begin(printer)
2749 page = printer.pageRect()
2750 self.drawit(
2751 painter, printmode=False, w=page.width(), h=page.height())
2753 painter.end()
2755 def savesvg(self, fn=None):
2757 if not fn:
2758 fn, _ = qw.QFileDialog.getSaveFileName(
2759 self,
2760 'Save as SVG|PNG',
2761 os.path.expanduser(os.path.join('~', 'untitled.svg')),
2762 'SVG|PNG (*.svg *.png)',
2763 options=qfiledialog_options)
2765 if fn == '':
2766 return
2768 fn = str(fn)
2770 if fn.lower().endswith('.svg'):
2771 try:
2772 w, h = 842, 595
2773 margin = 0.025
2774 m = max(w, h)*margin
2776 generator = qsvg.QSvgGenerator()
2777 generator.setFileName(fn)
2778 generator.setSize(qc.QSize(w, h))
2779 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m))
2781 painter = qg.QPainter()
2782 painter.begin(generator)
2783 self.drawit(painter, printmode=False, w=w, h=h)
2784 painter.end()
2786 except Exception as e:
2787 self.fail('Failed to write SVG file: %s' % str(e))
2789 elif fn.lower().endswith('.png'):
2790 pixmap = self.grab()
2792 try:
2793 pixmap.save(fn)
2795 except Exception as e:
2796 self.fail('Failed to write PNG file: %s' % str(e))
2798 else:
2799 self.fail(
2800 'Unsupported file type: filename must end with ".svg" or '
2801 '".png".')
2803 def paintEvent(self, paint_ev):
2804 '''
2805 Called by QT whenever widget needs to be painted.
2806 '''
2807 # Prevent a problem on macos with QOpenGLWidget, where paintEvent
2808 # was called twice (by different threads?), causing segfaults.
2809 if self.in_paint_event:
2810 logger.warning('Blocking reentrant call to paintEvent().')
2811 return
2813 self.in_paint_event = True
2815 painter = qg.QPainter(self)
2817 if self.menuitem_antialias.isChecked():
2818 painter.setRenderHint(qg.QPainter.Antialiasing)
2820 self.drawit(painter)
2822 logger.debug(
2823 'Time spent drawing: '
2824 ' user:%.3f sys:%.3f children_user:%.3f'
2825 ' childred_sys:%.3f elapsed:%.3f' %
2826 (self.timer_draw - self.timer_cutout))
2828 logger.debug(
2829 'Time spent processing:'
2830 ' user:%.3f sys:%.3f children_user:%.3f'
2831 ' childred_sys:%.3f elapsed:%.3f' %
2832 self.timer_cutout.get())
2834 self.time_spent_painting = self.timer_draw.get()[-1]
2835 self.time_last_painted = time.time()
2836 self.in_paint_event = False
2838 def determine_box_styles(self):
2840 traces = list(self.pile.iter_traces())
2841 traces.sort(key=operator.attrgetter('full_id'))
2842 istyle = 0
2843 trace_styles = {}
2844 for itr, tr in enumerate(traces):
2845 if itr > 0:
2846 other = traces[itr-1]
2847 if not (
2848 other.nslc_id == tr.nslc_id
2849 and other.deltat == tr.deltat
2850 and abs(other.tmax - tr.tmin)
2851 < gap_lap_tolerance*tr.deltat):
2853 istyle += 1
2855 trace_styles[tr.full_id, tr.deltat] = istyle
2857 self.trace_styles = trace_styles
2859 def draw_trace_boxes(self, p, time_projection, track_projections):
2861 for v_projection in track_projections.values():
2862 v_projection.set_in_range(0., 1.)
2864 def selector(x):
2865 return x.overlaps(*time_projection.get_in_range())
2867 if self.trace_filter is not None:
2868 def tselector(x):
2869 return selector(x) and self.trace_filter(x)
2871 else:
2872 tselector = selector
2874 traces = list(self.pile.iter_traces(
2875 group_selector=selector, trace_selector=tselector))
2877 traces.sort(key=operator.attrgetter('full_id'))
2879 def drawbox(itrack, istyle, traces):
2880 v_projection = track_projections[itrack]
2881 dvmin = v_projection(0.)
2882 dvmax = v_projection(1.)
2883 dtmin = time_projection.clipped(traces[0].tmin)
2884 dtmax = time_projection.clipped(traces[-1].tmax)
2886 style = box_styles[istyle % len(box_styles)]
2887 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2888 p.fillRect(rect, style.fill_brush)
2889 p.setPen(style.frame_pen)
2890 p.drawRect(rect)
2892 traces_by_style = {}
2893 for itr, tr in enumerate(traces):
2894 gt = self.gather(tr)
2895 if gt not in self.key_to_row:
2896 continue
2898 itrack = self.key_to_row[gt]
2899 if itrack not in track_projections:
2900 continue
2902 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0)
2904 if len(traces) < 500:
2905 drawbox(itrack, istyle, [tr])
2906 else:
2907 if (itrack, istyle) not in traces_by_style:
2908 traces_by_style[itrack, istyle] = []
2909 traces_by_style[itrack, istyle].append(tr)
2911 for (itrack, istyle), traces in traces_by_style.items():
2912 drawbox(itrack, istyle, traces)
2914 def draw_visible_markers(
2915 self, p, vcenter_projection, primary_pen):
2917 try:
2918 markers = self.markers.with_key_in_limited(
2919 self.tmin - self.markers_deltat_max, self.tmax, 2000)
2921 except pyrocko.pile.TooMany:
2922 tmin = self.markers[0].tmin
2923 tmax = self.markers[-1].tmax
2924 umin_view, umax_view = self.time_projection.get_out_range()
2925 umin = max(umin_view, self.time_projection(tmin))
2926 umax = min(umax_view, self.time_projection(tmax))
2927 v0, _ = vcenter_projection.get_out_range()
2928 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
2930 p.save()
2932 pen = qg.QPen(primary_pen)
2933 pen.setWidth(2)
2934 pen.setStyle(qc.Qt.DotLine)
2935 # pat = [5., 3.]
2936 # pen.setDashPattern(pat)
2937 p.setPen(pen)
2939 if self.n_selected_markers == len(self.markers):
2940 s_selected = ' (all selected)'
2941 elif self.n_selected_markers > 0:
2942 s_selected = ' (%i selected)' % self.n_selected_markers
2943 else:
2944 s_selected = ''
2946 draw_label(
2947 p, umin+10., v0-10.,
2948 '%i Markers' % len(self.markers) + s_selected,
2949 label_bg, 'LB')
2951 line = qc.QLineF(umin, v0, umax, v0)
2952 p.drawLine(line)
2953 p.restore()
2955 return
2957 for marker in markers:
2958 if marker.tmin < self.tmax and self.tmin < marker.tmax \
2959 and marker.kind in self.visible_marker_kinds:
2961 marker.draw(
2962 p, self.time_projection, vcenter_projection,
2963 with_label=True)
2965 def get_squirrel(self):
2966 try:
2967 return self.pile._squirrel
2968 except AttributeError:
2969 return None
2971 def draw_coverage(self, p, time_projection, track_projections):
2972 sq = self.get_squirrel()
2973 if sq is None:
2974 return
2976 def drawbox(itrack, tmin, tmax, style):
2977 v_projection = track_projections[itrack]
2978 dvmin = v_projection(0.)
2979 dvmax = v_projection(1.)
2980 dtmin = time_projection.clipped(tmin)
2981 dtmax = time_projection.clipped(tmax)
2983 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin)
2984 p.fillRect(rect, style.fill_brush)
2985 p.setPen(style.frame_pen)
2986 p.drawRect(rect)
2988 pattern_list = []
2989 pattern_to_itrack = {}
2990 for key in self.track_keys:
2991 itrack = self.key_to_row[key]
2992 if itrack not in track_projections:
2993 continue
2995 pattern = self.track_patterns[itrack]
2996 pattern_to_itrack[tuple(pattern)] = itrack
2997 pattern_list.append(tuple(pattern))
2999 vmin, vmax = self.get_time_range()
3001 for kind in ['waveform', 'waveform_promise']:
3002 for coverage in sq.get_coverage(
3003 kind, vmin, vmax, pattern_list, limit=500):
3004 itrack = pattern_to_itrack[coverage.pattern.nslc]
3006 if coverage.changes is None:
3007 drawbox(
3008 itrack, coverage.tmin, coverage.tmax,
3009 box_styles_coverage[kind][0])
3010 else:
3011 t = None
3012 pcount = 0
3013 for tb, count in coverage.changes:
3014 if t is not None and tb > t:
3015 if pcount > 0:
3016 drawbox(
3017 itrack, t, tb,
3018 box_styles_coverage[kind][
3019 min(len(box_styles_coverage)-1,
3020 pcount)])
3022 t = tb
3023 pcount = count
3025 def drawit(self, p, printmode=False, w=None, h=None):
3026 '''
3027 This performs the actual drawing.
3028 '''
3030 self.timer_draw.start()
3031 show_boxes = self.menuitem_showboxes.isChecked()
3032 sq = self.get_squirrel()
3034 if self.gather is None:
3035 self.set_gathering()
3037 if self.pile_has_changed:
3039 if not self.sortingmode_change_delayed():
3040 self.sortingmode_change()
3042 if show_boxes and sq is None:
3043 self.determine_box_styles()
3045 self.pile_has_changed = False
3047 if h is None:
3048 h = float(self.height())
3049 if w is None:
3050 w = float(self.width())
3052 if printmode:
3053 primary_color = (0, 0, 0)
3054 else:
3055 primary_color = pyrocko.plot.tango_colors['aluminium5']
3057 primary_pen = qg.QPen(qg.QColor(*primary_color))
3059 ax_h = float(self.ax_height)
3061 vbottom_ax_projection = Projection()
3062 vtop_ax_projection = Projection()
3063 vcenter_projection = Projection()
3065 self.time_projection.set_out_range(0., w)
3066 vbottom_ax_projection.set_out_range(h-ax_h, h)
3067 vtop_ax_projection.set_out_range(0., ax_h)
3068 vcenter_projection.set_out_range(ax_h, h-ax_h)
3069 vcenter_projection.set_in_range(0., 1.)
3070 self.track_to_screen.set_out_range(ax_h, h-ax_h)
3072 self.track_to_screen.set_in_range(*self.shown_tracks_range)
3073 track_projections = {}
3074 for i in range(*self.shown_tracks_range):
3075 proj = Projection()
3076 proj.set_out_range(
3077 self.track_to_screen(i+0.05),
3078 self.track_to_screen(i+1.-0.05))
3080 track_projections[i] = proj
3082 if self.tmin > self.tmax:
3083 return
3085 self.time_projection.set_in_range(self.tmin, self.tmax)
3086 vbottom_ax_projection.set_in_range(0, ax_h)
3088 self.tax.drawit(p, self.time_projection, vbottom_ax_projection)
3090 yscaler = pyrocko.plot.AutoScaler()
3092 p.setPen(primary_pen)
3094 font = qg.QFont()
3095 font.setBold(True)
3097 axannotfont = qg.QFont()
3098 axannotfont.setBold(True)
3099 axannotfont.setPointSize(8)
3101 processed_traces = self.prepare_cutout2(
3102 self.tmin, self.tmax,
3103 trace_selector=self.trace_selector,
3104 degap=self.menuitem_degap.isChecked(),
3105 demean=self.menuitem_demean.isChecked())
3107 if not printmode and show_boxes:
3108 if (self.view_mode is ViewMode.Wiggle) \
3109 or (self.view_mode is ViewMode.Waterfall
3110 and not processed_traces):
3112 if sq is None:
3113 self.draw_trace_boxes(
3114 p, self.time_projection, track_projections)
3116 else:
3117 self.draw_coverage(
3118 p, self.time_projection, track_projections)
3120 p.setFont(font)
3121 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100))
3123 color_lookup = dict(
3124 [(k, i) for (i, k) in enumerate(self.color_keys)])
3126 self.track_to_nslc_ids = {}
3127 nticks = 0
3128 annot_labels = []
3130 if self.view_mode is ViewMode.Waterfall and processed_traces:
3131 waterfall = self.waterfall
3132 waterfall.set_time_range(self.tmin, self.tmax)
3133 waterfall.set_traces(processed_traces)
3134 waterfall.set_cmap(self.waterfall_cmap)
3135 waterfall.set_integrate(self.waterfall_integrate)
3136 waterfall.set_clip(
3137 self.waterfall_clip_min, self.waterfall_clip_max)
3138 waterfall.show_absolute_values(
3139 self.waterfall_show_absolute)
3141 rect = qc.QRectF(
3142 0, self.ax_height,
3143 self.width(), self.height() - self.ax_height*2
3144 )
3145 waterfall.draw_waterfall(p, rect=rect)
3147 elif self.view_mode is ViewMode.Wiggle and processed_traces:
3148 show_scales = self.menuitem_showscalerange.isChecked() \
3149 or self.menuitem_showscaleaxis.isChecked()
3151 fm = qg.QFontMetrics(axannotfont, p.device())
3152 trackheight = self.track_to_screen(1.-0.05) \
3153 - self.track_to_screen(0.05)
3155 nlinesavail = trackheight/float(fm.lineSpacing())
3157 nticks = max(3, min(nlinesavail * 0.5, 15)) \
3158 if self.menuitem_showscaleaxis.isChecked() \
3159 else 15
3161 yscaler = pyrocko.plot.AutoScaler(
3162 no_exp_interval=(-3, 2), approx_ticks=nticks,
3163 snap=show_scales
3164 and not self.menuitem_showscaleaxis.isChecked())
3166 data_ranges = pyrocko.trace.minmax(
3167 processed_traces,
3168 key=self.scaling_key,
3169 mode=self.scaling_base)
3171 if not self.menuitem_fixscalerange.isChecked():
3172 self.old_data_ranges = data_ranges
3173 else:
3174 data_ranges.update(self.old_data_ranges)
3176 self.apply_scaling_hooks(data_ranges)
3178 trace_to_itrack = {}
3179 track_scaling_keys = {}
3180 track_scaling_colors = {}
3181 for trace in processed_traces:
3182 gt = self.gather(trace)
3183 if gt not in self.key_to_row:
3184 continue
3186 itrack = self.key_to_row[gt]
3187 if itrack not in track_projections:
3188 continue
3190 trace_to_itrack[trace] = itrack
3192 if itrack not in self.track_to_nslc_ids:
3193 self.track_to_nslc_ids[itrack] = set()
3195 self.track_to_nslc_ids[itrack].add(trace.nslc_id)
3197 if itrack not in track_scaling_keys:
3198 track_scaling_keys[itrack] = set()
3200 scaling_key = self.scaling_key(trace)
3201 track_scaling_keys[itrack].add(scaling_key)
3203 color = pyrocko.plot.color(
3204 color_lookup[self.color_gather(trace)])
3206 k = itrack, scaling_key
3207 if k not in track_scaling_colors \
3208 and self.menuitem_colortraces.isChecked():
3209 track_scaling_colors[k] = color
3210 else:
3211 track_scaling_colors[k] = primary_color
3213 # y axes, zero lines
3214 trace_projections = {}
3215 for itrack in list(track_projections.keys()):
3216 if itrack not in track_scaling_keys:
3217 continue
3218 uoff = 0
3219 for scaling_key in track_scaling_keys[itrack]:
3220 data_range = data_ranges[scaling_key]
3221 dymin, dymax = data_range
3222 ymin, ymax, yinc = yscaler.make_scale(
3223 (dymin/self.gain, dymax/self.gain))
3224 iexp = yscaler.make_exp(yinc)
3225 factor = 10**iexp
3226 trace_projection = track_projections[itrack].copy()
3227 trace_projection.set_in_range(ymax, ymin)
3228 trace_projections[itrack, scaling_key] = \
3229 trace_projection
3230 umin, umax = self.time_projection.get_out_range()
3231 vmin, vmax = trace_projection.get_out_range()
3232 umax_zeroline = umax
3233 uoffnext = uoff
3235 if show_scales:
3236 pen = qg.QPen(primary_pen)
3237 k = itrack, scaling_key
3238 if k in track_scaling_colors:
3239 c = qg.QColor(*track_scaling_colors[
3240 itrack, scaling_key])
3242 pen.setColor(c)
3244 p.setPen(pen)
3245 if nlinesavail > 3:
3246 if self.menuitem_showscaleaxis.isChecked():
3247 ymin_annot = math.ceil(ymin/yinc)*yinc
3248 ny_annot = int(
3249 math.floor(ymax/yinc)
3250 - math.ceil(ymin/yinc)) + 1
3252 for iy_annot in range(ny_annot):
3253 y = ymin_annot + iy_annot*yinc
3254 v = trace_projection(y)
3255 line = qc.QLineF(
3256 umax-10-uoff, v, umax-uoff, v)
3258 p.drawLine(line)
3259 if iy_annot == ny_annot - 1 \
3260 and iexp != 0:
3261 sexp = ' × ' \
3262 '10<sup>%i</sup>' % iexp
3263 else:
3264 sexp = ''
3266 snum = num_to_html(y/factor)
3267 lab = Label(
3268 p,
3269 umax-20-uoff,
3270 v, '%s%s' % (snum, sexp),
3271 label_bg=None,
3272 anchor='MR',
3273 font=axannotfont,
3274 color=c)
3276 uoffnext = max(
3277 lab.rect.width()+30., uoffnext)
3279 annot_labels.append(lab)
3280 if y == 0.:
3281 umax_zeroline = \
3282 umax - 20 \
3283 - lab.rect.width() - 10 \
3284 - uoff
3285 else:
3286 if not show_boxes:
3287 qpoints = make_QPolygonF(
3288 [umax-20-uoff,
3289 umax-10-uoff,
3290 umax-10-uoff,
3291 umax-20-uoff],
3292 [vmax, vmax, vmin, vmin])
3293 p.drawPolyline(qpoints)
3295 snum = num_to_html(ymin)
3296 labmin = Label(
3297 p, umax-15-uoff, vmax, snum,
3298 label_bg=None,
3299 anchor='BR',
3300 font=axannotfont,
3301 color=c)
3303 annot_labels.append(labmin)
3304 snum = num_to_html(ymax)
3305 labmax = Label(
3306 p, umax-15-uoff, vmin, snum,
3307 label_bg=None,
3308 anchor='TR',
3309 font=axannotfont,
3310 color=c)
3312 annot_labels.append(labmax)
3314 for lab in (labmin, labmax):
3315 uoffnext = max(
3316 lab.rect.width()+10., uoffnext)
3318 if self.menuitem_showzeroline.isChecked():
3319 v = trace_projection(0.)
3320 if vmin <= v <= vmax:
3321 line = qc.QLineF(umin, v, umax_zeroline, v)
3322 p.drawLine(line)
3324 uoff = uoffnext
3326 p.setFont(font)
3327 p.setPen(primary_pen)
3328 for trace in processed_traces:
3329 if self.view_mode is not ViewMode.Wiggle:
3330 break
3332 if trace not in trace_to_itrack:
3333 continue
3335 itrack = trace_to_itrack[trace]
3336 scaling_key = self.scaling_key(trace)
3337 trace_projection = trace_projections[
3338 itrack, scaling_key]
3340 vdata = trace_projection(trace.get_ydata())
3342 udata_min = float(self.time_projection(trace.tmin))
3343 udata_max = float(self.time_projection(
3344 trace.tmin+trace.deltat*(vdata.size-1)))
3345 udata = num.linspace(udata_min, udata_max, vdata.size)
3347 qpoints = make_QPolygonF(udata, vdata)
3349 umin, umax = self.time_projection.get_out_range()
3350 vmin, vmax = trace_projection.get_out_range()
3352 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin)
3354 if self.menuitem_cliptraces.isChecked():
3355 p.setClipRect(trackrect)
3357 if self.menuitem_colortraces.isChecked():
3358 color = pyrocko.plot.color(
3359 color_lookup[self.color_gather(trace)])
3360 pen = qg.QPen(qg.QColor(*color), 1)
3361 p.setPen(pen)
3363 p.drawPolyline(qpoints)
3365 if self.floating_marker:
3366 self.floating_marker.draw_trace(
3367 self, p, trace,
3368 self.time_projection, trace_projection, 1.0)
3370 for marker in self.markers.with_key_in(
3371 self.tmin - self.markers_deltat_max,
3372 self.tmax):
3374 if marker.tmin < self.tmax \
3375 and self.tmin < marker.tmax \
3376 and marker.kind \
3377 in self.visible_marker_kinds:
3378 marker.draw_trace(
3379 self, p, trace, self.time_projection,
3380 trace_projection, 1.0)
3382 p.setPen(primary_pen)
3384 if self.menuitem_cliptraces.isChecked():
3385 p.setClipRect(0, 0, int(w), int(h))
3387 if self.floating_marker:
3388 self.floating_marker.draw(
3389 p, self.time_projection, vcenter_projection)
3391 self.draw_visible_markers(
3392 p, vcenter_projection, primary_pen)
3394 p.setPen(primary_pen)
3395 while font.pointSize() > 2:
3396 fm = qg.QFontMetrics(font, p.device())
3397 trackheight = self.track_to_screen(1.-0.05) \
3398 - self.track_to_screen(0.05)
3399 nlinesavail = trackheight/float(fm.lineSpacing())
3400 if nlinesavail > 1:
3401 break
3403 font.setPointSize(font.pointSize()-1)
3405 p.setFont(font)
3406 mouse_pos = self.mapFromGlobal(qg.QCursor.pos())
3408 for key in self.track_keys:
3409 itrack = self.key_to_row[key]
3410 if itrack in track_projections:
3411 plabel = ' '.join(
3412 [str(x) for x in key if x is not None])
3413 lx = 10
3414 ly = self.track_to_screen(itrack+0.5)
3416 if p.font().pointSize() >= MIN_LABEL_SIZE_PT:
3417 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3418 continue
3420 contains_cursor = \
3421 self.track_to_screen(itrack) \
3422 < mouse_pos.y() \
3423 < self.track_to_screen(itrack+1)
3425 if not contains_cursor:
3426 continue
3428 font_large = p.font()
3429 font_large.setPointSize(MIN_LABEL_SIZE_PT)
3430 p.setFont(font_large)
3431 draw_label(p, lx, ly, plabel, label_bg, 'ML')
3432 p.setFont(font)
3434 for lab in annot_labels:
3435 lab.draw()
3437 self.timer_draw.stop()
3439 def see_data_params(self):
3441 min_deltat = self.content_deltat_range()[0]
3443 # determine padding and downampling requirements
3444 if self.lowpass is not None:
3445 deltat_target = 1./self.lowpass * 0.25
3446 ndecimate = min(
3447 50,
3448 max(1, int(round(deltat_target / min_deltat))))
3449 tpad = 1./self.lowpass * 2.
3450 else:
3451 ndecimate = 1
3452 tpad = min_deltat*5.
3454 if self.highpass is not None:
3455 tpad = max(1./self.highpass * 2., tpad)
3457 nsee_points_per_trace = 5000*10
3458 tsee = ndecimate*nsee_points_per_trace*min_deltat
3460 return ndecimate, tpad, tsee
3462 def clean_update(self):
3463 self.cached_processed_traces = None
3464 self.update()
3466 def get_adequate_tpad(self):
3467 tpad = 0.
3468 for f in [self.highpass, self.lowpass]:
3469 if f is not None:
3470 tpad = max(tpad, 1.0/f)
3472 for snuffling in self.snufflings:
3473 if snuffling._post_process_hook_enabled \
3474 or snuffling._pre_process_hook_enabled:
3476 tpad = max(tpad, snuffling.get_tpad())
3478 return tpad
3480 def prepare_cutout2(
3481 self, tmin, tmax, trace_selector=None, degap=True,
3482 demean=True, nmax=6000):
3484 if self.pile.is_empty():
3485 return []
3487 nmax = self.visible_length
3489 self.timer_cutout.start()
3491 tsee = tmax-tmin
3492 min_deltat_wo_decimate = tsee/nmax
3493 min_deltat_w_decimate = min_deltat_wo_decimate / 32.
3495 min_deltat_allow = min_deltat_wo_decimate
3496 if self.lowpass is not None:
3497 target_deltat_lp = 0.25/self.lowpass
3498 if target_deltat_lp > min_deltat_wo_decimate:
3499 min_deltat_allow = min_deltat_w_decimate
3501 min_deltat_allow = math.exp(
3502 int(math.floor(math.log(min_deltat_allow))))
3504 tmin_ = tmin
3505 tmax_ = tmax
3507 # fetch more than needed?
3508 if self.menuitem_liberal_fetch.isChecked():
3509 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5)
3510 tmin = math.floor(tmin/tlen) * tlen
3511 tmax = math.ceil(tmax/tlen) * tlen
3513 fft_filtering = self.menuitem_fft_filtering.isChecked()
3514 lphp = self.menuitem_lphp.isChecked()
3515 ads = self.menuitem_allowdownsampling.isChecked()
3517 tpad = self.get_adequate_tpad()
3518 tpad = max(tpad, tsee)
3520 # state vector to decide if cached traces can be used
3521 vec = (
3522 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass,
3523 self.highpass, fft_filtering, lphp,
3524 min_deltat_allow, self.rotate, self.shown_tracks_range,
3525 ads, self.pile.get_update_count())
3527 if (self.cached_vec
3528 and self.cached_vec[0] <= vec[0]
3529 and vec[1] <= self.cached_vec[1]
3530 and vec[2:] == self.cached_vec[2:]
3531 and not (self.reloaded or self.menuitem_watch.isChecked())
3532 and self.cached_processed_traces is not None):
3534 logger.debug('Using cached traces')
3535 processed_traces = self.cached_processed_traces
3537 else:
3538 processed_traces = []
3539 if self.pile.deltatmax >= min_deltat_allow:
3541 def group_selector(gr):
3542 return gr.deltatmax >= min_deltat_allow
3544 if trace_selector is not None:
3545 def trace_selectorx(tr):
3546 return tr.deltat >= min_deltat_allow \
3547 and trace_selector(tr)
3548 else:
3549 def trace_selectorx(tr):
3550 return tr.deltat >= min_deltat_allow
3552 for traces in self.pile.chopper(
3553 tmin=tmin, tmax=tmax, tpad=tpad,
3554 want_incomplete=True,
3555 degap=degap,
3556 maxgap=gap_lap_tolerance,
3557 maxlap=gap_lap_tolerance,
3558 keep_current_files_open=True,
3559 group_selector=group_selector,
3560 trace_selector=trace_selectorx,
3561 accessor_id=id(self),
3562 snap=(math.floor, math.ceil),
3563 include_last=True):
3565 if demean:
3566 for tr in traces:
3567 if (tr.meta and tr.meta.get('tabu', False)):
3568 continue
3569 y = tr.get_ydata()
3570 tr.set_ydata(y - num.mean(y))
3572 traces = self.pre_process_hooks(traces)
3574 for trace in traces:
3576 if not (trace.meta
3577 and trace.meta.get('tabu', False)):
3579 if fft_filtering:
3580 but = pyrocko.response.ButterworthResponse
3581 multres = pyrocko.response.MultiplyResponse
3582 if self.lowpass is not None \
3583 or self.highpass is not None:
3585 it = num.arange(
3586 trace.data_len(), dtype=float)
3587 detr_data, m, b = detrend(
3588 it, trace.get_ydata())
3590 trace.set_ydata(detr_data)
3592 freqs, fdata = trace.spectrum(
3593 pad_to_pow2=True, tfade=None)
3595 nfreqs = fdata.size
3597 key = (trace.deltat, nfreqs)
3599 if key not in self.tf_cache:
3600 resps = []
3601 if self.lowpass is not None:
3602 resps.append(but(
3603 order=4,
3604 corner=self.lowpass,
3605 type='low'))
3607 if self.highpass is not None:
3608 resps.append(but(
3609 order=4,
3610 corner=self.highpass,
3611 type='high'))
3613 resp = multres(resps)
3614 self.tf_cache[key] = \
3615 resp.evaluate(freqs)
3617 filtered_data = num.fft.irfft(
3618 fdata*self.tf_cache[key]
3619 )[:trace.data_len()]
3621 retrended_data = retrend(
3622 it, filtered_data, m, b)
3624 trace.set_ydata(retrended_data)
3626 else:
3628 if ads and self.lowpass is not None:
3629 while trace.deltat \
3630 < min_deltat_wo_decimate:
3632 trace.downsample(2, demean=False)
3634 fmax = 0.5/trace.deltat
3635 if not lphp and (
3636 self.lowpass is not None
3637 and self.highpass is not None
3638 and self.lowpass < fmax
3639 and self.highpass < fmax
3640 and self.highpass < self.lowpass):
3642 trace.bandpass(
3643 2, self.highpass, self.lowpass)
3644 else:
3645 if self.lowpass is not None:
3646 if self.lowpass < 0.5/trace.deltat:
3647 trace.lowpass(
3648 4, self.lowpass,
3649 demean=False)
3651 if self.highpass is not None:
3652 if self.lowpass is None \
3653 or self.highpass \
3654 < self.lowpass:
3656 if self.highpass < \
3657 0.5/trace.deltat:
3658 trace.highpass(
3659 4, self.highpass,
3660 demean=False)
3662 processed_traces.append(trace)
3664 if self.rotate != 0.0:
3665 phi = self.rotate/180.*math.pi
3666 cphi = math.cos(phi)
3667 sphi = math.sin(phi)
3668 for a in processed_traces:
3669 for b in processed_traces:
3670 if (a.network == b.network
3671 and a.station == b.station
3672 and a.location == b.location
3673 and ((a.channel.lower().endswith('n')
3674 and b.channel.lower().endswith('e'))
3675 or (a.channel.endswith('1')
3676 and b.channel.endswith('2')))
3677 and abs(a.deltat-b.deltat) < a.deltat*0.001
3678 and abs(a.tmin-b.tmin) < a.deltat*0.01 and
3679 len(a.get_ydata()) == len(b.get_ydata())):
3681 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi
3682 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi
3683 a.set_ydata(aydata)
3684 b.set_ydata(bydata)
3686 processed_traces = self.post_process_hooks(processed_traces)
3688 self.cached_processed_traces = processed_traces
3689 self.cached_vec = vec
3691 chopped_traces = []
3692 for trace in processed_traces:
3693 chop_tmin = tmin_ - trace.deltat*4
3694 chop_tmax = tmax_ + trace.deltat*4
3696 try:
3697 ctrace = trace.chop(
3698 chop_tmin, chop_tmax,
3699 inplace=False)
3701 except pyrocko.trace.NoData:
3702 continue
3704 if ctrace.data_len() < 2:
3705 continue
3707 chopped_traces.append(ctrace)
3709 self.timer_cutout.stop()
3710 return chopped_traces
3712 def pre_process_hooks(self, traces):
3713 for snuffling in self.snufflings:
3714 if snuffling._pre_process_hook_enabled:
3715 traces = snuffling.pre_process_hook(traces)
3717 return traces
3719 def post_process_hooks(self, traces):
3720 for snuffling in self.snufflings:
3721 if snuffling._post_process_hook_enabled:
3722 traces = snuffling.post_process_hook(traces)
3724 return traces
3726 def visible_length_change(self, ignore=None):
3727 for menuitem, vlen in self.menuitems_visible_length:
3728 if menuitem.isChecked():
3729 self.visible_length = vlen
3731 def scaling_base_change(self, ignore=None):
3732 for menuitem, scaling_base in self.menuitems_scaling_base:
3733 if menuitem.isChecked():
3734 self.scaling_base = scaling_base
3736 def scalingmode_change(self, ignore=None):
3737 for menuitem, scaling_key in self.menuitems_scaling:
3738 if menuitem.isChecked():
3739 self.scaling_key = scaling_key
3740 self.update()
3742 def apply_scaling_hooks(self, data_ranges):
3743 for k in sorted(self.scaling_hooks.keys()):
3744 hook = self.scaling_hooks[k]
3745 hook(data_ranges)
3747 def viewmode_change(self, ignore=True):
3748 for item, mode in self.menuitems_viewmode:
3749 if item.isChecked():
3750 self.view_mode = mode
3751 break
3752 else:
3753 raise AttributeError('unknown view mode')
3755 items_waterfall_disabled = (
3756 self.menuitem_showscaleaxis,
3757 self.menuitem_showscalerange,
3758 self.menuitem_showzeroline,
3759 self.menuitem_colortraces,
3760 self.menuitem_cliptraces,
3761 *(itm[0] for itm in self.menuitems_visible_length)
3762 )
3764 if self.view_mode is ViewMode.Waterfall:
3765 self.parent().show_colorbar_ctrl(True)
3766 self.parent().show_gain_ctrl(False)
3768 for item in items_waterfall_disabled:
3769 item.setDisabled(True)
3771 self.visible_length = 180.
3772 else:
3773 self.parent().show_colorbar_ctrl(False)
3774 self.parent().show_gain_ctrl(True)
3776 for item in items_waterfall_disabled:
3777 item.setDisabled(False)
3779 self.visible_length_change()
3780 self.update()
3782 def set_scaling_hook(self, k, hook):
3783 self.scaling_hooks[k] = hook
3785 def remove_scaling_hook(self, k):
3786 del self.scaling_hooks[k]
3788 def remove_scaling_hooks(self):
3789 self.scaling_hooks = {}
3791 def s_sortingmode_change(self, ignore=None):
3792 for menuitem, valfunc in self.menuitems_ssorting:
3793 if menuitem.isChecked():
3794 self._ssort = valfunc
3796 self.sortingmode_change()
3798 def sortingmode_change(self, ignore=None):
3799 for menuitem, (gather, color) in self.menuitems_sorting:
3800 if menuitem.isChecked():
3801 self.set_gathering(gather, color)
3803 self.sortingmode_change_time = time.time()
3805 def lowpass_change(self, value, ignore=None):
3806 self.lowpass = value
3807 self.passband_check()
3808 self.tf_cache = {}
3809 self.update()
3811 def highpass_change(self, value, ignore=None):
3812 self.highpass = value
3813 self.passband_check()
3814 self.tf_cache = {}
3815 self.update()
3817 def passband_check(self):
3818 if self.highpass and self.lowpass \
3819 and self.highpass >= self.lowpass:
3821 self.message = 'Corner frequency of highpass larger than ' \
3822 'corner frequency of lowpass! I will now ' \
3823 'deactivate the highpass.'
3825 self.update_status()
3826 else:
3827 oldmess = self.message
3828 self.message = None
3829 if oldmess is not None:
3830 self.update_status()
3832 def gain_change(self, value, ignore):
3833 self.gain = value
3834 self.update()
3836 def rot_change(self, value, ignore):
3837 self.rotate = value
3838 self.update()
3840 def waterfall_cmap_change(self, cmap):
3841 self.waterfall_cmap = cmap
3842 self.update()
3844 def waterfall_clip_change(self, clip_min, clip_max):
3845 self.waterfall_clip_min = clip_min
3846 self.waterfall_clip_max = clip_max
3847 self.update()
3849 def waterfall_show_absolute_change(self, toggle):
3850 self.waterfall_show_absolute = toggle
3851 self.update()
3853 def waterfall_set_integrate(self, toggle):
3854 self.waterfall_integrate = toggle
3855 self.update()
3857 def set_selected_markers(self, markers):
3858 '''
3859 Set a list of markers selected
3861 :param markers: list of markers
3862 '''
3863 self.deselect_all()
3864 for m in markers:
3865 m.selected = True
3867 self.update()
3869 def deselect_all(self):
3870 for marker in self.markers:
3871 marker.selected = False
3873 def animate_picking(self):
3874 point = self.mapFromGlobal(qg.QCursor.pos())
3875 self.update_picking(point.x(), point.y(), doshift=True)
3877 def get_nslc_ids_for_track(self, ftrack):
3878 itrack = int(ftrack)
3879 return self.track_to_nslc_ids.get(itrack, [])
3881 def stop_picking(self, x, y, abort=False):
3882 if self.picking:
3883 self.update_picking(x, y, doshift=False)
3884 self.picking = None
3885 self.picking_down = None
3886 self.picking_timer.stop()
3887 self.picking_timer = None
3888 if not abort:
3889 self.add_marker(self.floating_marker)
3890 self.floating_marker.selected = True
3891 self.emit_selected_markers()
3893 self.floating_marker = None
3895 def start_picking(self, ignore):
3897 if not self.picking:
3898 self.deselect_all()
3899 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle)
3900 point = self.mapFromGlobal(qg.QCursor.pos())
3902 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0))
3903 self.picking.setGeometry(
3904 gpoint.x(), gpoint.y(), 1, self.height())
3905 t = self.time_projection.rev(point.x())
3907 ftrack = self.track_to_screen.rev(point.y())
3908 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3909 self.floating_marker = Marker(nslc_ids, t, t)
3910 self.floating_marker.selected = True
3912 self.picking_timer = qc.QTimer()
3913 self.picking_timer.timeout.connect(
3914 self.animate_picking)
3916 self.picking_timer.setInterval(50)
3917 self.picking_timer.start()
3919 def update_picking(self, x, y, doshift=False):
3920 if self.picking:
3921 mouset = self.time_projection.rev(x)
3922 dt = 0.0
3923 if mouset < self.tmin or mouset > self.tmax:
3924 if mouset < self.tmin:
3925 dt = -(self.tmin - mouset)
3926 else:
3927 dt = mouset - self.tmax
3928 ddt = self.tmax-self.tmin
3929 dt = max(dt, -ddt/10.)
3930 dt = min(dt, ddt/10.)
3932 x0 = x
3933 if self.picking_down is not None:
3934 x0 = self.time_projection(self.picking_down[0])
3936 w = abs(x-x0)
3937 x0 = min(x0, x)
3939 tmin, tmax = (
3940 self.time_projection.rev(x0),
3941 self.time_projection.rev(x0+w))
3943 tmin, tmax = (
3944 max(working_system_time_range[0], tmin),
3945 min(working_system_time_range[1], tmax))
3947 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0))
3949 self.picking.setGeometry(
3950 p1.x(), p1.y(), int(round(max(w, 1))), self.height())
3952 ftrack = self.track_to_screen.rev(y)
3953 nslc_ids = self.get_nslc_ids_for_track(ftrack)
3954 self.floating_marker.set(nslc_ids, tmin, tmax)
3956 if dt != 0.0 and doshift:
3957 self.interrupt_following()
3958 self.set_time_range(self.tmin+dt, self.tmax+dt)
3960 self.update()
3962 def update_status(self):
3964 if self.message is None:
3965 point = self.mapFromGlobal(qg.QCursor.pos())
3967 mouse_t = self.time_projection.rev(point.x())
3968 if not is_working_time(mouse_t):
3969 return
3971 if self.floating_marker:
3972 tmi, tma = (
3973 self.floating_marker.tmin,
3974 self.floating_marker.tmax)
3976 tt, ms = gmtime_x(tmi)
3978 if tmi == tma:
3979 message = mystrftime(
3980 fmt='Pick: %Y-%m-%d %H:%M:%S .%r',
3981 tt=tt, milliseconds=ms)
3982 else:
3983 srange = '%g s' % (tma-tmi)
3984 message = mystrftime(
3985 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange,
3986 tt=tt, milliseconds=ms)
3987 else:
3988 tt, ms = gmtime_x(mouse_t)
3990 message = mystrftime(fmt=None, tt=tt, milliseconds=ms)
3991 else:
3992 message = self.message
3994 sb = self.window().statusBar()
3995 sb.clearMessage()
3996 sb.showMessage(message)
3998 def set_sortingmode_change_delay_time(self, dt):
3999 self.sortingmode_change_delay_time = dt
4001 def sortingmode_change_delayed(self):
4002 now = time.time()
4003 return (
4004 self.sortingmode_change_delay_time is not None
4005 and now - self.sortingmode_change_time
4006 < self.sortingmode_change_delay_time)
4008 def set_visible_marker_kinds(self, kinds):
4009 self.deselect_all()
4010 self.visible_marker_kinds = tuple(kinds)
4011 self.emit_selected_markers()
4013 def following(self):
4014 return self.follow_timer is not None \
4015 and not self.following_interrupted()
4017 def interrupt_following(self):
4018 self.interactive_range_change_time = time.time()
4020 def following_interrupted(self, now=None):
4021 if now is None:
4022 now = time.time()
4023 return now - self.interactive_range_change_time \
4024 < self.interactive_range_change_delay_time
4026 def follow(self, tlen, interval=50, lapse=None, tmax_start=None):
4027 if tmax_start is None:
4028 tmax_start = time.time()
4029 self.show_all = False
4030 self.follow_time = tlen
4031 self.follow_timer = qc.QTimer(self)
4032 self.follow_timer.timeout.connect(
4033 self.follow_update)
4034 self.follow_timer.setInterval(interval)
4035 self.follow_timer.start()
4036 self.follow_started = time.time()
4037 self.follow_lapse = lapse
4038 self.follow_tshift = self.follow_started - tmax_start
4039 self.interactive_range_change_time = 0.0
4041 def unfollow(self):
4042 if self.follow_timer is not None:
4043 self.follow_timer.stop()
4044 self.follow_timer = None
4045 self.interactive_range_change_time = 0.0
4047 def follow_update(self):
4048 rnow = time.time()
4049 if self.follow_lapse is None:
4050 now = rnow
4051 else:
4052 now = self.follow_started + (rnow - self.follow_started) \
4053 * self.follow_lapse
4055 if self.following_interrupted(rnow):
4056 return
4057 self.set_time_range(
4058 now-self.follow_time-self.follow_tshift,
4059 now-self.follow_tshift)
4061 self.update()
4063 def myclose(self, return_tag=''):
4064 self.return_tag = return_tag
4065 self.window().close()
4067 def cleanup(self):
4068 self.about_to_close.emit()
4069 self.timer.stop()
4070 if self.follow_timer is not None:
4071 self.follow_timer.stop()
4073 for snuffling in list(self.snufflings):
4074 self.remove_snuffling(snuffling)
4076 def set_error_message(self, key, value):
4077 if value is None:
4078 if key in self.error_messages:
4079 del self.error_messages[key]
4080 else:
4081 self.error_messages[key] = value
4083 def inputline_changed(self, text):
4084 pass
4086 def inputline_finished(self, text):
4087 line = str(text)
4089 toks = line.split()
4090 clearit, hideit, error = False, True, None
4091 if len(toks) >= 1:
4092 command = toks[0].lower()
4094 try:
4095 quick_filter_commands = {
4096 'n': '%s.*.*.*',
4097 's': '*.%s.*.*',
4098 'l': '*.*.%s.*',
4099 'c': '*.*.*.%s'}
4101 if command in quick_filter_commands:
4102 if len(toks) >= 2:
4103 patterns = [
4104 quick_filter_commands[toks[0]] % pat
4105 for pat in toks[1:]]
4106 self.set_quick_filter_patterns(patterns, line)
4107 else:
4108 self.set_quick_filter_patterns(None)
4110 self.update()
4112 elif command in ('hide', 'unhide'):
4113 if len(toks) >= 2:
4114 patterns = []
4115 if len(toks) == 2:
4116 patterns = [toks[1]]
4117 elif len(toks) >= 3:
4118 x = {
4119 'n': '%s.*.*.*',
4120 's': '*.%s.*.*',
4121 'l': '*.*.%s.*',
4122 'c': '*.*.*.%s'}
4124 if toks[1] in x:
4125 patterns.extend(
4126 x[toks[1]] % tok for tok in toks[2:])
4128 for pattern in patterns:
4129 if command == 'hide':
4130 self.add_blacklist_pattern(pattern)
4131 else:
4132 self.remove_blacklist_pattern(pattern)
4134 elif command == 'unhide' and len(toks) == 1:
4135 self.clear_blacklist()
4137 clearit = True
4139 self.update()
4141 elif command == 'markers':
4142 if len(toks) == 2:
4143 if toks[1] == 'all':
4144 kinds = self.all_marker_kinds
4145 else:
4146 kinds = []
4147 for x in toks[1]:
4148 try:
4149 kinds.append(int(x))
4150 except Exception:
4151 pass
4153 self.set_visible_marker_kinds(kinds)
4155 elif len(toks) == 1:
4156 self.set_visible_marker_kinds(())
4158 self.update()
4160 elif command == 'scaling':
4161 if len(toks) == 2:
4162 hideit = False
4163 error = 'wrong number of arguments'
4165 if len(toks) >= 3:
4166 vmin, vmax = [
4167 pyrocko.model.float_or_none(x)
4168 for x in toks[-2:]]
4170 def upd(d, k, vmin, vmax):
4171 if k in d:
4172 if vmin is not None:
4173 d[k] = vmin, d[k][1]
4174 if vmax is not None:
4175 d[k] = d[k][0], vmax
4177 if len(toks) == 1:
4178 self.remove_scaling_hooks()
4180 elif len(toks) == 3:
4181 def hook(data_ranges):
4182 for k in data_ranges:
4183 upd(data_ranges, k, vmin, vmax)
4185 self.set_scaling_hook('_', hook)
4187 elif len(toks) == 4:
4188 pattern = toks[1]
4190 def hook(data_ranges):
4191 for k in pyrocko.util.match_nslcs(
4192 pattern, list(data_ranges.keys())):
4194 upd(data_ranges, k, vmin, vmax)
4196 self.set_scaling_hook(pattern, hook)
4198 elif command == 'goto':
4199 toks2 = line.split(None, 1)
4200 if len(toks2) == 2:
4201 arg = toks2[1]
4202 m = re.match(
4203 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d'
4204 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg)
4205 if m:
4206 tlen = None
4207 if not m.group(1):
4208 tlen = 12*32*24*60*60
4209 elif not m.group(2):
4210 tlen = 32*24*60*60
4211 elif not m.group(3):
4212 tlen = 24*60*60
4213 elif not m.group(4):
4214 tlen = 60*60
4215 elif not m.group(5):
4216 tlen = 60
4218 supl = '1970-01-01 00:00:00'
4219 if len(supl) > len(arg):
4220 arg = arg + supl[-(len(supl)-len(arg)):]
4221 t = pyrocko.util.str_to_time(arg)
4222 self.go_to_time(t, tlen=tlen)
4224 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg):
4225 supl = '00:00:00'
4226 if len(supl) > len(arg):
4227 arg = arg + supl[-(len(supl)-len(arg)):]
4228 tmin, tmax = self.get_time_range()
4229 sdate = pyrocko.util.time_to_str(
4230 tmin/2.+tmax/2., format='%Y-%m-%d')
4231 t = pyrocko.util.str_to_time(sdate + ' ' + arg)
4232 self.go_to_time(t)
4234 elif arg == 'today':
4235 self.go_to_time(
4236 day_start(
4237 time.time()), tlen=24*60*60)
4239 elif arg == 'yesterday':
4240 self.go_to_time(
4241 day_start(
4242 time.time()-24*60*60), tlen=24*60*60)
4244 else:
4245 self.go_to_event_by_name(arg)
4247 else:
4248 raise PileViewerMainException(
4249 'No such command: %s' % command)
4251 except PileViewerMainException as e:
4252 error = str(e)
4253 hideit = False
4255 return clearit, hideit, error
4257 return PileViewerMain
4260PileViewerMain = MakePileViewerMainClass(qw.QWidget)
4261GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget)
4264class LineEditWithAbort(qw.QLineEdit):
4266 aborted = qc.pyqtSignal()
4267 history_down = qc.pyqtSignal()
4268 history_up = qc.pyqtSignal()
4270 def keyPressEvent(self, key_event):
4271 if key_event.key() == qc.Qt.Key_Escape:
4272 self.aborted.emit()
4273 elif key_event.key() == qc.Qt.Key_Down:
4274 self.history_down.emit()
4275 elif key_event.key() == qc.Qt.Key_Up:
4276 self.history_up.emit()
4277 else:
4278 return qw.QLineEdit.keyPressEvent(self, key_event)
4281class PileViewer(qw.QFrame):
4282 '''
4283 PileViewerMain + Controls + Inputline
4284 '''
4286 def __init__(
4287 self, pile,
4288 ntracks_shown_max=20,
4289 marker_editor_sortable=True,
4290 use_opengl=None,
4291 panel_parent=None,
4292 *args):
4294 qw.QFrame.__init__(self, *args)
4296 layout = qw.QGridLayout()
4297 layout.setContentsMargins(0, 0, 0, 0)
4298 layout.setSpacing(0)
4300 self.menu = PileViewerMenuBar(self)
4302 if use_opengl is None:
4303 use_opengl = is_macos
4305 if use_opengl:
4306 self.viewer = GLPileViewerMain(
4307 pile,
4308 ntracks_shown_max=ntracks_shown_max,
4309 panel_parent=panel_parent,
4310 menu=self.menu)
4311 else:
4312 self.viewer = PileViewerMain(
4313 pile,
4314 ntracks_shown_max=ntracks_shown_max,
4315 panel_parent=panel_parent,
4316 menu=self.menu)
4318 self.marker_editor_sortable = marker_editor_sortable
4320 self.setFrameShape(qw.QFrame.StyledPanel)
4321 self.setFrameShadow(qw.QFrame.Sunken)
4323 self.input_area = qw.QFrame(self)
4324 ia_layout = qw.QGridLayout()
4325 ia_layout.setContentsMargins(11, 11, 11, 11)
4326 self.input_area.setLayout(ia_layout)
4328 self.inputline = LineEditWithAbort(self.input_area)
4329 self.inputline.returnPressed.connect(
4330 self.inputline_returnpressed)
4331 self.inputline.editingFinished.connect(
4332 self.inputline_finished)
4333 self.inputline.aborted.connect(
4334 self.inputline_aborted)
4336 self.inputline.history_down.connect(
4337 lambda: self.step_through_history(1))
4338 self.inputline.history_up.connect(
4339 lambda: self.step_through_history(-1))
4341 self.inputline.textEdited.connect(
4342 self.inputline_changed)
4344 self.inputline.setPlaceholderText(
4345 u'Quick commands: e.g. \'c HH?\' to select channels. '
4346 u'Use ↑ or ↓ to navigate.')
4347 self.inputline.setFocusPolicy(qc.Qt.ClickFocus)
4348 self.input_area.hide()
4349 self.history = None
4351 self.inputline_error_str = None
4353 self.inputline_error = qw.QLabel()
4354 self.inputline_error.hide()
4356 ia_layout.addWidget(self.inputline, 0, 0)
4357 ia_layout.addWidget(self.inputline_error, 1, 0)
4358 layout.addWidget(self.input_area, 0, 0, 1, 2)
4359 layout.addWidget(self.viewer, 1, 0)
4361 pb = Progressbars(self)
4362 layout.addWidget(pb, 2, 0, 1, 2)
4363 self.progressbars = pb
4365 scrollbar = qw.QScrollBar(qc.Qt.Vertical)
4366 self.scrollbar = scrollbar
4367 layout.addWidget(scrollbar, 1, 1)
4368 self.scrollbar.valueChanged.connect(
4369 self.scrollbar_changed)
4371 self.block_scrollbar_changes = False
4373 self.viewer.want_input.connect(
4374 self.inputline_show)
4375 self.viewer.tracks_range_changed.connect(
4376 self.tracks_range_changed)
4377 self.viewer.pile_has_changed_signal.connect(
4378 self.adjust_controls)
4379 self.viewer.about_to_close.connect(
4380 self.save_inputline_history)
4382 self.setLayout(layout)
4384 def cleanup(self):
4385 self.viewer.cleanup()
4387 def get_progressbars(self):
4388 return self.progressbars
4390 def inputline_show(self):
4391 if not self.history:
4392 self.load_inputline_history()
4394 self.input_area.show()
4395 self.inputline.setFocus(qc.Qt.OtherFocusReason)
4396 self.inputline.selectAll()
4398 def inputline_set_error(self, string):
4399 self.inputline_error_str = string
4400 self.inputline.setPalette(pyrocko.gui.util.get_err_palette())
4401 self.inputline.selectAll()
4402 self.inputline_error.setText(string)
4403 self.input_area.show()
4404 self.inputline_error.show()
4406 def inputline_clear_error(self):
4407 if self.inputline_error_str:
4408 self.inputline.setPalette(qw.QApplication.palette())
4409 self.inputline_error_str = None
4410 self.inputline_error.clear()
4411 self.inputline_error.hide()
4413 def inputline_changed(self, line):
4414 self.viewer.inputline_changed(str(line))
4415 self.inputline_clear_error()
4417 def inputline_returnpressed(self):
4418 line = str(self.inputline.text())
4419 clearit, hideit, error = self.viewer.inputline_finished(line)
4421 if error:
4422 self.inputline_set_error(error)
4424 line = line.strip()
4426 if line != '' and not error:
4427 if not (len(self.history) >= 1 and line == self.history[-1]):
4428 self.history.append(line)
4430 if clearit:
4432 self.inputline.blockSignals(True)
4433 qpat, qinp = self.viewer.get_quick_filter_patterns()
4434 if qpat is None:
4435 self.inputline.clear()
4436 else:
4437 self.inputline.setText(qinp)
4438 self.inputline.blockSignals(False)
4440 if hideit and not error:
4441 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4442 self.input_area.hide()
4444 self.hist_ind = len(self.history)
4446 def inputline_aborted(self):
4447 '''
4448 Hide the input line.
4449 '''
4450 self.viewer.setFocus(qc.Qt.OtherFocusReason)
4451 self.hist_ind = len(self.history)
4452 self.input_area.hide()
4454 def save_inputline_history(self):
4455 '''
4456 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf"
4457 '''
4458 if not self.history:
4459 return
4461 conf = pyrocko.config
4462 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4463 with open(fn_hist, 'w') as f:
4464 i = min(100, len(self.history))
4465 for c in self.history[-i:]:
4466 f.write('%s\n' % c)
4468 def load_inputline_history(self):
4469 '''
4470 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf"
4471 '''
4472 conf = pyrocko.config
4473 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history'))
4474 if not os.path.exists(fn_hist):
4475 with open(fn_hist, 'w+') as f:
4476 f.write('\n')
4478 with open(fn_hist, 'r') as f:
4479 self.history = [line.strip() for line in f.readlines()]
4481 self.hist_ind = len(self.history)
4483 def step_through_history(self, ud=1):
4484 '''
4485 Step through input line history and set the input line text.
4486 '''
4487 n = len(self.history)
4488 self.hist_ind += ud
4489 self.hist_ind %= (n + 1)
4490 if len(self.history) != 0 and self.hist_ind != n:
4491 self.inputline.setText(self.history[self.hist_ind])
4492 else:
4493 self.inputline.setText('')
4495 def inputline_finished(self):
4496 pass
4498 def tracks_range_changed(self, ntracks, ilo, ihi):
4499 if self.block_scrollbar_changes:
4500 return
4502 self.scrollbar.blockSignals(True)
4503 self.scrollbar.setPageStep(ihi-ilo)
4504 vmax = max(0, ntracks-(ihi-ilo))
4505 self.scrollbar.setRange(0, vmax)
4506 self.scrollbar.setValue(ilo)
4507 self.scrollbar.setHidden(vmax == 0)
4508 self.scrollbar.blockSignals(False)
4510 def scrollbar_changed(self, value):
4511 self.block_scrollbar_changes = True
4512 ilo = value
4513 ihi = ilo + self.scrollbar.pageStep()
4514 self.viewer.set_tracks_range((ilo, ihi))
4515 self.block_scrollbar_changes = False
4516 self.update_contents()
4518 def controls(self):
4519 frame = qw.QFrame(self)
4520 layout = qw.QGridLayout()
4521 frame.setLayout(layout)
4523 minfreq = 0.001
4524 maxfreq = 1000.0
4525 self.lowpass_control = ValControl(high_is_none=True)
4526 self.lowpass_control.setup(
4527 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0)
4528 self.highpass_control = ValControl(low_is_none=True)
4529 self.highpass_control.setup(
4530 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1)
4531 self.gain_control = ValControl()
4532 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2)
4533 self.rot_control = LinValControl()
4534 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3)
4535 self.colorbar_control = ColorbarControl(self)
4537 self.lowpass_control.valchange.connect(
4538 self.viewer.lowpass_change)
4539 self.highpass_control.valchange.connect(
4540 self.viewer.highpass_change)
4541 self.gain_control.valchange.connect(
4542 self.viewer.gain_change)
4543 self.rot_control.valchange.connect(
4544 self.viewer.rot_change)
4545 self.colorbar_control.cmap_changed.connect(
4546 self.viewer.waterfall_cmap_change
4547 )
4548 self.colorbar_control.clip_changed.connect(
4549 self.viewer.waterfall_clip_change
4550 )
4551 self.colorbar_control.show_absolute_toggled.connect(
4552 self.viewer.waterfall_show_absolute_change
4553 )
4554 self.colorbar_control.show_integrate_toggled.connect(
4555 self.viewer.waterfall_set_integrate
4556 )
4558 for icontrol, control in enumerate((
4559 self.highpass_control,
4560 self.lowpass_control,
4561 self.gain_control,
4562 self.rot_control,
4563 self.colorbar_control)):
4565 for iwidget, widget in enumerate(control.widgets()):
4566 layout.addWidget(widget, icontrol, iwidget)
4568 spacer = qw.QSpacerItem(
4569 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
4570 layout.addItem(spacer, 4, 0, 1, 3)
4572 self.adjust_controls()
4573 self.viewer.viewmode_change(ViewMode.Wiggle)
4574 return frame
4576 def marker_editor(self):
4577 editor = pyrocko.gui.marker_editor.MarkerEditor(
4578 self, sortable=self.marker_editor_sortable)
4580 editor.set_viewer(self.get_view())
4581 editor.get_marker_model().dataChanged.connect(
4582 self.update_contents)
4583 return editor
4585 def adjust_controls(self):
4586 dtmin, dtmax = self.viewer.content_deltat_range()
4587 maxfreq = 0.5/dtmin
4588 minfreq = (0.5/dtmax)*0.001
4589 self.lowpass_control.set_range(minfreq, maxfreq)
4590 self.highpass_control.set_range(minfreq, maxfreq)
4592 def setup_snufflings(self):
4593 self.viewer.setup_snufflings()
4595 def get_view(self):
4596 return self.viewer
4598 def update_contents(self):
4599 self.viewer.update()
4601 def get_pile(self):
4602 return self.viewer.get_pile()
4604 def show_colorbar_ctrl(self, show):
4605 for w in self.colorbar_control.widgets():
4606 w.setVisible(show)
4608 def show_gain_ctrl(self, show):
4609 for w in self.gain_control.widgets():
4610 w.setVisible(show)