forked from pyrocko/pyrocko
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.
4339 lines
150 KiB
Python
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) + ' × 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 \
|
|