You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pyrocko/src/gui/pile_viewer.py

4339 lines
150 KiB
Python

# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------
from __future__ import absolute_import, print_function
import sys
import os
import time
import calendar
import datetime
import re
import math
import logging
import operator
import copy
from itertools import groupby
import numpy as num
import pyrocko.model
import pyrocko.pile
import pyrocko.shadow_pile
import pyrocko.trace
import pyrocko.util
import pyrocko.plot
import pyrocko.gui.snuffling
import pyrocko.gui.snufflings
import pyrocko.gui.marker_editor
from pyrocko.util import hpfloat, gmtime_x, mystrftime
from .marker import associate_phases_to_events, MarkerOneNSLCRequired
from .util import (ValControl, LinValControl, Marker, EventMarker,
PhaseMarker, make_QPolygonF, draw_label, Label,
Progressbars)
from .qt_compat import qc, qg, qw, qgl, qsvg, use_pyqt5
import scipy.stats as sstats
import platform
try:
newstr = unicode
except NameError:
newstr = str
def fnpatch(x):
if use_pyqt5:
return x
else:
return x, None
if sys.version_info[0] >= 3:
qc.QString = str
qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \
qw.QFileDialog.DontUseSheet
if platform.mac_ver() != ('', ('', '', ''), ''):
macosx = True
else:
macosx = False
logger = logging.getLogger('pyrocko.gui.pile_viewer')
def detrend(x, y):
slope, offset, _, _, _ = sstats.linregress(x, y)
y_detrended = y - slope * x - offset
return y_detrended, slope, offset
def retrend(x, y_detrended, slope, offset):
return x * slope + y_detrended + offset
class Global(object):
appOnDemand = None
class NSLC(object):
def __init__(self, n, s, l=None, c=None): # noqa
self.network = n
self.station = s
self.location = l
self.channel = c
class m_float(float):
def __str__(self):
if abs(self) >= 10000.:
return '%g km' % round(self/1000., 0)
elif abs(self) >= 1000.:
return '%g km' % round(self/1000., 1)
else:
return '%.5g m' % self
def __lt__(self, other):
if other is None:
return True
return float(self) < float(other)
def __gt__(self, other):
if other is None:
return False
return float(self) > float(other)
def m_float_or_none(x):
if x is None:
return None
else:
return m_float(x)
def make_chunks(items):
'''
Split a list of integers into sublists of consecutive elements.
'''
return [list(map(operator.itemgetter(1), g)) for k, g in groupby(
enumerate(items), (lambda x: x[1]-x[0]))]
class deg_float(float):
def __str__(self):
return '%4.0f' % self
def deg_float_or_none(x):
if x is None:
return None
else:
return deg_float(x)
class sector_int(int):
def __str__(self):
return '[%i]' % self
def num_to_html(num):
snum = '%g' % num
m = re.match(r'(.+)[eE]([+-]?\d+)$', snum)
if m:
snum = m.group(1) + ' &times; 10<sup>%i</sup>' % int(m.group(2))
return snum
gap_lap_tolerance = 5.
class Timer(object):
def __init__(self):
self._start = None
self._stop = None
def start(self):
self._start = os.times()
def stop(self):
self._stop = os.times()
def get(self):
a = self._start
b = self._stop
if a is not None and b is not None:
return tuple([b[i] - a[i] for i in range(5)])
else:
return tuple([0.] * 5)
def __sub__(self, other):
a = self.get()
b = other.get()
return tuple([a[i] - b[i] for i in range(5)])
class Integrator(pyrocko.shadow_pile.ShadowPile):
def process(self, iblock, tmin, tmax, traces):
for trace in traces:
trace.ydata = trace.ydata - trace.ydata.mean()
trace.ydata = num.cumsum(trace.ydata)
return traces
class ObjectStyle(object):
def __init__(self, frame_pen, fill_brush):
self.frame_pen = frame_pen
self.fill_brush = fill_brush
box_styles = []
box_alpha = 100
for color in 'orange skyblue butter chameleon chocolate plum ' \
'scarletred'.split():
box_styles.append(ObjectStyle(
qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])),
qg.QBrush(qg.QColor(
*(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))),
))
sday = 60*60*24. # \
smonth = 60*60*24*30. # | only used as approx. intervals...
syear = 60*60*24*365. # /
acceptable_tincs = num.array([
1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3,
60*60*6, 60*60*12, sday, smonth, syear], dtype=float)
working_system_time_range = \
pyrocko.util.working_system_time_range()
initial_time_range = []
try:
initial_time_range.append(
calendar.timegm((1950, 1, 1, 0, 0, 0)))
except Exception:
initial_time_range.append(working_system_time_range[0])
try:
initial_time_range.append(
calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0)))
except Exception:
initial_time_range.append(working_system_time_range[1])
def is_working_time(t):
return working_system_time_range[0] <= t and \
t <= working_system_time_range[1]
def fancy_time_ax_format(inc):
l0_fmt_brief = ''
l2_fmt = ''
l2_trig = 0
if inc < 0.000001:
l0_fmt = '.%n'
l0_center = False
l1_fmt = '%H:%M:%S'
l1_trig = 6
l2_fmt = '%b %d, %Y'
l2_trig = 3
elif inc < 0.001:
l0_fmt = '.%u'
l0_center = False
l1_fmt = '%H:%M:%S'
l1_trig = 6
l2_fmt = '%b %d, %Y'
l2_trig = 3
elif inc < 1:
l0_fmt = '.%r'
l0_center = False
l1_fmt = '%H:%M:%S'
l1_trig = 6
l2_fmt = '%b %d, %Y'
l2_trig = 3
elif inc < 60:
l0_fmt = '%H:%M:%S'
l0_center = False
l1_fmt = '%b %d, %Y'
l1_trig = 3
elif inc < 3600:
l0_fmt = '%H:%M'
l0_center = False
l1_fmt = '%b %d, %Y'
l1_trig = 3
elif inc < sday:
l0_fmt = '%H:%M'
l0_center = False
l1_fmt = '%b %d, %Y'
l1_trig = 3
elif inc < smonth:
l0_fmt = '%a %d'
l0_fmt_brief = '%d'
l0_center = True
l1_fmt = '%b, %Y'
l1_trig = 2
elif inc < syear:
l0_fmt = '%b'
l0_center = True
l1_fmt = '%Y'
l1_trig = 1
else:
l0_fmt = '%Y'
l0_center = False
l1_fmt = ''
l1_trig = 0
return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig
def day_start(timestamp):
tt = time.gmtime(int(timestamp))
tts = tt[0:3] + (0, 0, 0) + tt[6:9]
return calendar.timegm(tts)
def month_start(timestamp):
tt = time.gmtime(int(timestamp))
tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9]
return calendar.timegm(tts)
def year_start(timestamp):
tt = time.gmtime(int(timestamp))
tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9]
return calendar.timegm(tts)
def time_nice_value(inc0):
if inc0 < acceptable_tincs[0]:
return pyrocko.plot.nice_value(inc0)
elif inc0 > acceptable_tincs[-1]:
return pyrocko.plot.nice_value(inc0/syear)*syear
else:
i = num.argmin(num.abs(acceptable_tincs-inc0))
return acceptable_tincs[i]
class TimeScaler(pyrocko.plot.AutoScaler):
def __init__(self):
pyrocko.plot.AutoScaler.__init__(self)
self.mode = 'min-max'
def make_scale(self, data_range):
assert self.mode in ('min-max', 'off'), \
'mode must be "min-max" or "off" for TimeScaler'
data_min = min(data_range)
data_max = max(data_range)
is_reverse = (data_range[0] > data_range[1])
mi, ma = data_min, data_max
nmi = mi
if self.mode != 'off':
nmi = mi - self.space*(ma-mi)
nma = ma
if self.mode != 'off':
nma = ma + self.space*(ma-mi)
mi, ma = nmi, nma
if mi == ma and self.mode != 'off':
mi -= 1.0
ma += 1.0
mi = max(working_system_time_range[0], mi)
ma = min(working_system_time_range[1], ma)
# make nice tick increment
if self.inc is not None:
inc = self.inc
else:
if self.approx_ticks > 0.:
inc = time_nice_value((ma-mi)/self.approx_ticks)
else:
inc = time_nice_value((ma-mi)*10.)
if inc == 0.0:
inc = 1.0
if is_reverse:
return ma, mi, -inc
else:
return mi, ma, inc
def make_ticks(self, data_range):
mi, ma, inc = self.make_scale(data_range)
is_reverse = False
if inc < 0:
mi, ma, inc = ma, mi, -inc
is_reverse = True
ticks = []
if inc < sday:
mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
if inc < 0.001:
mi_day = hpfloat(mi_day)
base = mi_day+num.ceil((mi-mi_day)/inc)*inc
if inc < 0.001:
base = hpfloat(base)
base_day = mi_day
i = 0
while True:
tick = base+i*inc
if tick > ma:
break
tick_day = day_start(tick)
if tick_day > base_day:
base_day = tick_day
base = base_day
i = 0
else:
ticks.append(tick)
i += 1
elif inc < smonth:
mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5))
dt_base = datetime.datetime(*time.gmtime(mi_day)[:6])
delta = datetime.timedelta(days=int(round(inc/sday)))
if mi_day == mi:
dt_base += delta
i = 0
while True:
current = dt_base + i*delta
tick = calendar.timegm(current.timetuple())
if tick > ma:
break
ticks.append(tick)
i += 1
elif inc < syear:
mi_month = month_start(max(
mi, working_system_time_range[0]+smonth*1.5))
y, m = time.gmtime(mi_month)[:2]
while True:
tick = calendar.timegm((y, m, 1, 0, 0, 0))
m += 1
if m > 12:
y, m = y+1, 1
if tick > ma:
break
if tick >= mi:
ticks.append(tick)
else:
mi_year = year_start(max(
mi, working_system_time_range[0]+syear*1.5))
incy = int(round(inc/syear))
y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy)
while True:
tick = calendar.timegm((y, 1, 1, 0, 0, 0))
y += incy
if tick > ma:
break
if tick >= mi:
ticks.append(tick)
if is_reverse:
ticks.reverse()
return ticks, inc
def need_l1_tick(tt, ms, l1_trig):
return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0
def tick_to_labels(tick, inc):
tt, ms = gmtime_x(tick)
l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
fancy_time_ax_format(inc)
l0 = mystrftime(l0_fmt, tt, ms)
l0_brief = mystrftime(l0_fmt_brief, tt, ms)
l1, l2 = None, None
if need_l1_tick(tt, ms, l1_trig):
l1 = mystrftime(l1_fmt, tt, ms)
if need_l1_tick(tt, ms, l2_trig):
l2 = mystrftime(l2_fmt, tt, ms)
return l0, l0_brief, l0_center, l1, l2
def l1_l2_tick(tick, inc):
tt, ms = gmtime_x(tick)
l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \
fancy_time_ax_format(inc)
l1 = mystrftime(l1_fmt, tt, ms)
l2 = mystrftime(l2_fmt, tt, ms)
return l1, l2
class TimeAx(TimeScaler):
def __init__(self, *args):
TimeScaler.__init__(self, *args)
def drawit(self, p, xprojection, yprojection):
pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1)
p.setPen(pen)
font = qg.QFont()
font.setBold(True)
p.setFont(font)
fm = p.fontMetrics()
ticklen = 10
pad = 10
tmin, tmax = xprojection.get_in_range()
ticks, inc = self.make_ticks((tmin, tmax))
l1_hits = 0
l2_hits = 0
vmin, vmax = yprojection(0), yprojection(ticklen)
uumin, uumax = xprojection.get_out_range()
first_tick_with_label = None
for tick in ticks:
umin = xprojection(tick)
umin_approx_next = xprojection(tick+inc)
umax = xprojection(tick)
pinc_approx = umin_approx_next - umin
p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax))
l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc)
if tick == 0.0 and tmax - tmin < 3600*24:
# hide year at epoch (we assume that synthetic data is shown)
if l2:
l2 = None
elif l1:
l1 = None
if l0_center:
ushift = (umin_approx_next-umin)/2.
else:
ushift = 0.
for l0x in (l0, l0_brief, ''):
label0 = l0x
rect0 = fm.boundingRect(label0)
if rect0.width() <= pinc_approx*0.9:
break
if uumin+pad < umin-rect0.width()/2.+ushift and \
umin+rect0.width()/2.+ushift < uumax-pad:
if first_tick_with_label is None:
first_tick_with_label = tick
p.drawText(qc.QPointF(
umin-rect0.width()/2.+ushift,
vmin+rect0.height()+ticklen), label0)
if l1:
label1 = l1
rect1 = fm.boundingRect(label1)
if uumin+pad < umin-rect1.width()/2. and \
umin+rect1.width()/2. < uumax-pad:
p.drawText(qc.QPointF(
umin-rect1.width()/2.,
vmin+rect0.height()+rect1.height()+ticklen),
label1)
l1_hits += 1
if l2:
label2 = l2
rect2 = fm.boundingRect(label2)
if uumin+pad < umin-rect2.width()/2. and \
umin+rect2.width()/2. < uumax-pad:
p.drawText(qc.QPointF(
umin-rect2.width()/2.,
vmin+rect0.height()+rect1.height()+rect2.height() +
ticklen), label2)
l2_hits += 1
if first_tick_with_label is None:
first_tick_with_label = tmin
l1, l2 = l1_l2_tick(first_tick_with_label, inc)
if -3600.*25 < first_tick_with_label <= 3600.*25 and \
tmax - tmin < 3600*24:
# hide year at epoch (we assume that synthetic data is shown)
if l2:
l2 = None
elif l1:
l1 = None
if l1_hits == 0 and l1:
label1 = l1
rect1 = fm.boundingRect(label1)
p.drawText(qc.QPointF(
uumin+pad,
vmin+rect0.height()+rect1.height()+ticklen),
label1)
l1_hits += 1
if l2_hits == 0 and l2:
label2 = l2
rect2 = fm.boundingRect(label2)
p.drawText(qc.QPointF(
uumin+pad,
vmin+rect0.height()+rect1.height()+rect2.height()+ticklen),
label2)
v = yprojection(0)
p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v))
class Projection(object):
def __init__(self):
self.xr = 0., 1.
self.ur = 0., 1.
def set_in_range(self, xmin, xmax):
if xmax == xmin:
xmax = xmin + 1.
self.xr = xmin, xmax
def get_in_range(self):
return self.xr
def set_out_range(self, umin, umax):
if umax == umin:
umax = umin + 1.
self.ur = umin, umax
def get_out_range(self):
return self.ur
def __call__(self, x):
umin, umax = self.ur
xmin, xmax = self.xr
return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
def clipped(self, x):
umin, umax = self.ur
xmin, xmax = self.xr
return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
def rev(self, u):
umin, umax = self.ur
xmin, xmax = self.xr
return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
def copy(self):
return copy.copy(self)
def add_radiobuttongroup(menu, menudef, obj, target, default=None):
group = qw.QActionGroup(menu)
menuitems = []
for name, v in menudef:
k = qw.QAction(name, menu)
group.addAction(k)
menu.addAction(k)
k.setCheckable(True)
group.triggered.connect(target)
menuitems.append((k, v))
if default is not None:
if name.lower().replace(' ', '_') == default:
k.setChecked(True)
if default is None:
menuitems[0][0].setChecked(True)
return menuitems
def sort_actions(menu):
actions = menu.actions()
for action in actions:
menu.removeAction(action)
actions.sort(key=lambda x: newstr(x.text()))
help_action = [a for a in actions if a.text() == 'Snuffler Controls']
if help_action:
actions.insert(0, actions.pop(actions.index(help_action[0])))
for action in actions:
menu.addAction(action)
fkey_map = dict(zip(
(qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5,
qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10,
qc.Qt.Key_F11, qc.Qt.Key_F12),
range(12)))
class PileViewerMainException(Exception):
pass
def MakePileViewerMainClass(base):
class PileViewerMain(base):
want_input = qc.pyqtSignal()
about_to_close = qc.pyqtSignal()
pile_has_changed_signal = qc.pyqtSignal()
tracks_range_changed = qc.pyqtSignal(int, int, int)
begin_markers_add = qc.pyqtSignal(int, int)
end_markers_add = qc.pyqtSignal()
begin_markers_remove = qc.pyqtSignal(int, int)
end_markers_remove = qc.pyqtSignal()
marker_selection_changed = qc.pyqtSignal(list)
active_event_marker_changed = qc.pyqtSignal()
def __init__(self, pile, ntracks_shown_max, panel_parent, *args):
if base == qgl.QGLWidget:
from OpenGL import GL # noqa
base.__init__(
self, qgl.QGLFormat(qgl.QGL.SampleBuffers), *args)
else:
base.__init__(self, *args)
self.pile = pile
self.ax_height = 80
self.panel_parent = panel_parent
self.click_tolerance = 5
self.ntracks_shown_max = ntracks_shown_max
self.initial_ntracks_shown_max = ntracks_shown_max
self.ntracks = 0
self.show_all = True
self.shown_tracks_range = None
self.track_start = None
self.track_trange = None
self.lowpass = None
self.highpass = None
self.gain = 1.0
self.rotate = 0.0
self.picking_down = None
self.picking = None
self.floating_marker = None
self.markers = pyrocko.pile.Sorted([], 'tmin')
self.markers_deltat_max = 0.
self.n_selected_markers = 0
self.all_marker_kinds = (0, 1, 2, 3, 4, 5)
self.visible_marker_kinds = self.all_marker_kinds
self.active_event_marker = None
self.ignore_releases = 0
self.message = None
self.reloaded = False
self.pile_has_changed = False
self.config = pyrocko.config.config('snuffler')
self.tax = TimeAx()
self.setBackgroundRole(qg.QPalette.Base)
self.setAutoFillBackground(True)
poli = qw.QSizePolicy(
qw.QSizePolicy.Expanding,
qw.QSizePolicy.Expanding)
self.setSizePolicy(poli)
self.setMinimumSize(300, 200)
self.setFocusPolicy(qc.Qt.ClickFocus)
self.menu = qw.QMenu(self)
mi = qw.QAction('Open waveform files...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.open_waveforms)
mi = qw.QAction('Open waveform directory...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.open_waveform_directory)
mi = qw.QAction('Open station files...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.open_stations)
mi = qw.QAction('Open StationXML files...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.open_stations_xml)
mi = qw.QAction('Save markers...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.write_markers)
mi = qw.QAction('Save selected markers...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.write_selected_markers)
mi = qw.QAction('Open marker file...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.read_markers)
mi = qw.QAction('Open event file...', self.menu)
self.menu.addAction(mi)
mi.triggered.connect(self.read_events)
self.menu.addSeparator()
menudef = [
('Individual Scale',
lambda tr: tr.nslc_id),
('Common Scale',
lambda tr: None),
('Common Scale per Station',
lambda tr: (tr.network, tr.station)),
('Common Scale per Station Location',
lambda tr: (tr.network, tr.station, tr.location)),
('Common Scale per Component',
lambda tr: (tr.channel)),
]
self.menuitems_scaling = add_radiobuttongroup(
self.menu, menudef, self, self.scalingmode_change,
default=self.config.trace_scale)
self.scaling_key = self.menuitems_scaling[0][1]
self.scaling_hooks = {}
self.scalingmode_change()
self.menu.addSeparator()
menudef = [
('Scaling based on Minimum and Maximum', 'minmax'),
('Scaling based on Mean +- 2 x Std. Deviation', 2),
('Scaling based on Mean +- 4 x Std. Deviation', 4),
]
self.menuitems_scaling_base = add_radiobuttongroup(
self.menu, menudef, self, self.scaling_base_change)
self.scaling_base = self.menuitems_scaling_base[0][1]
self.menu.addSeparator()
def sector_dist(sta):
if sta.dist_m is None:
return None, None
else:
return (
sector_int(round((sta.azimuth+15.)/30.)),
m_float(sta.dist_m))
menudef = [
('Sort by Names',
lambda tr: ()),
('Sort by Distance',
lambda tr: self.station_attrib(
tr,
lambda sta: (m_float_or_none(sta.dist_m),),
lambda tr: (None,))),
('Sort by Azimuth',
lambda tr: self.station_attrib(
tr,
lambda sta: (deg_float_or_none(sta.azimuth),),
lambda tr: (None,))),
('Sort by Distance in 12 Azimuthal Blocks',
lambda tr: self.station_attrib(
tr,
sector_dist,
lambda tr: (None, None))),
('Sort by Backazimuth',
lambda tr: self.station_attrib(
tr,
lambda sta: (deg_float_or_none(sta.backazimuth),),
lambda tr: (None,))),
]
self.menuitems_ssorting = add_radiobuttongroup(
self.menu, menudef, self, self.s_sortingmode_change)
self._ssort = lambda tr: ()
self.menuitem_distances_3d = qw.QAction('3D distances', self.menu)
self.menuitem_distances_3d.setCheckable(True)
self.menuitem_distances_3d.setChecked(False)
self.menuitem_distances_3d.toggled.connect(
self.distances_3d_changed)
self.menu.addAction(self.menuitem_distances_3d)
self.menu.addSeparator()
menudef = [
('Subsort by Network, Station, Location, Channel',
(lambda tr: self.ssort(tr) + tr.nslc_id, # gathering
lambda a: a, # sorting
lambda tr: tr.location)), # coloring
('Subsort by Network, Station, Channel, Location',
(lambda tr: self.ssort(tr) + (
tr.network, tr.station, tr.channel, tr.location),
lambda a: a,
lambda tr: tr.channel)),
('Subsort by Station, Network, Channel, Location',
(lambda tr: self.ssort(tr) + (
tr.station, tr.network, tr.channel, tr.location),
lambda a: a,
lambda tr: tr.channel)),
('Subsort by Location, Network, Station, Channel',
(lambda tr: self.ssort(tr) + (
tr.location, tr.network, tr.station, tr.channel),
lambda a: a,
lambda tr: tr.channel)),
('Subsort by Channel, Network, Station, Location',
(lambda tr: self.ssort(tr) + (
tr.channel, tr.network, tr.station, tr.location),
lambda a: a,
lambda tr: (tr.network, tr.station, tr.location))),
('Subsort by Network, Station, Channel (Grouped by Location)',
(lambda tr: self.ssort(tr) + (
tr.network, tr.station, tr.channel),
lambda a: a,
lambda tr: tr.location)),
('Subsort by Station, Network, Channel (Grouped by Location)',
(lambda tr: self.ssort(tr) + (
tr.station, tr.network, tr.channel),
lambda a: a,
lambda tr: tr.location)),
]
self.menuitems_sorting = add_radiobuttongroup(
self.menu, menudef, self, self.sortingmode_change)
self.menu.addSeparator()
self.menuitem_antialias = qw.QAction('Antialiasing', self.menu)
self.menuitem_antialias.setCheckable(True)
self.menu.addAction(self.menuitem_antialias)
self.menuitem_liberal_fetch = qw.QAction(
'Liberal Fetch Optimization', self.menu)
self.menuitem_liberal_fetch.setCheckable(True)
self.menu.addAction(self.menuitem_liberal_fetch)
self.menuitem_cliptraces = qw.QAction('Clip Traces', self.menu)
self.menuitem_cliptraces.setCheckable(True)
self.menuitem_cliptraces.setChecked(self.config.clip_traces)
self.menu.addAction(self.menuitem_cliptraces)
self.menuitem_showboxes = qw.QAction('Show Boxes', self.menu)
self.menuitem_showboxes.setCheckable(True)
self.menuitem_showboxes.setChecked(
self.config.show_boxes)
self.menu.addAction(self.menuitem_showboxes)
self.menuitem_colortraces = qw.QAction('Color Traces', self.menu)
self.menuitem_colortraces.setCheckable(True)
self.menuitem_colortraces.setChecked(False)
self.menu.addAction(self.menuitem_colortraces)
self.menuitem_showscalerange = qw.QAction(
'Show Scale Ranges', self.menu)
self.menuitem_showscalerange.setCheckable(True)
self.menuitem_showscalerange.setChecked(
self.config.show_scale_ranges)
self.menu.addAction(self.menuitem_showscalerange)
self.menuitem_showscaleaxis = qw.QAction(
'Show Scale Axes', self.menu)
self.menuitem_showscaleaxis.setCheckable(True)
self.menuitem_showscaleaxis.setChecked(
self.config.show_scale_axes)
self.menu.addAction(self.menuitem_showscaleaxis)
self.menuitem_showzeroline = qw.QAction(
'Show Zero Lines', self.menu)
self.menuitem_showzeroline.setCheckable(True)
self.menu.addAction(self.menuitem_showzeroline)
self.menuitem_fixscalerange = qw.QAction(
'Fix Scale Ranges', self.menu)
self.menuitem_fixscalerange.setCheckable(True)
self.menu.addAction(self.menuitem_fixscalerange)
self.menuitem_allowdownsampling = qw.QAction(
'Allow Downsampling', self.menu)
self.menuitem_allowdownsampling.setCheckable(True)
self.menuitem_allowdownsampling.setChecked(True)
self.menu.addAction(self.menuitem_allowdownsampling)
self.menuitem_degap = qw.QAction('Allow Degapping', self.menu)
self.menuitem_degap.setCheckable(True)
self.menuitem_degap.setChecked(True)
self.menu.addAction(self.menuitem_degap)
self.menuitem_demean = qw.QAction('Demean', self.menu)
self.menuitem_demean.setCheckable(True)
self.menuitem_demean.setChecked(self.config.demean)
self.menu.addAction(self.menuitem_demean)
self.menuitem_fft_filtering = qw.QAction(
'FFT Filtering', self.menu)
self.menuitem_fft_filtering.setCheckable(True)
self.menuitem_fft_filtering.setChecked(False)
self.menu.addAction(self.menuitem_fft_filtering)
self.menuitem_lphp = qw.QAction(
'Bandpass is Lowpass + Highpass', self.menu)
self.menuitem_lphp.setCheckable(True)
self.menuitem_lphp.setChecked(True)
self.menu.addAction(self.menuitem_lphp)
self.menuitem_watch = qw.QAction('Watch Files', self.menu)
self.menuitem_watch.setCheckable(True)
self.menuitem_watch.setChecked(False)
self.menu.addAction(self.menuitem_watch)
self.visible_length_menu = qw.QMenu('Visible Length', self.menu)
menudef = [(x.key, x.value) for x in
self.config.visible_length_setting]
self.menuitems_visible_length = add_radiobuttongroup(
self.visible_length_menu, menudef, self,
self.visible_length_change)
self.visible_length = menudef[0][1]
self.menu.addMenu(self.visible_length_menu)
self.menu.addSeparator()
self.snufflings_menu = qw.QMenu('Run Snuffling', self.menu)
self.menu.addMenu(self.snufflings_menu)
self.toggle_panel_menu = qw.QMenu('Panels', self.menu)
self.menu.addMenu(self.toggle_panel_menu)
self.menuitem_reload = qw.QAction('Reload Snufflings', self.menu)
self.menu.addAction(self.menuitem_reload)
self.menuitem_reload.triggered.connect(
self.setup_snufflings)
self.menu.addSeparator()
# Disable ShadowPileTest
if False:
self.menuitem_test = qw.QAction('Test', self.menu)
self.menuitem_test.setCheckable(True)
self.menuitem_test.setChecked(False)
self.menu.addAction(self.menuitem_test)
self.menuitem_test.triggered.connect(
self.toggletest)
self.menuitem_print = qw.QAction('Print', self.menu)
self.menu.addAction(self.menuitem_print)
self.menuitem_print.triggered.connect(
self.printit)
self.menuitem_svg = qw.QAction('Save as SVG|PNG', self.menu)
self.menu.addAction(self.menuitem_svg)
self.menuitem_svg.triggered.connect(
self.savesvg)
self.snuffling_help_menu = qw.QMenu('Help', self.menu)
self.menu.addMenu(self.snuffling_help_menu)
self.menuitem_help = qw.QAction(
'Snuffler Controls', self.snuffling_help_menu)
self.snuffling_help_menu.addAction(self.menuitem_help)
self.menuitem_help.triggered.connect(self.help)
self.snuffling_help_menu.addSeparator()
self.menuitem_about = qw.QAction('About', self.menu)
self.menu.addAction(self.menuitem_about)
self.menuitem_about.triggered.connect(self.about)
self.menuitem_close = qw.QAction('Close', self.menu)
self.menu.addAction(self.menuitem_close)
self.menuitem_close.triggered.connect(self.myclose)
self.menu.addSeparator()
self.menu.triggered.connect(self.update)
self.time_projection = Projection()
self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax())
self.time_projection.set_out_range(0., self.width())
self.gather = None
self.trace_filter = None
self.quick_filter = None
self.quick_filter_patterns = None, None
self.blacklist = []
self.track_to_screen = Projection()
self.track_to_nslc_ids = {}
self.old_vec = None
self.old_processed_traces = None
self.timer = qc.QTimer(self)
self.timer.timeout.connect(self.periodical)
self.timer.setInterval(1000)
self.timer.start()
self.pile.add_listener(self)
self.trace_styles = {}
self.determine_box_styles()
self.setMouseTracking(True)
user_home_dir = os.path.expanduser('~')
self.snuffling_modules = {}
self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')]
self.default_snufflings = None
self.snufflings = []
self.stations = {}
self.timer_draw = Timer()
self.timer_cutout = Timer()
self.time_spent_painting = 0.0
self.time_last_painted = time.time()
self.interactive_range_change_time = 0.0
self.interactive_range_change_delay_time = 10.0
self.follow_timer = None
self.sortingmode_change_time = 0.0
self.sortingmode_change_delay_time = None
self.old_data_ranges = {}
self.error_messages = {}
self.return_tag = None
self.wheel_pos = 60
self.setAcceptDrops(True)
self._paths_to_load = []
self.tf_cache = {}
self.automatic_updates = True
self.closing = False
self.paint_timer = qc.QTimer(self)
self.paint_timer.timeout.connect(self.reset_updates)
self.paint_timer.setInterval(20)
self.paint_timer.start()
@qc.pyqtSlot()
def reset_updates(self):
if not self.updatesEnabled():
self.setUpdatesEnabled(True)
def fail(self, reason):
box = qw.QMessageBox(self)
box.setText(reason)
box.exec_()
def set_trace_filter(self, filter_func):
self.trace_filter = filter_func
self.sortingmode_change()
def update_trace_filter(self):
if self.blacklist:
def blacklist_func(tr):
return not pyrocko.util.match_nslc(
self.blacklist, tr.nslc_id)
else:
blacklist_func = None
if self.quick_filter is None and blacklist_func is None:
self.set_trace_filter(None)
elif self.quick_filter is None:
self.set_trace_filter(blacklist_func)
elif blacklist_func is None:
self.set_trace_filter(self.quick_filter)
else:
self.set_trace_filter(
lambda tr: blacklist_func(tr) and self.quick_filter(tr))
def set_quick_filter(self, filter_func):
self.quick_filter = filter_func
self.update_trace_filter()
def set_quick_filter_patterns(self, patterns, inputline=None):
if patterns is not None:
self.set_quick_filter(
lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id))
else:
self.set_quick_filter(None)
self.quick_filter_patterns = patterns, inputline
def get_quick_filter_patterns(self):
return self.quick_filter_patterns
def add_blacklist_pattern(self, pattern):
if pattern == 'empty':
keys = set(self.pile.nslc_ids)
trs = self.pile.all(
tmin=self.tmin,
tmax=self.tmax,
load_data=False,
degap=False)
for tr in trs:
if tr.nslc_id in keys:
keys.remove(tr.nslc_id)
for key in keys:
xpattern = '.'.join(key)
if xpattern not in self.blacklist:
self.blacklist.append(xpattern)
else:
if pattern in self.blacklist:
self.blacklist.remove(pattern)
self.blacklist.append(pattern)
logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
self.update_trace_filter()
def remove_blacklist_pattern(self, pattern):
if pattern in self.blacklist:
self.blacklist.remove(pattern)
else:
raise PileViewerMainException(
'Pattern not found in blacklist.')
logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist))
self.update_trace_filter()
def clear_blacklist(self):
self.blacklist = []
self.update_trace_filter()
def ssort(self, tr):
return self._ssort(tr)
def station_key(self, x):
return x.network, x.station
def station_keys(self, x):
return [
(x.network, x.station, x.location),
(x.network, x.station)]
def station_attrib(self, tr, getter, default_getter):
for sk in self.station_keys(tr):
if sk in self.stations:
station = self.stations[sk]
return getter(station)
return default_getter(tr)
def get_station(self, sk):
return self.stations[sk]
def has_station(self, station):
for sk in self.station_keys(station):
if sk in self.stations:
return True
return False
def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)):
return self.station_attrib(
tr, lambda sta: (sta.lat, sta.lon), default_getter)
def set_stations(self, stations):
self.stations = {}
self.add_stations(stations)
def add_stations(self, stations):
for station in stations:
for sk in self.station_keys(station):
self.stations[sk] = station
ev = self.get_active_event()
if ev:
self.set_origin(ev)
def add_event(self, event):
marker = EventMarker(event)
self.add_marker(marker)
def add_events(self, events):
markers = [EventMarker(e) for e in events]
self.add_markers(markers)
def set_event_marker_as_origin(self, ignore=None):
selected = self.selected_markers()
if not selected:
self.fail('An event marker must be selected.')
return
m = selected[0]
if not isinstance(m, EventMarker):
self.fail('Selected marker is not an event.')
return
self.set_active_event_marker(m)
def deactivate_event_marker(self):
if self.active_event_marker:
self.active_event_marker.active = False
self.active_event_marker_changed.emit()
self.active_event_marker = None
def set_active_event_marker(self, event_marker):
if self.active_event_marker:
self.active_event_marker.active = False
self.active_event_marker = event_marker
event_marker.active = True
event = event_marker.get_event()
self.set_origin(event)
self.active_event_marker_changed.emit()
def set_active_event(self, event):
for marker in self.markers:
if isinstance(marker, EventMarker):
if marker.get_event() is event:
self.set_active_event_marker(marker)
def get_active_event_marker(self):
return self.active_event_marker
def get_active_event(self):
m = self.get_active_event_marker()
if m is not None:
return m.get_event()
else:
return None
def get_active_markers(self):
emarker = self.get_active_event_marker()
if emarker is None:
return None, []
else:
ev = emarker.get_event()
pmarkers = [
m for m in self.markers
if isinstance(m, PhaseMarker) and m.get_event() is ev]
return emarker, pmarkers
def set_origin(self, location):
for station in self.stations.values():
station.set_event_relative_data(
location,
distance_3d=self.menuitem_distances_3d.isChecked())
self.sortingmode_change()
def distances_3d_changed(self, ignore):
self.set_event_marker_as_origin(ignore)
def toggletest(self, checked):
if checked:
sp = Integrator()
self.add_shadow_pile(sp)
else:
self.remove_shadow_piles()
def add_shadow_pile(self, shadow_pile):
shadow_pile.set_basepile(self.pile)
shadow_pile.add_listener(self)
self.pile = shadow_pile
def remove_shadow_piles(self):
self.pile = self.pile.get_basepile()
def iter_snuffling_modules(self):
pjoin = os.path.join
for path in self.snuffling_paths:
if not os.path.isdir(path):
os.mkdir(path)
for entry in os.listdir(path):
directory = path
fn = entry
d = pjoin(path, entry)
if os.path.isdir(d):
directory = d
if os.path.isfile(
os.path.join(directory, 'snuffling.py')):
fn = 'snuffling.py'
if not fn.endswith('.py'):
continue
name = fn[:-3]
if (directory, name) not in self.snuffling_modules:
self.snuffling_modules[directory, name] = \
pyrocko.gui.snuffling.SnufflingModule(
directory, name, self)
yield self.snuffling_modules[directory, name]
def setup_snufflings(self):
# user snufflings
for mod in self.iter_snuffling_modules():
try:
mod.load_if_needed()
except pyrocko.gui.snuffling.BrokenSnufflingModule as e:
logger.warning('Snuffling module "%s" is broken' % e)
# load the default snufflings on first run
if self.default_snufflings is None:
self.default_snufflings = pyrocko.gui\
.snufflings.__snufflings__()
for snuffling in self.default_snufflings:
self.add_snuffling(snuffling)
def set_panel_parent(self, panel_parent):
self.panel_parent = panel_parent
def get_panel_parent(self):
return self.panel_parent
def add_snuffling(self, snuffling, reloaded=False):
logger.debug('Adding snuffling %s' % snuffling.get_name())
snuffling.init_gui(
self, self.get_panel_parent(), self, reloaded=reloaded)
self.snufflings.append(snuffling)
self.update()
def remove_snuffling(self, snuffling):
snuffling.delete_gui()
self.update()
self.snufflings.remove(snuffling)
snuffling.pre_destroy()
def add_snuffling_menuitem(self, item):
self.snufflings_menu.addAction(item)
item.setParent(self.snufflings_menu)
sort_actions(self.snufflings_menu)
def remove_snuffling_menuitem(self, item):
self.snufflings_menu.removeAction(item)
def add_snuffling_help_menuitem(self, item):
self.snuffling_help_menu.addAction(item)
item.setParent(self.snuffling_help_menu)
sort_actions(self.snuffling_help_menu)
def remove_snuffling_help_menuitem(self, item):
self.snuffling_help_menu.removeAction(item)
def add_panel_toggler(self, item):
self.toggle_panel_menu.addAction(item)
item.setParent(self.toggle_panel_menu)
sort_actions(self.toggle_panel_menu)
def remove_panel_toggler(self, item):
self.toggle_panel_menu.removeAction(item)
def load(self, paths, regex=None, format='from_extension',
cache_dir=None, force_cache=False):
if cache_dir is None:
cache_dir = pyrocko.config.config().cache_dir
if isinstance(paths, str):
paths = [paths]
fns = pyrocko.util.select_files(
paths, selector=None, regex=regex, show_progress=False)
if not fns:
return
cache = pyrocko.pile.get_cache(cache_dir)
t = [time.time()]
def update_bar(label, value):
pbs = self.parent().get_progressbars()
if label.lower() == 'looking at files':
label = 'Looking at %i files' % len(fns)
else:
label = 'Scanning %i files' % len(fns)
return pbs.set_status(label, value)
def update_progress(label, i, n):
abort = False
qw.qApp.processEvents()
if n != 0:
perc = i*100/n
else:
perc = 100
abort |= update_bar(label, perc)
abort |= self.window().is_closing()
tnow = time.time()
if t[0] + 1. + self.time_spent_painting * 10. < tnow:
self.update()
t[0] = tnow
return abort
self.automatic_updates = False
self.pile.load_files(
sorted(fns),
filename_attributes=regex,
cache=cache,
fileformat=format,
show_progress=False,
update_progress=update_progress)
self.automatic_updates = True
self.update()
def load_queued(self):
if not self._paths_to_load:
return
paths = self._paths_to_load
self._paths_to_load = []
self.load(paths)
def load_soon(self, paths):
self._paths_to_load.extend(paths)
qc.QTimer.singleShot(200, self.load_queued)
def open_waveforms(self):
caption = 'Select one or more files to open'
fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
self, caption, options=qfiledialog_options))
if fns:
self.load(list(str(fn) for fn in fns))
def open_waveform_directory(self):
caption = 'Select directory to scan for waveform files'
dn = qw.QFileDialog.getExistingDirectory(
self, caption, options=qfiledialog_options)
if dn:
self.load([str(dn)])
def open_stations(self, fns=None):
caption = 'Select one or more files to open'
if not fns:
fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
self, caption, options=qfiledialog_options))
try:
stations = [pyrocko.model.load_stations(str(x)) for x in fns]
for stat in stations:
self.add_stations(stat)
except Exception as e:
self.fail('Failed to read station file: %s' % str(e))
def open_stations_xml(self, fns=None):
from pyrocko.io import stationxml
caption = 'Select one or more StationXML files to open'
if not fns:
fns, _ = fnpatch(qw.QFileDialog.getOpenFileNames(
self, caption, options=qfiledialog_options,
filter='StationXML *.xml (*.xml *.XML);;All files (*)'))
try:
stations = [
stationxml.load_xml(filename=str(x)).get_pyrocko_stations()
for x in fns]
for stat in stations:
self.add_stations(stat)
except Exception as e:
self.fail('Failed to read StationXML file: %s' % str(e))
def add_traces(self, traces):
if traces:
mtf = pyrocko.pile.MemTracesFile(None, traces)
self.pile.add_file(mtf)
ticket = (self.pile, mtf)
return ticket
else:
return (None, None)
def release_data(self, tickets):
for ticket in tickets:
pile, mtf = ticket
if pile is not None:
pile.remove_file(mtf)
def periodical(self):
if self.menuitem_watch.isChecked():
if self.pile.reload_modified():
self.update()
def get_pile(self):
return self.pile
def pile_changed(self, what):
self.pile_has_changed = True
self.pile_has_changed_signal.emit()
if self.automatic_updates:
self.update()
def set_gathering(self, gather=None, order=None, color=None):
if gather is None:
def gather(tr):
return tr.nslc_id
if order is None:
def order(a):
return a
if color is None:
def color(tr):
return tr.location
self.gather = gather
keys = self.pile.gather_keys(gather, self.trace_filter)
self.color_gather = color
self.color_keys = self.pile.gather_keys(color)
previous_ntracks = self.ntracks
self.set_ntracks(len(keys))
if self.shown_tracks_range is None or \
previous_ntracks == 0 or \
self.show_all:
low, high = 0, min(self.ntracks_shown_max, self.ntracks)
key_at_top = None
n = high-low
else:
low, high = self.shown_tracks_range
key_at_top = self.track_keys[low]
n = high-low
self.track_keys = sorted(keys, key=order)
if key_at_top is not None:
try:
ind = self.track_keys.index(key_at_top)
low = ind
high = low+n
except Exception:
pass
self.set_tracks_range((low, high))
self.key_to_row = dict(
[(key, i) for (i, key) in enumerate(self.track_keys)])
def inrange(x, r):
return r[0] <= x and x < r[1]
def trace_selector(trace):
gt = self.gather(trace)
return (
gt in self.key_to_row and
inrange(self.key_to_row[gt], self.shown_tracks_range))
if self.trace_filter is not None:
self.trace_selector = lambda x: \
self.trace_filter(x) and trace_selector(x)
else:
self.trace_selector = trace_selector
if self.tmin == working_system_time_range[0] and \
self.tmax == working_system_time_range[1] or \
self.show_all:
tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax()
if tmin is not None and tmax is not None:
tlen = (tmax - tmin)
tpad = tlen * 5./self.width()
self.set_time_range(tmin-tpad, tmax+tpad)
def set_time_range(self, tmin, tmax):
if tmin is None:
tmin = initial_time_range[0]
if tmax is None:
tmax = initial_time_range[1]
if tmin > tmax:
tmin, tmax = tmax, tmin
if tmin == tmax:
tmin -= 1.
tmax += 1.
tmin = max(working_system_time_range[0], tmin)
tmax = min(working_system_time_range[1], tmax)
min_deltat = self.content_deltat_range()[0]
if (tmax - tmin < min_deltat):
m = (tmin + tmax) / 2.
tmin = m - min_deltat/2.
tmax = m + min_deltat/2.
self.time_projection.set_in_range(tmin, tmax)
self.tmin, self.tmax = tmin, tmax
def get_time_range(self):
return self.tmin, self.tmax
def ypart(self, y):
if y < self.ax_height:
return -1
elif y > self.height()-self.ax_height:
return 1
else:
return 0
def time_fractional_digits(self):
min_deltat = self.content_deltat_range()[0]
return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2))
def write_markers(self, fn=None):
caption = "Choose a file name to write markers"
if not fn:
fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
self, caption, options=qfiledialog_options))
if fn:
try:
Marker.save_markers(
self.markers, fn,
fdigits=self.time_fractional_digits())
except Exception as e:
self.fail('Failed to write marker file: %s' % str(e))
def write_selected_markers(self, fn=None):
caption = "Choose a file name to write selected markers"
if not fn:
fn, _ = fnpatch(qw.QFileDialog.getSaveFileName(
self, caption, options=qfiledialog_options))
if fn:
try:
Marker.save_markers(
self.iter_selected_markers(),
fn,
fdigits=self.time_fractional_digits())
except Exception as e:
self.fail('Failed to write marker file: %s' % str(e))
def read_events(self, fn=None):
'''
Open QFileDialog to open, read and add
:py:class:`pyrocko.model.Event` instances and their marker
representation to the pile viewer.
'''
caption = "Selet one or more files to open"
if not fn:
fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
self, caption, options=qfiledialog_options))
if fn:
try:
self.add_events(pyrocko.model.load_events(fn))
self.associate_phases_to_events()
except Exception as e:
self.fail('Failed to read event file: %s' % str(e))
def read_markers(self, fn=None):
'''
Open QFileDialog to open, read and add markers to the pile viewer.
'''
caption = "Selet one or more files to open"
if not fn:
fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
self, caption, options=qfiledialog_options))
if fn:
try:
self.add_markers(Marker.load_markers(fn))
self.associate_phases_to_events()
except Exception as e:
self.fail('Failed to read marker file: %s' % str(e))
def associate_phases_to_events(self):
associate_phases_to_events(self.markers)
def add_marker(self, marker):
# need index to inform QAbstactTableModel about upcoming change,
# but have to restore current state in order to not cause problems
self.markers.insert(marker)
i = self.markers.remove(marker)
self.begin_markers_add.emit(i, i)
self.markers.insert(marker)
self.end_markers_add.emit()
self.markers_deltat_max = max(
self.markers_deltat_max, marker.tmax - marker.tmin)
def add_markers(self, markers):
if not self.markers:
self.begin_markers_add.emit(0, len(markers) - 1)
self.markers.insert_many(markers)
self.end_markers_add.emit()
self.update_markers_deltat_max()
else:
for marker in markers:
self.add_marker(marker)
def update_markers_deltat_max(self):
if self.markers:
self.markers_deltat_max = max(
marker.tmax - marker.tmin for marker in self.markers)
def remove_marker(self, marker):
'''
Remove a ``marker`` from the :py:class:`PileViewer`.
:param marker: :py:class:`Marker` (or subclass) instance
'''
if marker is self.active_event_marker:
self.deactivate_event_marker()
try:
i = self.markers.index(marker)
self.begin_markers_remove.emit(i, i)
self.markers.remove_at(i)
self.end_markers_remove.emit()
except ValueError:
pass
def remove_markers(self, markers):
'''
Remove a list of ``markers`` from the :py:class:`PileViewer`.
:param markers: list of :py:class:`Marker` (or subclass)
instances
'''
if markers is self.markers:
markers = list(markers)
for marker in markers:
self.remove_marker(marker)
self.update_markers_deltat_max()
def remove_selected_markers(self):
def delete_segment(istart, iend):
self.begin_markers_remove.emit(istart, iend-1)
for _ in range(iend - istart):
self.markers.remove_at(istart)
self.end_markers_remove.emit()
istart = None
ipos = 0
markers = self.markers
nmarkers = len(self.markers)
while ipos < nmarkers:
marker = markers[ipos]
if marker.is_selected():
if marker is self.active_event_marker:
self.deactivate_event_marker()
if istart is None:
istart = ipos
else:
if istart is not None:
delete_segment(istart, ipos)
nmarkers -= ipos - istart
ipos = istart - 1
istart = None
ipos += 1
if istart is not None:
delete_segment(istart, ipos)
self.update_markers_deltat_max()
def selected_markers(self):
return [marker for marker in self.markers if marker.is_selected()]
def iter_selected_markers(self):
for marker in self.markers:
if marker.is_selected():
yield marker
def get_markers(self):
return self.markers
def mousePressEvent(self, mouse_ev):
self.show_all = False
point = self.mapFromGlobal(mouse_ev.globalPos())
if mouse_ev.button() == qc.Qt.LeftButton:
marker = self.marker_under_cursor(point.x(), point.y())
if self.picking:
if self.picking_down is None:
self.picking_down = (
self.time_projection.rev(mouse_ev.x()),
mouse_ev.y())
elif marker is not None:
if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier):
self.deselect_all()
marker.selected = True
self.emit_selected_markers()
self.update()
else:
self.track_start = mouse_ev.x(), mouse_ev.y()
self.track_trange = self.tmin, self.tmax
if mouse_ev.button() == qc.Qt.RightButton:
self.menu.exec_(qg.QCursor.pos())
self.update_status()
def mouseReleaseEvent(self, mouse_ev):
if self.ignore_releases:
self.ignore_releases -= 1
return
if self.picking:
self.stop_picking(mouse_ev.x(), mouse_ev.y())
self.emit_selected_markers()
if self.track_start:
self.update()
self.track_start = None
self.track_trange = None
self.update_status()
def mouseDoubleClickEvent(self, mouse_ev):
self.show_all = False
self.start_picking(None)
self.ignore_releases = 1
def mouseMoveEvent(self, mouse_ev):
self.setUpdatesEnabled(False)
point = self.mapFromGlobal(mouse_ev.globalPos())
if self.picking:
self.update_picking(point.x(), point.y())
elif self.track_start is not None:
x0, y0 = self.track_start
dx = (point.x() - x0)/float(self.width())
dy = (point.y() - y0)/float(self.height())
if self.ypart(y0) == 1:
dy = 0
tmin0, tmax0 = self.track_trange
scale = math.exp(-dy*5.)
dtr = scale*(tmax0-tmin0) - (tmax0-tmin0)
frac = x0/float(self.width())
dt = dx*(tmax0-tmin0)*scale
self.interrupt_following()
self.set_time_range(
tmin0 - dt - dtr*frac,
tmax0 - dt + dtr*(1.-frac))
self.update()
else:
self.hoovering(point.x(), point.y())
self.update_status()
def nslc_ids_under_cursor(self, x, y):
ftrack = self.track_to_screen.rev(y)
nslc_ids = self.get_nslc_ids_for_track(ftrack)
return nslc_ids
def marker_under_cursor(self, x, y):
mouset = self.time_projection.rev(x)
deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
relevant_nslc_ids = None
for marker in self.markers:
if marker.kind not in self.visible_marker_kinds:
continue
if (abs(mouset-marker.tmin) < deltat or
abs(mouset-marker.tmax) < deltat):
if relevant_nslc_ids is None:
relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
marker_nslc_ids = marker.get_nslc_ids()
if not marker_nslc_ids:
return marker
for nslc_id in marker_nslc_ids:
if nslc_id in relevant_nslc_ids:
return marker
def hoovering(self, x, y):
mouset = self.time_projection.rev(x)
deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width()
needupdate = False
haveone = False
relevant_nslc_ids = self.nslc_ids_under_cursor(x, y)
for marker in self.markers:
if marker.kind not in self.visible_marker_kinds:
continue
state = abs(mouset-marker.tmin) < deltat or \
abs(mouset-marker.tmax) < deltat and not haveone
if state:
xstate = False
marker_nslc_ids = marker.get_nslc_ids()
if not marker_nslc_ids:
xstate = True
for nslc in relevant_nslc_ids:
if marker.match_nslc(nslc):
xstate = True
state = xstate
if state:
haveone = True
oldstate = marker.is_alerted()
if oldstate != state:
needupdate = True
marker.set_alerted(state)
if state:
self.message = marker.hoover_message()
if not haveone:
self.message = None
if needupdate:
self.update()
def event(self, event):
if event.type() == qc.QEvent.KeyPress:
self.keyPressEvent(event)
return True
else:
return base.event(self, event)
def keyPressEvent(self, key_event):
self.show_all = False
dt = self.tmax - self.tmin
tmid = (self.tmin + self.tmax) / 2.
try:
keytext = str(key_event.text())
except UnicodeEncodeError:
return
if keytext == '?':
self.help()
elif keytext == ' ':
self.interrupt_following()
self.set_time_range(self.tmin+dt, self.tmax+dt)
elif key_event.key() == qc.Qt.Key_Up:
for m in self.selected_markers():
if isinstance(m, PhaseMarker):
if key_event.modifiers() & qc.Qt.ShiftModifier:
p = 0
else:
p = 1 if m.get_polarity() != 1 else None
m.set_polarity(p)
elif key_event.key() == qc.Qt.Key_Down:
for m in self.selected_markers():
if isinstance(m, PhaseMarker):
if key_event.modifiers() & qc.Qt.ShiftModifier:
p = 0
else:
p = -1 if m.get_polarity() != -1 else None
m.set_polarity(p)
elif keytext == 'b':
dt = self.tmax - self.tmin
self.interrupt_following()
self.set_time_range(self.tmin-dt, self.tmax-dt)
elif key_event.key() in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab):
self.interrupt_following()
tgo = None
class TraceDummy(object):
def __init__(self, marker):
self._marker = marker
@property
def nslc_id(self):
return self._marker.one_nslc()
def marker_to_itrack(marker):
try:
return self.key_to_row.get(
self.gather(TraceDummy(marker)), -1)
except MarkerOneNSLCRequired:
return -1
emarker, pmarkers = self.get_active_markers()
pmarkers = [
m for m in pmarkers if m.kind in self.visible_marker_kinds]
pmarkers.sort(key=lambda m: (
marker_to_itrack(m), (m.tmin + m.tmax) / 2.0))
if key_event.key() == qc.Qt.Key_Backtab:
pmarkers.reverse()
smarkers = self.selected_markers()
iselected = []
for sm in smarkers:
try:
iselected.append(pmarkers.index(sm))
except ValueError:
pass
if iselected:
icurrent = max(iselected) + 1
else:
icurrent = 0
if icurrent < len(pmarkers):
self.deselect_all()
cmarker = pmarkers[icurrent]
cmarker.selected = True
tgo = cmarker.tmin
if not self.tmin < tgo < self.tmax:
self.set_time_range(tgo-dt/2., tgo+dt/2.)
itrack = marker_to_itrack(cmarker)
if itrack != -1:
if itrack < self.shown_tracks_range[0]:
self.scroll_tracks(
- (self.shown_tracks_range[0] - itrack))
elif self.shown_tracks_range[1] <= itrack:
self.scroll_tracks(
itrack - self.shown_tracks_range[1]+1)
if itrack not in self.track_to_nslc_ids:
self.go_to_selection()
elif keytext in ('p', 'n', 'P', 'N'):
smarkers = self.selected_markers()
tgo = None
dir = str(keytext)
if smarkers:
tmid = smarkers[0].tmin
for smarker in smarkers:
if dir == 'n':
tmid = max(smarker.tmin, tmid)
else:
tmid = min(smarker.tmin, tmid)
tgo = tmid
if dir.lower() == 'n':
for marker in sorted(
self.markers,
key=operator.attrgetter('tmin')):
t = marker.tmin
if t > tmid and \
marker.kind in self.visible_marker_kinds and \
(dir == 'n' or
isinstance(marker, EventMarker)):
self.deselect_all()
marker.selected = True
tgo = t
break
else:
for marker in sorted(
self.markers,
key=operator.attrgetter('tmin'),
reverse=True):
t = marker.tmin
if t < tmid and \
marker.kind in self.visible_marker_kinds and \
(dir == 'p' or
isinstance(marker, EventMarker)):
self.deselect_all()
marker.selected = True
tgo = t
break
if tgo is not None:
self.interrupt_following()
self.set_time_range(tgo-dt/2., tgo+dt/2.)
elif keytext == 'q' or keytext == 'x':
self.myclose(keytext)
elif keytext == 'r':
if self.pile.reload_modified():
self.reloaded = True
elif keytext == 'R':
self.setup_snufflings()
elif key_event.key() == qc.Qt.Key_Backspace:
self.remove_selected_markers()
elif keytext == 'a':
for marker in self.markers:
if ((self.tmin <= marker.tmin <= self.tmax or
self.tmin <= marker.tmax <= self.tmax) and
marker.kind in self.visible_marker_kinds):
marker.selected = True
else:
marker.selected = False
elif keytext == 'A':
for marker in self.markers:
if marker.kind in self.visible_marker_kinds:
marker.selected = True
elif keytext == 'd':
self.deselect_all()
elif keytext == 'E':
self.deactivate_event_marker()
elif keytext == 'e':
markers = self.selected_markers()
event_markers_in_spe = [
marker for marker in markers
if not isinstance(marker, PhaseMarker)]
phase_markers = [
marker for marker in markers
if isinstance(marker, PhaseMarker)]
if len(event_markers_in_spe) == 1:
event_marker = event_markers_in_spe[0]
if not isinstance(event_marker, EventMarker):
nslcs = list(event_marker.nslc_ids)
lat, lon = 0.0, 0.0
old = self.get_active_event()
if len(nslcs) == 1:
lat, lon = self.station_latlon(NSLC(*nslcs[0]))
elif old is not None:
lat, lon = old.lat, old.lon
event_marker.convert_to_event_marker(lat, lon)
self.set_active_event_marker(event_marker)
event = event_marker.get_event()
for marker in phase_markers:
marker.set_event(event)
else:
for marker in event_markers_in_spe:
marker.convert_to_event_marker()
elif keytext in ('0', '1', '2', '3', '4', '5'):
for marker in self.selected_markers():
marker.set_kind(int(keytext))
self.emit_selected_markers()
elif key_event.key() in fkey_map:
self.handle_fkeys(key_event.key())
elif key_event.key() == qc.Qt.Key_Escape:
if self.picking:
self.stop_picking(0, 0, abort=True)
elif key_event.key() == qc.Qt.Key_PageDown:
self.scroll_tracks(
self.shown_tracks_range[1]-self.shown_tracks_range[0])
elif key_event.key() == qc.Qt.Key_PageUp:
self.scroll_tracks(
self.shown_tracks_range[0]-self.shown_tracks_range[1])
elif keytext == '+':
self.zoom_tracks(0., 1.)
elif keytext == '-':
self.zoom_tracks(0., -1.)
elif keytext == '=':
ntracks_shown = self.shown_tracks_range[1] - \
self.shown_tracks_range[0]
dtracks = self.initial_ntracks_shown_max - ntracks_shown
self.zoom_tracks(0., dtracks)
elif keytext == ':':
self.want_input.emit()
elif keytext == 'f':
if self.window().windowState() & qc.Qt.WindowFullScreen or \
self.window().windowState() & qc.Qt.WindowMaximized:
self.window().showNormal()
else:
if macosx:
self.window().showMaximized()
else:
self.window().showFullScreen()
elif keytext == 'g':
self.go_to_selection()
elif keytext == 'G':
self.go_to_selection(tight=True)
elif keytext == 'm':
self.toggle_marker_editor()
elif keytext == 'c':
self.toggle_main_controls()
elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right):
dir = 1
amount = 1
if key_event.key() == qc.Qt.Key_Left:
dir = -1
if key_event.modifiers() & qc.Qt.ShiftModifier:
amount = 10
self.nudge_selected_markers(dir*amount)
if keytext != '' and keytext in 'degaApPnN':
self.emit_selected_markers()
self.update()
self.update_status()
def handle_fkeys(self, key):
self.set_phase_kind(
self.selected_markers(),
fkey_map[key] + 1)
self.emit_selected_markers()
def emit_selected_markers(self):
ibounds = []
last_selected = False
for imarker, marker in enumerate(self.markers):
this_selected = marker.is_selected()
if this_selected != last_selected:
ibounds.append(imarker)
last_selected = this_selected
if last_selected:
ibounds.append(len(self.markers))
chunks = list(zip(ibounds[::2], ibounds[1::2]))
self.n_selected_markers = sum(
chunk[1] - chunk[0] for chunk in chunks)
self.marker_selection_changed.emit(chunks)
def toggle_marker_editor(self):
self.panel_parent.toggle_marker_editor()
def toggle_main_controls(self):
self.panel_parent.toggle_main_controls()
def nudge_selected_markers(self, npixels):
a, b = self.time_projection.ur
c, d = self.time_projection.xr
for marker in self.selected_markers():
if not isinstance(marker, EventMarker):
marker.tmin += npixels * (d-c)/b
marker.tmax += npixels * (d-c)/b
def about(self):
fn = pyrocko.util.data_file('snuffler.png')
with open(pyrocko.util.data_file('snuffler_about.html')) as f:
txt = f.read()
label = qw.QLabel(txt % {'logo': fn})
label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter)
self.show_doc('About', [label], target='tab')
def help(self):
class MyScrollArea(qw.QScrollArea):
def sizeHint(self):
s = qc.QSize()
s.setWidth(self.widget().sizeHint().width())
s.setHeight(self.widget().sizeHint().height())
return s
with open(pyrocko.util.data_file(
'snuffler_help.html')) as f:
hcheat = qw.QLabel(f.read())
with open(pyrocko.util.data_file(
'snuffler_help_epilog.html')) as f:
hepilog = qw.QLabel(f.read())
for h in [hcheat, hepilog]:
h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter)
h.setWordWrap(True)
self.show_doc('Help', [hcheat, hepilog], target='panel')
def show_doc(self, name, labels, target='panel'):
scroller = qw.QScrollArea()
frame = qw.QFrame(scroller)
frame.setLineWidth(0)
layout = qw.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
frame.setLayout(layout)
scroller.setWidget(frame)
scroller.setWidgetResizable(True)
frame.setBackgroundRole(qg.QPalette.Base)
for h in labels:
h.setParent(frame)
h.setMargin(3)
h.setTextInteractionFlags(
qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse)
h.setBackgroundRole(qg.QPalette.Base)
layout.addWidget(h)
h.linkActivated.connect(
self.open_link)
if self.panel_parent is not None:
if target == 'panel':
self.panel_parent.add_panel(
name, scroller, True, volatile=False)
else:
self.panel_parent.add_tab(name, scroller)
def open_link(self, link):
qg.QDesktopServices.openUrl(qc.QUrl(link))
def wheelEvent(self, wheel_event):
if use_pyqt5:
self.wheel_pos += wheel_event.angleDelta().y()
else:
self.wheel_pos += wheel_event.delta()
n = self.wheel_pos // 120
self.wheel_pos = self.wheel_pos % 120
if n == 0:
return
amount = max(
1.,
abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.)
wdelta = amount * n
trmin, trmax = self.track_to_screen.get_in_range()
anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \
/ (trmax-trmin)
if wheel_event.modifiers() & qc.Qt.ControlModifier:
self.zoom_tracks(anchor, wdelta)
else:
self.scroll_tracks(-wdelta)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
if any(url.toLocalFile() for url in event.mimeData().urls()):
event.setDropAction(qc.Qt.LinkAction)
event.accept()
def dropEvent(self, event):
if event.mimeData().hasUrls():
paths = list(
str(url.toLocalFile()) for url in event.mimeData().urls())
event.acceptProposedAction()
self.load(paths)
def get_phase_name(self, kind):
return self.config.get_phase_name(kind)
def set_phase_kind(self, markers, kind):
phasename = self.get_phase_name(kind)
for marker in markers:
if isinstance(marker, PhaseMarker):
if kind == 10:
marker.convert_to_marker()
else:
marker.set_phasename(phasename)
marker.set_event(self.get_active_event())
elif isinstance(marker, EventMarker):
pass
else:
if kind != 10:
event = self.get_active_event()
marker.convert_to_phase_marker(
event, phasename, None, False)
def set_ntracks(self, ntracks):
if self.ntracks != ntracks:
self.ntracks = ntracks
if self.shown_tracks_range is not None:
l, h = self.shown_tracks_range
else:
l, h = 0, self.ntracks
self.tracks_range_changed.emit(self.ntracks, l, h)
def set_tracks_range(self, range, start=None):
low, high = range
low = min(self.ntracks-1, low)
high = min(self.ntracks, high)
low = max(0, low)
high = max(1, high)
if start is None:
start = float(low)
if self.shown_tracks_range != (low, high):
self.shown_tracks_range = low, high
self.shown_tracks_start = start
self.tracks_range_changed.emit(self.ntracks, low, high)
def scroll_tracks(self, shift):
shown = self.shown_tracks_range
shiftmin = -shown[0]
shiftmax = self.ntracks-shown[1]
shift = max(shiftmin, shift)
shift = min(shiftmax, shift)
shown = shown[0] + shift, shown[1] + shift
self.set_tracks_range((int(shown[0]), int(shown[1])))
self.update()
def zoom_tracks(self, anchor, delta):
ntracks_shown = self.shown_tracks_range[1] \
- self.shown_tracks_range[0]
if (ntracks_shown == 1 and delta <= 0) or \