Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/gui/snuffler/pile_viewer.py: 71%

2855 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-12-04 10:41 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import os 

7import time 

8import calendar 

9import datetime 

10import re 

11import math 

12import logging 

13import operator 

14import copy 

15import enum 

16from itertools import groupby 

17 

18import numpy as num 

19import pyrocko.model 

20import pyrocko.pile 

21import pyrocko.trace 

22import pyrocko.response 

23import pyrocko.util 

24import pyrocko.plot 

25import pyrocko.gui.snuffler.snuffling 

26import pyrocko.gui.snuffler.snufflings 

27import pyrocko.gui.snuffler.marker_editor 

28 

29from pyrocko.util import hpfloat, gmtime_x, mystrftime 

30 

31from .marker import associate_phases_to_events, MarkerOneNSLCRequired 

32 

33from ..util import (ValControl, LinValControl, Marker, EventMarker, 

34 PhaseMarker, make_QPolygonF, draw_label, Label, 

35 Progressbars, ColorbarControl) 

36 

37from ..qt_compat import qc, qg, qw, qsvg 

38 

39from .pile_viewer_waterfall import TraceWaterfall 

40 

41import scipy.stats as sstats 

42import platform 

43 

44MIN_LABEL_SIZE_PT = 6 

45 

46qc.QString = str 

47 

48qfiledialog_options = qw.QFileDialog.DontUseNativeDialog | \ 

49 qw.QFileDialog.DontUseSheet 

50 

51is_macos = platform.uname()[0] == 'Darwin' 

52 

53logger = logging.getLogger('pyrocko.gui.snuffler.pile_viewer') 

54 

55 

56def detrend(x, y): 

57 slope, offset, _, _, _ = sstats.linregress(x, y) 

58 y_detrended = y - slope * x - offset 

59 return y_detrended, slope, offset 

60 

61 

62def retrend(x, y_detrended, slope, offset): 

63 return x * slope + y_detrended + offset 

64 

65 

66class Global(object): 

67 appOnDemand = None 

68 

69 

70class NSLC(object): 

71 def __init__(self, n, s, l=None, c=None): # noqa 

72 self.network = n 

73 self.station = s 

74 self.location = l 

75 self.channel = c 

76 

77 

78class m_float(float): 

79 

80 def __str__(self): 

81 if abs(self) >= 10000.: 

82 return '%g km' % round(self/1000., 0) 

83 elif abs(self) >= 1000.: 

84 return '%g km' % round(self/1000., 1) 

85 else: 

86 return '%.5g m' % self 

87 

88 def __lt__(self, other): 

89 if other is None: 

90 return True 

91 return float(self) < float(other) 

92 

93 def __gt__(self, other): 

94 if other is None: 

95 return False 

96 return float(self) > float(other) 

97 

98 

99def m_float_or_none(x): 

100 if x is None: 

101 return None 

102 else: 

103 return m_float(x) 

104 

105 

106def make_chunks(items): 

107 ''' 

108 Split a list of integers into sublists of consecutive elements. 

109 ''' 

110 return [list(map(operator.itemgetter(1), g)) for k, g in groupby( 

111 enumerate(items), (lambda x: x[1]-x[0]))] 

112 

113 

114class deg_float(float): 

115 

116 def __str__(self): 

117 return '%4.0f' % self 

118 

119 def __lt__(self, other): 

120 if other is None: 

121 return True 

122 return float(self) < float(other) 

123 

124 def __gt__(self, other): 

125 if other is None: 

126 return False 

127 return float(self) > float(other) 

128 

129 

130def deg_float_or_none(x): 

131 if x is None: 

132 return None 

133 else: 

134 return deg_float(x) 

135 

136 

137class sector_int(int): 

138 

139 def __str__(self): 

140 return '[%i]' % self 

141 

142 def __lt__(self, other): 

143 if other is None: 

144 return True 

145 return int(self) < int(other) 

146 

147 def __gt__(self, other): 

148 if other is None: 

149 return False 

150 return int(self) > int(other) 

151 

152 

153def num_to_html(num): 

154 snum = '%g' % num 

155 m = re.match(r'(.+)[eE]([+-]?\d+)$', snum) 

156 if m: 

157 snum = m.group(1) + ' &times; 10<sup>%i</sup>' % int(m.group(2)) 

158 

159 return snum 

160 

161 

162gap_lap_tolerance = 5. 

163 

164 

165class ViewMode(enum.Enum): 

166 Wiggle = 1 

167 Waterfall = 2 

168 

169 

170class Timer(object): 

171 def __init__(self): 

172 self._start = None 

173 self._stop = None 

174 

175 def start(self): 

176 self._start = os.times() 

177 

178 def stop(self): 

179 self._stop = os.times() 

180 

181 def get(self): 

182 a = self._start 

183 b = self._stop 

184 if a is not None and b is not None: 

185 return tuple([b[i] - a[i] for i in range(5)]) 

186 else: 

187 return tuple([0.] * 5) 

188 

189 def __sub__(self, other): 

190 a = self.get() 

191 b = other.get() 

192 return tuple([a[i] - b[i] for i in range(5)]) 

193 

194 

195class ObjectStyle(object): 

196 def __init__(self, frame_pen, fill_brush): 

197 self.frame_pen = frame_pen 

198 self.fill_brush = fill_brush 

199 

200 

201box_styles = [] 

202box_alpha = 100 

203for color in 'orange skyblue butter chameleon chocolate plum ' \ 

204 'scarletred'.split(): 

205 

206 box_styles.append(ObjectStyle( 

207 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors[color+'3'])), 

208 qg.QBrush(qg.QColor( 

209 *(pyrocko.plot.tango_colors[color+'1'] + (box_alpha,)))), 

210 )) 

211 

212box_styles_coverage = {} 

213 

214box_styles_coverage['waveform'] = [ 

215 ObjectStyle( 

216 qg.QPen( 

217 qg.QColor(*pyrocko.plot.tango_colors['aluminium3']), 

218 1, qc.Qt.DashLine), 

219 qg.QBrush(qg.QColor( 

220 *(pyrocko.plot.tango_colors['aluminium1'] + (50,)))), 

221 ), 

222 ObjectStyle( 

223 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium4'])), 

224 qg.QBrush(qg.QColor( 

225 *(pyrocko.plot.tango_colors['aluminium2'] + (50,)))), 

226 ), 

227 ObjectStyle( 

228 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['plum3'])), 

229 qg.QBrush(qg.QColor( 

230 *(pyrocko.plot.tango_colors['plum1'] + (50,)))), 

231 )] 

232 

233box_styles_coverage['waveform_promise'] = [ 

234 ObjectStyle( 

235 qg.QPen( 

236 qg.QColor(*pyrocko.plot.tango_colors['skyblue3']), 

237 1, qc.Qt.DashLine), 

238 qg.QBrush(qg.QColor( 

239 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))), 

240 ), 

241 ObjectStyle( 

242 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])), 

243 qg.QBrush(qg.QColor( 

244 *(pyrocko.plot.tango_colors['skyblue1'] + (50,)))), 

245 ), 

246 ObjectStyle( 

247 qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['skyblue3'])), 

248 qg.QBrush(qg.QColor( 

249 *(pyrocko.plot.tango_colors['skyblue2'] + (50,)))), 

250 )] 

251 

252sday = 60*60*24. # \ 

253smonth = 60*60*24*30. # | only used as approx. intervals... 

254syear = 60*60*24*365. # / 

255 

256acceptable_tincs = num.array([ 

257 1, 2, 5, 10, 20, 30, 60, 60*5, 60*10, 60*20, 60*30, 60*60, 60*60*3, 

258 60*60*6, 60*60*12, sday, smonth, syear], dtype=float) 

259 

260 

261working_system_time_range = \ 

262 pyrocko.util.working_system_time_range() 

263 

264initial_time_range = [] 

265 

266try: 

267 initial_time_range.append( 

268 calendar.timegm((1950, 1, 1, 0, 0, 0))) 

269except Exception: 

270 initial_time_range.append(working_system_time_range[0]) 

271 

272try: 

273 initial_time_range.append( 

274 calendar.timegm((time.gmtime().tm_year + 11, 1, 1, 0, 0, 0))) 

275except Exception: 

276 initial_time_range.append(working_system_time_range[1]) 

277 

278 

279def is_working_time(t): 

280 return working_system_time_range[0] <= t and \ 

281 t <= working_system_time_range[1] 

282 

283 

284def fancy_time_ax_format(inc): 

285 l0_fmt_brief = '' 

286 l2_fmt = '' 

287 l2_trig = 0 

288 if inc < 0.000001: 

289 l0_fmt = '.%n' 

290 l0_center = False 

291 l1_fmt = '%H:%M:%S' 

292 l1_trig = 6 

293 l2_fmt = '%b %d, %Y' 

294 l2_trig = 3 

295 elif inc < 0.001: 

296 l0_fmt = '.%u' 

297 l0_center = False 

298 l1_fmt = '%H:%M:%S' 

299 l1_trig = 6 

300 l2_fmt = '%b %d, %Y' 

301 l2_trig = 3 

302 elif inc < 1: 

303 l0_fmt = '.%r' 

304 l0_center = False 

305 l1_fmt = '%H:%M:%S' 

306 l1_trig = 6 

307 l2_fmt = '%b %d, %Y' 

308 l2_trig = 3 

309 elif inc < 60: 

310 l0_fmt = '%H:%M:%S' 

311 l0_center = False 

312 l1_fmt = '%b %d, %Y' 

313 l1_trig = 3 

314 elif inc < 3600: 

315 l0_fmt = '%H:%M' 

316 l0_center = False 

317 l1_fmt = '%b %d, %Y' 

318 l1_trig = 3 

319 elif inc < sday: 

320 l0_fmt = '%H:%M' 

321 l0_center = False 

322 l1_fmt = '%b %d, %Y' 

323 l1_trig = 3 

324 elif inc < smonth: 

325 l0_fmt = '%a %d' 

326 l0_fmt_brief = '%d' 

327 l0_center = True 

328 l1_fmt = '%b, %Y' 

329 l1_trig = 2 

330 elif inc < syear: 

331 l0_fmt = '%b' 

332 l0_center = True 

333 l1_fmt = '%Y' 

334 l1_trig = 1 

335 else: 

336 l0_fmt = '%Y' 

337 l0_center = False 

338 l1_fmt = '' 

339 l1_trig = 0 

340 

341 return l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig 

342 

343 

344def day_start(timestamp): 

345 tt = time.gmtime(int(timestamp)) 

346 tts = tt[0:3] + (0, 0, 0) + tt[6:9] 

347 return calendar.timegm(tts) 

348 

349 

350def month_start(timestamp): 

351 tt = time.gmtime(int(timestamp)) 

352 tts = tt[0:2] + (1, 0, 0, 0) + tt[6:9] 

353 return calendar.timegm(tts) 

354 

355 

356def year_start(timestamp): 

357 tt = time.gmtime(int(timestamp)) 

358 tts = tt[0:1] + (1, 1, 0, 0, 0) + tt[6:9] 

359 return calendar.timegm(tts) 

360 

361 

362def time_nice_value(inc0): 

363 if inc0 < acceptable_tincs[0]: 

364 return pyrocko.plot.nice_value(inc0) 

365 elif inc0 > acceptable_tincs[-1]: 

366 return pyrocko.plot.nice_value(inc0/syear)*syear 

367 else: 

368 i = num.argmin(num.abs(acceptable_tincs-inc0)) 

369 return acceptable_tincs[i] 

370 

371 

372class TimeScaler(pyrocko.plot.AutoScaler): 

373 def __init__(self): 

374 pyrocko.plot.AutoScaler.__init__(self) 

375 self.mode = 'min-max' 

376 

377 def make_scale(self, data_range): 

378 assert self.mode in ('min-max', 'off'), \ 

379 'mode must be "min-max" or "off" for TimeScaler' 

380 

381 data_min = min(data_range) 

382 data_max = max(data_range) 

383 is_reverse = (data_range[0] > data_range[1]) 

384 

385 mi, ma = data_min, data_max 

386 nmi = mi 

387 if self.mode != 'off': 

388 nmi = mi - self.space*(ma-mi) 

389 

390 nma = ma 

391 if self.mode != 'off': 

392 nma = ma + self.space*(ma-mi) 

393 

394 mi, ma = nmi, nma 

395 

396 if mi == ma and self.mode != 'off': 

397 mi -= 1.0 

398 ma += 1.0 

399 

400 mi = max(working_system_time_range[0], mi) 

401 ma = min(working_system_time_range[1], ma) 

402 

403 # make nice tick increment 

404 if self.inc is not None: 

405 inc = self.inc 

406 else: 

407 if self.approx_ticks > 0.: 

408 inc = time_nice_value((ma-mi)/self.approx_ticks) 

409 else: 

410 inc = time_nice_value((ma-mi)*10.) 

411 

412 if inc == 0.0: 

413 inc = 1.0 

414 

415 if is_reverse: 

416 return ma, mi, -inc 

417 else: 

418 return mi, ma, inc 

419 

420 def make_ticks(self, data_range): 

421 mi, ma, inc = self.make_scale(data_range) 

422 

423 is_reverse = False 

424 if inc < 0: 

425 mi, ma, inc = ma, mi, -inc 

426 is_reverse = True 

427 

428 ticks = [] 

429 

430 if inc < sday: 

431 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5)) 

432 if inc < 0.001: 

433 mi_day = hpfloat(mi_day) 

434 

435 base = mi_day+num.ceil((mi-mi_day)/inc)*inc 

436 if inc < 0.001: 

437 base = hpfloat(base) 

438 

439 base_day = mi_day 

440 i = 0 

441 while True: 

442 tick = base+i*inc 

443 if tick > ma: 

444 break 

445 

446 tick_day = day_start(tick) 

447 if tick_day > base_day: 

448 base_day = tick_day 

449 base = base_day 

450 i = 0 

451 else: 

452 ticks.append(tick) 

453 i += 1 

454 

455 elif inc < smonth: 

456 mi_day = day_start(max(mi, working_system_time_range[0]+sday*1.5)) 

457 dt_base = datetime.datetime(*time.gmtime(mi_day)[:6]) 

458 delta = datetime.timedelta(days=int(round(inc/sday))) 

459 if mi_day == mi: 

460 dt_base += delta 

461 i = 0 

462 while True: 

463 current = dt_base + i*delta 

464 tick = calendar.timegm(current.timetuple()) 

465 if tick > ma: 

466 break 

467 ticks.append(tick) 

468 i += 1 

469 

470 elif inc < syear: 

471 mi_month = month_start(max( 

472 mi, working_system_time_range[0]+smonth*1.5)) 

473 

474 y, m = time.gmtime(mi_month)[:2] 

475 while True: 

476 tick = calendar.timegm((y, m, 1, 0, 0, 0)) 

477 m += 1 

478 if m > 12: 

479 y, m = y+1, 1 

480 

481 if tick > ma: 

482 break 

483 

484 if tick >= mi: 

485 ticks.append(tick) 

486 

487 else: 

488 mi_year = year_start(max( 

489 mi, working_system_time_range[0]+syear*1.5)) 

490 

491 incy = int(round(inc/syear)) 

492 y = int(num.ceil(time.gmtime(mi_year)[0]/incy)*incy) 

493 

494 while True: 

495 tick = calendar.timegm((y, 1, 1, 0, 0, 0)) 

496 y += incy 

497 if tick > ma: 

498 break 

499 if tick >= mi: 

500 ticks.append(tick) 

501 

502 if is_reverse: 

503 ticks.reverse() 

504 

505 return ticks, inc 

506 

507 

508def need_l1_tick(tt, ms, l1_trig): 

509 return (0, 1, 1, 0, 0, 0)[l1_trig:] == tt[l1_trig:6] and ms == 0.0 

510 

511 

512def tick_to_labels(tick, inc): 

513 tt, ms = gmtime_x(tick) 

514 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \ 

515 fancy_time_ax_format(inc) 

516 

517 l0 = mystrftime(l0_fmt, tt, ms) 

518 l0_brief = mystrftime(l0_fmt_brief, tt, ms) 

519 l1, l2 = None, None 

520 if need_l1_tick(tt, ms, l1_trig): 

521 l1 = mystrftime(l1_fmt, tt, ms) 

522 if need_l1_tick(tt, ms, l2_trig): 

523 l2 = mystrftime(l2_fmt, tt, ms) 

524 

525 return l0, l0_brief, l0_center, l1, l2 

526 

527 

528def l1_l2_tick(tick, inc): 

529 tt, ms = gmtime_x(tick) 

530 l0_fmt, l0_fmt_brief, l0_center, l1_fmt, l1_trig, l2_fmt, l2_trig = \ 

531 fancy_time_ax_format(inc) 

532 

533 l1 = mystrftime(l1_fmt, tt, ms) 

534 l2 = mystrftime(l2_fmt, tt, ms) 

535 return l1, l2 

536 

537 

538class TimeAx(TimeScaler): 

539 def __init__(self, *args): 

540 TimeScaler.__init__(self, *args) 

541 

542 def drawit(self, p, xprojection, yprojection): 

543 pen = qg.QPen(qg.QColor(*pyrocko.plot.tango_colors['aluminium5']), 1) 

544 p.setPen(pen) 

545 font = qg.QFont() 

546 font.setBold(True) 

547 p.setFont(font) 

548 fm = p.fontMetrics() 

549 ticklen = 10 

550 pad = 10 

551 tmin, tmax = xprojection.get_in_range() 

552 ticks, inc = self.make_ticks((tmin, tmax)) 

553 l1_hits = 0 

554 l2_hits = 0 

555 

556 vmin, vmax = yprojection(0), yprojection(ticklen) 

557 uumin, uumax = xprojection.get_out_range() 

558 first_tick_with_label = None 

559 

560 data = [] 

561 for tick in ticks: 

562 umin = xprojection(tick) 

563 

564 umin_approx_next = xprojection(tick+inc) 

565 umax = xprojection(tick) 

566 

567 pinc_approx = umin_approx_next - umin 

568 

569 p.drawLine(qc.QPointF(umin, vmin), qc.QPointF(umax, vmax)) 

570 l0, l0_brief, l0_center, l1, l2 = tick_to_labels(tick, inc) 

571 

572 if tick == 0.0 and tmax - tmin < 3600*24: 

573 # hide year at epoch 

574 # (we assume that synthetic data is shown) 

575 if l2: 

576 l2 = None 

577 elif l1: 

578 l1 = None 

579 

580 if l0_center: 

581 ushift = (umin_approx_next-umin)/2. 

582 else: 

583 ushift = 0. 

584 

585 abbr_level = 0 

586 for l0x in (l0, l0_brief, ''): 

587 label0 = l0x 

588 rect0 = fm.boundingRect(label0) 

589 if rect0.width() <= pinc_approx*0.9: 

590 break 

591 

592 abbr_level += 1 

593 

594 data.append(( 

595 l0, l0_brief, l0_center, l1, l2, tick, ushift, umin, 

596 pinc_approx)) 

597 

598 for (l0, l0_brief, l0_center, l1, l2, tick, ushift, umin, 

599 pinc_approx) in data: 

600 

601 label0 = (l0, l0_brief, '')[abbr_level] 

602 rect0 = fm.boundingRect(label0) 

603 

604 if uumin+pad < umin-rect0.width()/2.+ushift and \ 

605 umin+rect0.width()/2.+ushift < uumax-pad: 

606 

607 if first_tick_with_label is None: 

608 first_tick_with_label = tick 

609 p.drawText(qc.QPointF( 

610 umin-rect0.width()/2.+ushift, 

611 vmin+rect0.height()+ticklen), label0) 

612 

613 if l1: 

614 label1 = l1 

615 rect1 = fm.boundingRect(label1) 

616 if uumin+pad < umin-rect1.width()/2. and \ 

617 umin+rect1.width()/2. < uumax-pad: 

618 

619 p.drawText(qc.QPointF( 

620 umin-rect1.width()/2., 

621 vmin+rect0.height()+rect1.height()+ticklen), 

622 label1) 

623 

624 l1_hits += 1 

625 

626 if l2: 

627 label2 = l2 

628 rect2 = fm.boundingRect(label2) 

629 if uumin+pad < umin-rect2.width()/2. and \ 

630 umin+rect2.width()/2. < uumax-pad: 

631 

632 p.drawText(qc.QPointF( 

633 umin-rect2.width()/2., 

634 vmin+rect0.height()+rect1.height()+rect2.height() + 

635 ticklen), label2) 

636 

637 l2_hits += 1 

638 

639 if first_tick_with_label is None: 

640 first_tick_with_label = tmin 

641 

642 l1, l2 = l1_l2_tick(first_tick_with_label, inc) 

643 

644 if -3600.*25 < first_tick_with_label <= 3600.*25 and \ 

645 tmax - tmin < 3600*24: 

646 

647 # hide year at epoch (we assume that synthetic data is shown) 

648 if l2: 

649 l2 = None 

650 elif l1: 

651 l1 = None 

652 

653 if l1_hits == 0 and l1: 

654 label1 = l1 

655 rect1 = fm.boundingRect(label1) 

656 p.drawText(qc.QPointF( 

657 uumin+pad, 

658 vmin+rect0.height()+rect1.height()+ticklen), 

659 label1) 

660 

661 l1_hits += 1 

662 

663 if l2_hits == 0 and l2: 

664 label2 = l2 

665 rect2 = fm.boundingRect(label2) 

666 p.drawText(qc.QPointF( 

667 uumin+pad, 

668 vmin+rect0.height()+rect1.height()+rect2.height()+ticklen), 

669 label2) 

670 

671 v = yprojection(0) 

672 p.drawLine(qc.QPointF(uumin, v), qc.QPointF(uumax, v)) 

673 

674 

675class Projection(object): 

676 def __init__(self): 

677 self.xr = 0., 1. 

678 self.ur = 0., 1. 

679 

680 def set_in_range(self, xmin, xmax): 

681 if xmax == xmin: 

682 xmax = xmin + 1. 

683 

684 self.xr = xmin, xmax 

685 

686 def get_in_range(self): 

687 return self.xr 

688 

689 def set_out_range(self, umin, umax): 

690 if umax == umin: 

691 umax = umin + 1. 

692 

693 self.ur = umin, umax 

694 

695 def get_out_range(self): 

696 return self.ur 

697 

698 def __call__(self, x): 

699 umin, umax = self.ur 

700 xmin, xmax = self.xr 

701 return umin + (x-xmin)*((umax-umin)/(xmax-xmin)) 

702 

703 def clipped(self, x, umax_pad): 

704 umin, umax = self.ur 

705 xmin, xmax = self.xr 

706 return min( 

707 umax-umax_pad, 

708 max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin)))) 

709 

710 def rev(self, u): 

711 umin, umax = self.ur 

712 xmin, xmax = self.xr 

713 return xmin + (u-umin)*((xmax-xmin)/(umax-umin)) 

714 

715 def copy(self): 

716 return copy.copy(self) 

717 

718 

719def add_radiobuttongroup(menu, menudef, target, default=None): 

720 group = qw.QActionGroup(menu) 

721 group.setExclusive(True) 

722 menuitems = [] 

723 

724 for name, value, *shortcut in menudef: 

725 action = menu.addAction(name) 

726 action.setCheckable(True) 

727 action.setActionGroup(group) 

728 if shortcut: 

729 action.setShortcut(shortcut[0]) 

730 

731 menuitems.append((action, value)) 

732 if default is not None and ( 

733 name.lower().replace(' ', '_') == default or 

734 value == default): 

735 action.setChecked(True) 

736 

737 group.triggered.connect(target) 

738 

739 if default is None: 

740 menuitems[0][0].setChecked(True) 

741 

742 return menuitems 

743 

744 

745def sort_actions(menu): 

746 actions = [act for act in menu.actions() if not act.menu()] 

747 for action in actions: 

748 menu.removeAction(action) 

749 actions.sort(key=lambda x: str(x.text())) 

750 

751 help_action = [a for a in actions if a.text() == 'Snuffler Controls'] 

752 if help_action: 

753 actions.insert(0, actions.pop(actions.index(help_action[0]))) 

754 for action in actions: 

755 menu.addAction(action) 

756 

757 

758fkey_map = dict(zip( 

759 (qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F3, qc.Qt.Key_F4, qc.Qt.Key_F5, 

760 qc.Qt.Key_F6, qc.Qt.Key_F7, qc.Qt.Key_F8, qc.Qt.Key_F9, qc.Qt.Key_F10), 

761 range(10))) 

762 

763 

764class PileViewerMainException(Exception): 

765 pass 

766 

767 

768class PileViewerMenuBar(qw.QMenuBar): 

769 ... 

770 

771 

772class PileViewerMenuBarButton(qw.QPushButton): 

773 

774 def __init__(self, text, *args, **kwargs): 

775 qw.QPushButton.__init__(self, text, *args, **kwargs) 

776 self.setFlat(True) 

777 self.setSizePolicy( 

778 qw.QSizePolicy.Preferred, qw.QSizePolicy.Preferred) 

779 self.setContentsMargins(0, 0, 0, 0) 

780 s = self.fontMetrics().boundingRect(text) 

781 self.setMaximumHeight(s.height() + 3) 

782 self.setMaximumWidth(max(s.height() + 3, s.width() + 10)) 

783 

784 def sizeHint(self): 

785 s = qw.QPushButton.sizeHint(self) 

786 return qc.QSize(max(s.height(), s.width()), s.height()) 

787 

788 

789def MakePileViewerMainClass(base): 

790 

791 class PileViewerMain(base): 

792 

793 want_input = qc.pyqtSignal() 

794 toggle_input = qc.pyqtSignal() 

795 about_to_close = qc.pyqtSignal() 

796 pile_has_changed_signal = qc.pyqtSignal() 

797 tracks_range_changed = qc.pyqtSignal(int, int, int) 

798 frequency_filter_changed = qc.pyqtSignal(float, float) 

799 

800 begin_markers_add = qc.pyqtSignal(int, int) 

801 end_markers_add = qc.pyqtSignal() 

802 begin_markers_remove = qc.pyqtSignal(int, int) 

803 end_markers_remove = qc.pyqtSignal() 

804 

805 marker_selection_changed = qc.pyqtSignal(list) 

806 active_event_marker_changed = qc.pyqtSignal() 

807 

808 def __init__(self, pile, ntracks_shown_max, panel_parent, *args, 

809 menu=None): 

810 base.__init__(self, *args) 

811 

812 self.pile = pile 

813 self.ax_height = 80 

814 self.panel_parent = panel_parent 

815 

816 self.click_tolerance = 5 

817 

818 self.ntracks_shown_max = ntracks_shown_max 

819 self.initial_ntracks_shown_max = ntracks_shown_max 

820 self.ntracks = 0 

821 self.show_all = True 

822 self.shown_tracks_range = None 

823 self.track_start = None 

824 self.track_trange = None 

825 

826 self.lowpass = None 

827 self.highpass = None 

828 self.gain = 1.0 

829 self.rotate = 0.0 

830 self.picking_down = None 

831 self.picking = None 

832 self.floating_marker = None 

833 self.markers = pyrocko.pile.Sorted([], 'tmin') 

834 self.markers_deltat_max = 0. 

835 self.n_selected_markers = 0 

836 self.all_marker_kinds = (0, 1, 2, 3, 4, 5, 6, 7) 

837 self.visible_marker_kinds = self.all_marker_kinds 

838 self.active_event_marker = None 

839 self.ignore_releases = 0 

840 self.reloaded = False 

841 self.pile_has_changed = False 

842 self.config = pyrocko.config.config('snuffler') 

843 

844 self.tax = TimeAx() 

845 self.setBackgroundRole(qg.QPalette.Base) 

846 self.setAutoFillBackground(True) 

847 poli = qw.QSizePolicy( 

848 qw.QSizePolicy.Expanding, 

849 qw.QSizePolicy.Expanding) 

850 

851 self.setSizePolicy(poli) 

852 self.setMinimumSize(300, 200) 

853 self.setFocusPolicy(qc.Qt.ClickFocus) 

854 

855 self.menu = menu 

856 

857 file_menu = self.menu.addMenu('&File') 

858 view_menu = self.menu.addMenu('&View') 

859 options_menu = self.menu.addMenu('&Options') 

860 scale_menu = self.menu.addMenu('&Scaling') 

861 sort_menu = self.menu.addMenu('Sor&ting') 

862 self.toggle_panel_menu = self.menu.addMenu('Sn&ufflings') 

863 

864 help_menu = self.menu.addMenu('&Help') 

865 

866 self.snufflings_menu = self.toggle_panel_menu.addMenu( 

867 'Run Snuffling') 

868 self.toggle_panel_menu.addSeparator() 

869 self.snuffling_help = help_menu.addMenu('Snuffling Help') 

870 help_menu.addSeparator() 

871 

872 file_menu.addAction( 

873 qg.QIcon.fromTheme('document-open'), 

874 'Open waveform files...', 

875 self.open_waveforms, 

876 qg.QKeySequence.Open) 

877 

878 file_menu.addAction( 

879 qg.QIcon.fromTheme('document-open'), 

880 'Open waveform directory...', 

881 self.open_waveform_directory) 

882 

883 file_menu.addAction( 

884 'Open station files...', 

885 self.open_stations) 

886 

887 file_menu.addAction( 

888 'Open StationXML files...', 

889 self.open_stations_xml) 

890 

891 file_menu.addAction( 

892 'Open event file...', 

893 self.read_events) 

894 

895 file_menu.addSeparator() 

896 file_menu.addAction( 

897 'Open marker file...', 

898 self.read_markers) 

899 

900 file_menu.addAction( 

901 qg.QIcon.fromTheme('document-save'), 

902 'Save markers...', 

903 self.write_markers, 

904 qg.QKeySequence.Save) 

905 

906 file_menu.addAction( 

907 qg.QIcon.fromTheme('document-save-as'), 

908 'Save selected markers...', 

909 self.write_selected_markers, 

910 qg.QKeySequence.SaveAs) 

911 

912 file_menu.addSeparator() 

913 file_menu.addAction( 

914 qg.QIcon.fromTheme('document-print'), 

915 'Print', 

916 self.printit, 

917 qg.QKeySequence.Print) 

918 

919 file_menu.addAction( 

920 qg.QIcon.fromTheme('insert-image'), 

921 'Save as SVG or PNG', 

922 self.savesvg, 

923 qg.QKeySequence(qc.Qt.CTRL + qc.Qt.Key_E)) 

924 

925 file_menu.addSeparator() 

926 close = file_menu.addAction( 

927 qg.QIcon.fromTheme('window-close'), 

928 'Close', 

929 self.myclose) 

930 close.setShortcuts( 

931 (qg.QKeySequence(qc.Qt.Key_Q), 

932 qg.QKeySequence(qc.Qt.Key_X))) 

933 

934 # Scale Menu 

935 menudef = [ 

936 ('Individual Scale', 

937 lambda tr: tr.nslc_id, 

938 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_I)), 

939 ('Common Scale', 

940 lambda tr: None, 

941 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_C)), 

942 ('Common Scale per Station', 

943 lambda tr: (tr.network, tr.station), 

944 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_S)), 

945 ('Common Scale per Station Location', 

946 lambda tr: (tr.network, tr.station, tr.location)), 

947 ('Common Scale per Component', 

948 lambda tr: (tr.channel)), 

949 ] 

950 

951 self.menuitems_scaling = add_radiobuttongroup( 

952 scale_menu, menudef, self.scalingmode_change, 

953 default=self.config.trace_scale) 

954 scale_menu.addSeparator() 

955 

956 self.scaling_key = self.menuitems_scaling[0][1] 

957 self.scaling_hooks = {} 

958 self.scalingmode_change() 

959 

960 menudef = [ 

961 ('Scaling based on Minimum and Maximum', 

962 ('minmax', 'minmax')), 

963 ('Scaling based on Minimum and Maximum (Robust)', 

964 ('minmax', 'robust')), 

965 ('Scaling based on Mean ± 2x Std. Deviation', (2, 'minmax')), 

966 ('Scaling based on Mean ± 4x Std. Deviation', (4, 'minmax')), 

967 ] 

968 

969 self.menuitems_scaling_base = add_radiobuttongroup( 

970 scale_menu, menudef, self.scaling_base_change) 

971 

972 self.scaling_base = self.menuitems_scaling_base[0][1] 

973 scale_menu.addSeparator() 

974 

975 self.menuitem_fixscalerange = scale_menu.addAction( 

976 'Fix Scale Ranges') 

977 self.menuitem_fixscalerange.setCheckable(True) 

978 

979 # Sort Menu 

980 def sector_dist(sta): 

981 if sta.dist_m is None: 

982 return None, None 

983 else: 

984 return ( 

985 sector_int(round((sta.azimuth+15.)/30.)), 

986 m_float(sta.dist_m)) 

987 

988 menudef = [ 

989 ('Sort by Names', 

990 lambda tr: (), 

991 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_N)), 

992 ('Sort by Distance', 

993 lambda tr: self.station_attrib( 

994 tr, 

995 lambda sta: (m_float_or_none(sta.dist_m),), 

996 lambda tr: (None,)), 

997 qg.QKeySequence(qc.Qt.Key_S, qc.Qt.Key_D)), 

998 ('Sort by Azimuth', 

999 lambda tr: self.station_attrib( 

1000 tr, 

1001 lambda sta: (deg_float_or_none(sta.azimuth),), 

1002 lambda tr: (None,))), 

1003 ('Sort by Distance in 12 Azimuthal Blocks', 

1004 lambda tr: self.station_attrib( 

1005 tr, 

1006 sector_dist, 

1007 lambda tr: (None, None))), 

1008 ('Sort by Backazimuth', 

1009 lambda tr: self.station_attrib( 

1010 tr, 

1011 lambda sta: (deg_float_or_none(sta.backazimuth),), 

1012 lambda tr: (None,))), 

1013 ] 

1014 self.menuitems_ssorting = add_radiobuttongroup( 

1015 sort_menu, menudef, self.s_sortingmode_change) 

1016 sort_menu.addSeparator() 

1017 

1018 self._ssort = lambda tr: () 

1019 

1020 self.menu.addSeparator() 

1021 

1022 menudef = [ 

1023 ('Subsort by Network, Station, Location, Channel', 

1024 ((0, 1, 2, 3), # gathering 

1025 lambda tr: tr.location)), # coloring 

1026 ('Subsort by Network, Station, Channel, Location', 

1027 ((0, 1, 3, 2), 

1028 lambda tr: tr.channel)), 

1029 ('Subsort by Station, Network, Channel, Location', 

1030 ((1, 0, 3, 2), 

1031 lambda tr: tr.channel)), 

1032 ('Subsort by Location, Network, Station, Channel', 

1033 ((2, 0, 1, 3), 

1034 lambda tr: tr.channel)), 

1035 ('Subsort by Channel, Network, Station, Location', 

1036 ((3, 0, 1, 2), 

1037 lambda tr: (tr.network, tr.station, tr.location))), 

1038 ('Subsort by Network, Station, Channel (Grouped by Location)', 

1039 ((0, 1, 3), 

1040 lambda tr: tr.location)), 

1041 ('Subsort by Station, Network, Channel (Grouped by Location)', 

1042 ((1, 0, 3), 

1043 lambda tr: tr.location)), 

1044 ] 

1045 

1046 self.menuitems_sorting = add_radiobuttongroup( 

1047 sort_menu, menudef, self.sortingmode_change) 

1048 

1049 menudef = [(x.key, x.value) for x in 

1050 self.config.visible_length_setting] 

1051 

1052 # View menu 

1053 self.menuitems_visible_length = add_radiobuttongroup( 

1054 view_menu, menudef, 

1055 self.visible_length_change) 

1056 view_menu.addSeparator() 

1057 

1058 view_modes = [ 

1059 ('Wiggle Plot', ViewMode.Wiggle), 

1060 ('Waterfall', ViewMode.Waterfall) 

1061 ] 

1062 

1063 self.menuitems_viewmode = add_radiobuttongroup( 

1064 view_menu, view_modes, 

1065 self.viewmode_change, default=ViewMode.Wiggle) 

1066 view_menu.addSeparator() 

1067 

1068 self.menuitem_cliptraces = view_menu.addAction( 

1069 'Clip Traces') 

1070 self.menuitem_cliptraces.setCheckable(True) 

1071 self.menuitem_cliptraces.setChecked(self.config.clip_traces) 

1072 

1073 self.menuitem_showboxes = view_menu.addAction( 

1074 'Show Boxes') 

1075 self.menuitem_showboxes.setCheckable(True) 

1076 self.menuitem_showboxes.setChecked( 

1077 self.config.show_boxes) 

1078 

1079 self.menuitem_colortraces = view_menu.addAction( 

1080 'Color Traces') 

1081 self.menuitem_colortraces.setCheckable(True) 

1082 self.menuitem_antialias = view_menu.addAction( 

1083 'Antialiasing') 

1084 self.menuitem_antialias.setCheckable(True) 

1085 

1086 view_menu.addSeparator() 

1087 self.menuitem_showscalerange = view_menu.addAction( 

1088 'Show Scale Ranges') 

1089 self.menuitem_showscalerange.setCheckable(True) 

1090 self.menuitem_showscalerange.setChecked( 

1091 self.config.show_scale_ranges) 

1092 

1093 self.menuitem_showscaleaxis = view_menu.addAction( 

1094 'Show Scale Axes') 

1095 self.menuitem_showscaleaxis.setCheckable(True) 

1096 self.menuitem_showscaleaxis.setChecked( 

1097 self.config.show_scale_axes) 

1098 

1099 self.menuitem_showzeroline = view_menu.addAction( 

1100 'Show Zero Lines') 

1101 self.menuitem_showzeroline.setCheckable(True) 

1102 

1103 view_menu.addSeparator() 

1104 view_menu.addAction( 

1105 qg.QIcon.fromTheme('view-fullscreen'), 

1106 'Fullscreen', 

1107 self.toggle_fullscreen, 

1108 qg.QKeySequence(qc.Qt.Key_F11)) 

1109 

1110 # Options Menu 

1111 self.menuitem_demean = options_menu.addAction('Demean') 

1112 self.menuitem_demean.setCheckable(True) 

1113 self.menuitem_demean.setChecked(self.config.demean) 

1114 self.menuitem_demean.setShortcut( 

1115 qg.QKeySequence(qc.Qt.Key_Underscore)) 

1116 

1117 self.menuitem_distances_3d = options_menu.addAction( 

1118 '3D distances', 

1119 self.distances_3d_changed) 

1120 self.menuitem_distances_3d.setCheckable(True) 

1121 

1122 self.menuitem_allowdownsampling = options_menu.addAction( 

1123 'Allow Downsampling') 

1124 self.menuitem_allowdownsampling.setCheckable(True) 

1125 self.menuitem_allowdownsampling.setChecked(True) 

1126 

1127 self.menuitem_degap = options_menu.addAction( 

1128 'Allow Degapping') 

1129 self.menuitem_degap.setCheckable(True) 

1130 self.menuitem_degap.setChecked(True) 

1131 

1132 options_menu.addSeparator() 

1133 

1134 self.menuitem_fft_filtering = options_menu.addAction( 

1135 'FFT Filtering') 

1136 self.menuitem_fft_filtering.setCheckable(True) 

1137 

1138 self.menuitem_lphp = options_menu.addAction( 

1139 'Bandpass is Low- + Highpass') 

1140 self.menuitem_lphp.setCheckable(True) 

1141 self.menuitem_lphp.setChecked(True) 

1142 

1143 options_menu.addSeparator() 

1144 self.menuitem_watch = options_menu.addAction( 

1145 'Watch Files') 

1146 self.menuitem_watch.setCheckable(True) 

1147 

1148 self.menuitem_liberal_fetch = options_menu.addAction( 

1149 'Liberal Fetch Optimization') 

1150 self.menuitem_liberal_fetch.setCheckable(True) 

1151 

1152 self.visible_length = menudef[0][1] 

1153 

1154 self.snufflings_menu.addAction( 

1155 'Reload Snufflings', 

1156 self.setup_snufflings) 

1157 

1158 # Disable ShadowPileTest 

1159 if False: 

1160 test_action = self.menu.addAction( 

1161 'Test', 

1162 self.toggletest) 

1163 test_action.setCheckable(True) 

1164 

1165 help_menu.addAction( 

1166 qg.QIcon.fromTheme('preferences-desktop-keyboard'), 

1167 'Snuffler Controls', 

1168 self.help, 

1169 qg.QKeySequence(qc.Qt.Key_Question)) 

1170 

1171 help_menu.addAction( 

1172 'About', 

1173 self.about) 

1174 

1175 toolbar = qw.QFrame(self.menu) 

1176 toolbar_layout = qw.QHBoxLayout() 

1177 toolbar_layout.setContentsMargins(1, 1, 1, 1) 

1178 toolbar.setLayout(toolbar_layout) 

1179 

1180 def tracks_plus(*args): 

1181 self.zoom_tracks(0., 1.) 

1182 

1183 button = PileViewerMenuBarButton('+') 

1184 button.clicked.connect(tracks_plus) 

1185 button.setToolTip('Show more traces.') 

1186 toolbar_layout.addWidget(button) 

1187 

1188 def tracks_minus(*args): 

1189 self.zoom_tracks(0., -1.) 

1190 

1191 button = PileViewerMenuBarButton('-') 

1192 button.clicked.connect(tracks_minus) 

1193 button.setToolTip('Show fewer traces.') 

1194 toolbar_layout.addWidget(button) 

1195 

1196 def toggle_input(*args): 

1197 self.toggle_input.emit() 

1198 

1199 button = PileViewerMenuBarButton(':') 

1200 button.setToolTip('Show command line.') 

1201 button.clicked.connect(toggle_input) 

1202 toolbar_layout.addWidget(button) 

1203 

1204 self.menu.setCornerWidget(toolbar) 

1205 

1206 self.time_projection = Projection() 

1207 self.set_time_range(self.pile.get_tmin(), self.pile.get_tmax()) 

1208 self.time_projection.set_out_range(0., self.width()) 

1209 

1210 self.gather = None 

1211 

1212 self.trace_filter = None 

1213 self.quick_filter = None 

1214 self.quick_filter_patterns = None, None 

1215 self.blacklist = [] 

1216 

1217 self.track_to_screen = Projection() 

1218 self.track_to_nslc_ids = {} 

1219 

1220 self.cached_vec = None 

1221 self.cached_processed_traces = None 

1222 

1223 self.timer = qc.QTimer(self) 

1224 self.timer.timeout.connect(self.periodical) 

1225 self.timer.setInterval(1000) 

1226 self.timer.start() 

1227 

1228 self._pile_changed = self.pile_changed # need to keep a strong ref 

1229 self.pile.add_listener(self._pile_changed) 

1230 

1231 self.trace_styles = {} 

1232 if self.get_squirrel() is None: 

1233 self.determine_box_styles() 

1234 

1235 self.setMouseTracking(True) 

1236 

1237 user_home_dir = os.path.expanduser('~') 

1238 self.snuffling_modules = {} 

1239 self.snuffling_paths = [os.path.join(user_home_dir, '.snufflings')] 

1240 self.default_snufflings = None 

1241 self.snufflings = [] 

1242 

1243 self.stations = {} 

1244 

1245 self.timer_draw = Timer() 

1246 self.timer_cutout = Timer() 

1247 self.time_spent_painting = 0.0 

1248 self.time_last_painted = time.time() 

1249 

1250 self.interactive_range_change_time = 0.0 

1251 self.interactive_range_change_delay_time = 10.0 

1252 self.follow_timer = None 

1253 

1254 self.sortingmode_change_time = 0.0 

1255 self.sortingmode_change_delay_time = None 

1256 

1257 self.old_data_ranges = {} 

1258 

1259 self.return_tag = None 

1260 self.wheel_pos = 60 

1261 

1262 self.setAcceptDrops(True) 

1263 self._paths_to_load = [] 

1264 

1265 self.tf_cache = {} 

1266 

1267 self.waterfall = TraceWaterfall() 

1268 self.waterfall_cmap = 'viridis' 

1269 self.waterfall_clip_min = 0. 

1270 self.waterfall_clip_max = 1. 

1271 self.waterfall_show_absolute = False 

1272 self.waterfall_integrate = False 

1273 self.view_mode = ViewMode.Wiggle 

1274 

1275 self.automatic_updates = True 

1276 

1277 self.closing = False 

1278 self.in_paint_event = False 

1279 

1280 def fail(self, reason): 

1281 box = qw.QMessageBox(self) 

1282 box.setText(reason) 

1283 box.exec_() 

1284 

1285 def set_trace_filter(self, filter_func): 

1286 self.trace_filter = filter_func 

1287 self.sortingmode_change() 

1288 

1289 def update_trace_filter(self): 

1290 if self.blacklist: 

1291 

1292 def blacklist_func(tr): 

1293 return not pyrocko.util.match_nslc( 

1294 self.blacklist, tr.nslc_id) 

1295 

1296 else: 

1297 blacklist_func = None 

1298 

1299 if self.quick_filter is None and blacklist_func is None: 

1300 self.set_trace_filter(None) 

1301 elif self.quick_filter is None: 

1302 self.set_trace_filter(blacklist_func) 

1303 elif blacklist_func is None: 

1304 self.set_trace_filter(self.quick_filter) 

1305 else: 

1306 self.set_trace_filter( 

1307 lambda tr: blacklist_func(tr) and self.quick_filter(tr)) 

1308 

1309 def set_quick_filter(self, filter_func): 

1310 self.quick_filter = filter_func 

1311 self.update_trace_filter() 

1312 

1313 def set_quick_filter_patterns(self, patterns, inputline=None): 

1314 if patterns is not None: 

1315 self.set_quick_filter( 

1316 lambda tr: pyrocko.util.match_nslc(patterns, tr.nslc_id)) 

1317 else: 

1318 self.set_quick_filter(None) 

1319 

1320 self.quick_filter_patterns = patterns, inputline 

1321 

1322 def get_quick_filter_patterns(self): 

1323 return self.quick_filter_patterns 

1324 

1325 def add_blacklist_pattern(self, pattern): 

1326 if pattern == 'empty': 

1327 keys = set(self.pile.nslc_ids) 

1328 trs = self.pile.all( 

1329 tmin=self.tmin, 

1330 tmax=self.tmax, 

1331 load_data=False, 

1332 degap=False) 

1333 

1334 for tr in trs: 

1335 if tr.nslc_id in keys: 

1336 keys.remove(tr.nslc_id) 

1337 

1338 for key in keys: 

1339 xpattern = '.'.join(key) 

1340 if xpattern not in self.blacklist: 

1341 self.blacklist.append(xpattern) 

1342 

1343 else: 

1344 if pattern in self.blacklist: 

1345 self.blacklist.remove(pattern) 

1346 

1347 self.blacklist.append(pattern) 

1348 

1349 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist)) 

1350 self.update_trace_filter() 

1351 

1352 def remove_blacklist_pattern(self, pattern): 

1353 if pattern in self.blacklist: 

1354 self.blacklist.remove(pattern) 

1355 else: 

1356 raise PileViewerMainException( 

1357 'Pattern not found in blacklist.') 

1358 

1359 logger.info('Blacklist is [ %s ]' % ', '.join(self.blacklist)) 

1360 self.update_trace_filter() 

1361 

1362 def clear_blacklist(self): 

1363 self.blacklist = [] 

1364 self.update_trace_filter() 

1365 

1366 def ssort(self, tr): 

1367 return self._ssort(tr) 

1368 

1369 def station_key(self, x): 

1370 return x.network, x.station 

1371 

1372 def station_keys(self, x): 

1373 return [ 

1374 (x.network, x.station, x.location), 

1375 (x.network, x.station)] 

1376 

1377 def station_attrib(self, tr, getter, default_getter): 

1378 for sk in self.station_keys(tr): 

1379 if sk in self.stations: 

1380 station = self.stations[sk] 

1381 return getter(station) 

1382 

1383 return default_getter(tr) 

1384 

1385 def get_station(self, sk): 

1386 return self.stations[sk] 

1387 

1388 def has_station(self, station): 

1389 for sk in self.station_keys(station): 

1390 if sk in self.stations: 

1391 return True 

1392 

1393 return False 

1394 

1395 def station_latlon(self, tr, default_getter=lambda tr: (0., 0.)): 

1396 return self.station_attrib( 

1397 tr, lambda sta: (sta.lat, sta.lon), default_getter) 

1398 

1399 def set_stations(self, stations): 

1400 self.stations = {} 

1401 self.add_stations(stations) 

1402 

1403 def add_stations(self, stations): 

1404 for station in stations: 

1405 for sk in self.station_keys(station): 

1406 self.stations[sk] = station 

1407 

1408 ev = self.get_active_event() 

1409 if ev: 

1410 self.set_origin(ev) 

1411 

1412 def add_event(self, event): 

1413 marker = EventMarker(event) 

1414 self.add_marker(marker) 

1415 

1416 def add_events(self, events): 

1417 markers = [EventMarker(e) for e in events] 

1418 self.add_markers(markers) 

1419 

1420 def set_event_marker_as_origin(self, ignore=None): 

1421 selected = self.selected_markers() 

1422 if not selected: 

1423 self.fail('An event marker must be selected.') 

1424 return 

1425 

1426 m = selected[0] 

1427 if not isinstance(m, EventMarker): 

1428 self.fail('Selected marker is not an event.') 

1429 return 

1430 

1431 self.set_active_event_marker(m) 

1432 

1433 def deactivate_event_marker(self): 

1434 if self.active_event_marker: 

1435 self.active_event_marker.active = False 

1436 

1437 self.active_event_marker_changed.emit() 

1438 self.active_event_marker = None 

1439 

1440 def set_active_event_marker(self, event_marker): 

1441 if self.active_event_marker: 

1442 self.active_event_marker.active = False 

1443 

1444 self.active_event_marker = event_marker 

1445 event_marker.active = True 

1446 event = event_marker.get_event() 

1447 self.set_origin(event) 

1448 self.active_event_marker_changed.emit() 

1449 

1450 def set_active_event(self, event): 

1451 for marker in self.markers: 

1452 if isinstance(marker, EventMarker): 

1453 if marker.get_event() is event: 

1454 self.set_active_event_marker(marker) 

1455 

1456 def get_active_event_marker(self): 

1457 return self.active_event_marker 

1458 

1459 def get_active_event(self): 

1460 m = self.get_active_event_marker() 

1461 if m is not None: 

1462 return m.get_event() 

1463 else: 

1464 return None 

1465 

1466 def get_active_markers(self): 

1467 emarker = self.get_active_event_marker() 

1468 if emarker is None: 

1469 return None, [] 

1470 

1471 else: 

1472 ev = emarker.get_event() 

1473 pmarkers = [ 

1474 m for m in self.markers 

1475 if isinstance(m, PhaseMarker) and m.get_event() is ev] 

1476 

1477 return emarker, pmarkers 

1478 

1479 def set_origin(self, location): 

1480 for station in self.stations.values(): 

1481 station.set_event_relative_data( 

1482 location, 

1483 distance_3d=self.menuitem_distances_3d.isChecked()) 

1484 

1485 self.sortingmode_change() 

1486 

1487 def distances_3d_changed(self): 

1488 ignore = self.menuitem_distances_3d.isChecked() 

1489 self.set_event_marker_as_origin(ignore) 

1490 

1491 def iter_snuffling_modules(self): 

1492 pjoin = os.path.join 

1493 for path in self.snuffling_paths: 

1494 

1495 if not os.path.isdir(path): 

1496 os.mkdir(path) 

1497 

1498 for entry in os.listdir(path): 

1499 directory = path 

1500 fn = entry 

1501 d = pjoin(path, entry) 

1502 if os.path.isdir(d): 

1503 directory = d 

1504 if os.path.isfile( 

1505 os.path.join(directory, 'snuffling.py')): 

1506 fn = 'snuffling.py' 

1507 

1508 if not fn.endswith('.py'): 

1509 continue 

1510 

1511 name = fn[:-3] 

1512 

1513 if (directory, name) not in self.snuffling_modules: 

1514 self.snuffling_modules[directory, name] = \ 

1515 pyrocko.gui.snuffler.snuffling.SnufflingModule( 

1516 directory, name, self) 

1517 

1518 yield self.snuffling_modules[directory, name] 

1519 

1520 def setup_snufflings(self): 

1521 # user snufflings 

1522 from pyrocko.gui.snuffler.snuffling import BrokenSnufflingModule 

1523 for mod in self.iter_snuffling_modules(): 

1524 try: 

1525 mod.load_if_needed() 

1526 except BrokenSnufflingModule as e: 

1527 logger.warning('Snuffling module "%s" is broken' % e) 

1528 

1529 # load the default snufflings on first run 

1530 if self.default_snufflings is None: 

1531 self.default_snufflings = pyrocko.gui.snuffler\ 

1532 .snufflings.__snufflings__() 

1533 for snuffling in self.default_snufflings: 

1534 self.add_snuffling(snuffling) 

1535 

1536 def set_panel_parent(self, panel_parent): 

1537 self.panel_parent = panel_parent 

1538 

1539 def get_panel_parent(self): 

1540 return self.panel_parent 

1541 

1542 def add_snuffling(self, snuffling, reloaded=False): 

1543 logger.debug('Adding snuffling %s' % snuffling.get_name()) 

1544 snuffling.init_gui( 

1545 self, self.get_panel_parent(), self, reloaded=reloaded) 

1546 self.snufflings.append(snuffling) 

1547 self.update() 

1548 

1549 def remove_snuffling(self, snuffling): 

1550 snuffling.delete_gui() 

1551 self.update() 

1552 self.snufflings.remove(snuffling) 

1553 snuffling.pre_destroy() 

1554 

1555 def add_snuffling_menuitem(self, item): 

1556 self.snufflings_menu.addAction(item) 

1557 item.setParent(self.snufflings_menu) 

1558 sort_actions(self.snufflings_menu) 

1559 

1560 def remove_snuffling_menuitem(self, item): 

1561 self.snufflings_menu.removeAction(item) 

1562 

1563 def add_snuffling_help_menuitem(self, item): 

1564 self.snuffling_help.addAction(item) 

1565 item.setParent(self.snuffling_help) 

1566 sort_actions(self.snuffling_help) 

1567 

1568 def remove_snuffling_help_menuitem(self, item): 

1569 self.snuffling_help.removeAction(item) 

1570 

1571 def add_panel_toggler(self, item): 

1572 self.toggle_panel_menu.addAction(item) 

1573 item.setParent(self.toggle_panel_menu) 

1574 sort_actions(self.toggle_panel_menu) 

1575 

1576 def remove_panel_toggler(self, item): 

1577 self.toggle_panel_menu.removeAction(item) 

1578 

1579 def load(self, paths, regex=None, format='detect', 

1580 cache_dir=None, force_cache=False): 

1581 

1582 if cache_dir is None: 

1583 cache_dir = pyrocko.config.config().cache_dir 

1584 if isinstance(paths, str): 

1585 paths = [paths] 

1586 

1587 fns = pyrocko.util.select_files( 

1588 paths, selector=None, include=regex, show_progress=False) 

1589 

1590 if not fns: 

1591 return 

1592 

1593 cache = pyrocko.pile.get_cache(cache_dir) 

1594 

1595 t = [time.time()] 

1596 

1597 def update_bar(label, value): 

1598 pbs = self.parent().get_progressbars() 

1599 if label.lower() == 'looking at files': 

1600 label = 'Looking at %i files' % len(fns) 

1601 else: 

1602 label = 'Scanning %i files' % len(fns) 

1603 

1604 return pbs.set_status(label, value) 

1605 

1606 def update_progress(label, i, n): 

1607 abort = False 

1608 

1609 qw.qApp.processEvents() 

1610 if n != 0: 

1611 perc = i*100/n 

1612 else: 

1613 perc = 100 

1614 abort |= update_bar(label, perc) 

1615 abort |= self.window().is_closing() 

1616 

1617 tnow = time.time() 

1618 if t[0] + 1. + self.time_spent_painting * 10. < tnow: 

1619 self.update() 

1620 t[0] = tnow 

1621 

1622 return abort 

1623 

1624 self.automatic_updates = False 

1625 

1626 self.pile.load_files( 

1627 sorted(fns), 

1628 filename_attributes=regex, 

1629 cache=cache, 

1630 fileformat=format, 

1631 show_progress=False, 

1632 update_progress=update_progress) 

1633 

1634 self.automatic_updates = True 

1635 self.update() 

1636 

1637 def load_queued(self): 

1638 if not self._paths_to_load: 

1639 return 

1640 paths = self._paths_to_load 

1641 self._paths_to_load = [] 

1642 self.load(paths) 

1643 

1644 def load_soon(self, paths): 

1645 self._paths_to_load.extend(paths) 

1646 qc.QTimer.singleShot(200, self.load_queued) 

1647 

1648 def open_waveforms(self): 

1649 caption = 'Select one or more files to open' 

1650 

1651 fns, _ = qw.QFileDialog.getOpenFileNames( 

1652 self, caption, options=qfiledialog_options) 

1653 

1654 if fns: 

1655 self.load(list(str(fn) for fn in fns)) 

1656 

1657 def open_waveform_directory(self): 

1658 caption = 'Select directory to scan for waveform files' 

1659 

1660 dn = qw.QFileDialog.getExistingDirectory( 

1661 self, caption, options=qfiledialog_options) 

1662 

1663 if dn: 

1664 self.load([str(dn)]) 

1665 

1666 def open_stations(self, fns=None): 

1667 caption = 'Select one or more Pyrocko station files to open' 

1668 

1669 if not fns: 

1670 fns, _ = qw.QFileDialog.getOpenFileNames( 

1671 self, caption, options=qfiledialog_options) 

1672 

1673 try: 

1674 stations = [pyrocko.model.load_stations(str(x)) for x in fns] 

1675 for stat in stations: 

1676 self.add_stations(stat) 

1677 

1678 except Exception as e: 

1679 self.fail('Failed to read station file: %s' % str(e)) 

1680 

1681 def open_stations_xml(self, fns=None): 

1682 from pyrocko.io import stationxml 

1683 

1684 caption = 'Select one or more StationXML files' 

1685 if not fns: 

1686 fns, _ = qw.QFileDialog.getOpenFileNames( 

1687 self, caption, options=qfiledialog_options, 

1688 filter='StationXML (*.xml *.XML *.stationxml *.stationXML)' 

1689 ';;All files (*)') 

1690 

1691 try: 

1692 stations = [ 

1693 stationxml.load_xml(filename=str(x)).get_pyrocko_stations() 

1694 for x in fns] 

1695 

1696 for stat in stations: 

1697 self.add_stations(stat) 

1698 

1699 except Exception as e: 

1700 self.fail('Failed to read StationXML file: %s' % str(e)) 

1701 

1702 def add_traces(self, traces): 

1703 if traces: 

1704 mtf = pyrocko.pile.MemTracesFile(None, traces) 

1705 self.pile.add_file(mtf) 

1706 ticket = (self.pile, mtf) 

1707 return ticket 

1708 else: 

1709 return (None, None) 

1710 

1711 def release_data(self, tickets): 

1712 for ticket in tickets: 

1713 pile, mtf = ticket 

1714 if pile is not None: 

1715 pile.remove_file(mtf) 

1716 

1717 def periodical(self): 

1718 if self.menuitem_watch.isChecked(): 

1719 if self.pile.reload_modified(): 

1720 self.update() 

1721 

1722 def get_pile(self): 

1723 return self.pile 

1724 

1725 def pile_changed(self, what, content): 

1726 self.pile_has_changed = True 

1727 self.pile_has_changed_signal.emit() 

1728 if self.automatic_updates: 

1729 self.update() 

1730 

1731 def set_gathering(self, gather=None, color=None): 

1732 

1733 if gather is None: 

1734 def gather_func(tr): 

1735 return tr.nslc_id 

1736 

1737 gather = (0, 1, 2, 3) 

1738 

1739 else: 

1740 def gather_func(tr): 

1741 return ( 

1742 self.ssort(tr) + tuple(tr.nslc_id[i] for i in gather)) 

1743 

1744 if color is None: 

1745 def color(tr): 

1746 return tr.location 

1747 

1748 self.gather = gather_func 

1749 keys = self.pile.gather_keys(gather_func, self.trace_filter) 

1750 

1751 self.color_gather = color 

1752 self.color_keys = self.pile.gather_keys(color) 

1753 previous_ntracks = self.ntracks 

1754 self.set_ntracks(len(keys)) 

1755 

1756 if self.shown_tracks_range is None or \ 

1757 previous_ntracks == 0 or \ 

1758 self.show_all: 

1759 

1760 low, high = 0, min(self.ntracks_shown_max, self.ntracks) 

1761 key_at_top = None 

1762 n = high-low 

1763 

1764 else: 

1765 low, high = self.shown_tracks_range 

1766 key_at_top = self.track_keys[low] 

1767 n = high-low 

1768 

1769 self.track_keys = sorted(keys) 

1770 

1771 track_patterns = [] 

1772 for k in self.track_keys: 

1773 pat = ['*', '*', '*', '*'] 

1774 for i, j in enumerate(gather): 

1775 pat[j] = k[-len(gather)+i] 

1776 

1777 track_patterns.append(pat) 

1778 

1779 self.track_patterns = track_patterns 

1780 

1781 if key_at_top is not None: 

1782 try: 

1783 ind = self.track_keys.index(key_at_top) 

1784 low = ind 

1785 high = low+n 

1786 except Exception: 

1787 pass 

1788 

1789 self.set_tracks_range((low, high)) 

1790 

1791 self.key_to_row = dict( 

1792 [(key, i) for (i, key) in enumerate(self.track_keys)]) 

1793 

1794 def inrange(x, r): 

1795 return r[0] <= x and x < r[1] 

1796 

1797 def trace_selector(trace): 

1798 gt = self.gather(trace) 

1799 return ( 

1800 gt in self.key_to_row and 

1801 inrange(self.key_to_row[gt], self.shown_tracks_range)) 

1802 

1803 self.trace_selector = lambda x: \ 

1804 (self.trace_filter is None or self.trace_filter(x)) \ 

1805 and trace_selector(x) 

1806 

1807 if self.tmin == working_system_time_range[0] and \ 

1808 self.tmax == working_system_time_range[1] or \ 

1809 self.show_all: 

1810 

1811 tmin, tmax = self.pile.get_tmin(), self.pile.get_tmax() 

1812 if tmin is not None and tmax is not None: 

1813 tlen = (tmax - tmin) 

1814 tpad = tlen * 5./self.width() 

1815 self.set_time_range(tmin-tpad, tmax+tpad) 

1816 

1817 def set_time_range(self, tmin, tmax): 

1818 if tmin is None: 

1819 tmin = initial_time_range[0] 

1820 

1821 if tmax is None: 

1822 tmax = initial_time_range[1] 

1823 

1824 if tmin > tmax: 

1825 tmin, tmax = tmax, tmin 

1826 

1827 if tmin == tmax: 

1828 tmin -= 1. 

1829 tmax += 1. 

1830 

1831 tmin = max(working_system_time_range[0], tmin) 

1832 tmax = min(working_system_time_range[1], tmax) 

1833 

1834 min_deltat = self.content_deltat_range()[0] 

1835 if (tmax - tmin < min_deltat): 

1836 m = (tmin + tmax) / 2. 

1837 tmin = m - min_deltat/2. 

1838 tmax = m + min_deltat/2. 

1839 

1840 self.time_projection.set_in_range(tmin, tmax) 

1841 self.tmin, self.tmax = tmin, tmax 

1842 

1843 def get_time_range(self): 

1844 return self.tmin, self.tmax 

1845 

1846 def ypart(self, y): 

1847 if y < self.ax_height: 

1848 return -1 

1849 elif y > self.height()-self.ax_height: 

1850 return 1 

1851 else: 

1852 return 0 

1853 

1854 def time_fractional_digits(self): 

1855 min_deltat = self.content_deltat_range()[0] 

1856 return min(9, max(1, int(-math.floor(math.log10(min_deltat)))+2)) 

1857 

1858 def write_markers(self, fn=None): 

1859 caption = 'Choose a file name to write markers' 

1860 if not fn: 

1861 fn, _ = qw.QFileDialog.getSaveFileName( 

1862 self, caption, options=qfiledialog_options) 

1863 if fn: 

1864 try: 

1865 Marker.save_markers( 

1866 self.markers, fn, 

1867 fdigits=self.time_fractional_digits()) 

1868 

1869 except Exception as e: 

1870 self.fail('Failed to write marker file: %s' % str(e)) 

1871 

1872 def write_selected_markers(self, fn=None): 

1873 caption = 'Choose a file name to write selected markers' 

1874 if not fn: 

1875 fn, _ = qw.QFileDialog.getSaveFileName( 

1876 self, caption, options=qfiledialog_options) 

1877 if fn: 

1878 try: 

1879 Marker.save_markers( 

1880 self.iter_selected_markers(), 

1881 fn, 

1882 fdigits=self.time_fractional_digits()) 

1883 

1884 except Exception as e: 

1885 self.fail('Failed to write marker file: %s' % str(e)) 

1886 

1887 def read_events(self, fn=None): 

1888 ''' 

1889 Open QFileDialog to open, read and add 

1890 :py:class:`pyrocko.model.Event` instances and their marker 

1891 representation to the pile viewer. 

1892 ''' 

1893 caption = 'Selet one or more files to open' 

1894 if not fn: 

1895 fn, _ = qw.QFileDialog.getOpenFileName( 

1896 self, caption, options=qfiledialog_options) 

1897 if fn: 

1898 try: 

1899 self.add_events(pyrocko.model.load_events(fn)) 

1900 self.associate_phases_to_events() 

1901 

1902 except Exception as e: 

1903 self.fail('Failed to read event file: %s' % str(e)) 

1904 

1905 def read_markers(self, fn=None): 

1906 ''' 

1907 Open QFileDialog to open, read and add markers to the pile viewer. 

1908 ''' 

1909 caption = 'Selet one or more marker files to open' 

1910 if not fn: 

1911 fn, _ = qw.QFileDialog.getOpenFileName( 

1912 self, caption, options=qfiledialog_options) 

1913 if fn: 

1914 try: 

1915 self.add_markers(Marker.load_markers(fn)) 

1916 self.associate_phases_to_events() 

1917 

1918 except Exception as e: 

1919 self.fail('Failed to read marker file: %s' % str(e)) 

1920 

1921 def associate_phases_to_events(self): 

1922 associate_phases_to_events(self.markers) 

1923 

1924 def add_marker(self, marker): 

1925 # need index to inform QAbstactTableModel about upcoming change, 

1926 # but have to restore current state in order to not cause problems 

1927 self.markers.insert(marker) 

1928 i = self.markers.remove(marker) 

1929 

1930 self.begin_markers_add.emit(i, i) 

1931 self.markers.insert(marker) 

1932 self.end_markers_add.emit() 

1933 self.markers_deltat_max = max( 

1934 self.markers_deltat_max, marker.tmax - marker.tmin) 

1935 

1936 def add_markers(self, markers): 

1937 if not self.markers: 

1938 self.begin_markers_add.emit(0, len(markers) - 1) 

1939 self.markers.insert_many(markers) 

1940 self.end_markers_add.emit() 

1941 self.update_markers_deltat_max() 

1942 else: 

1943 for marker in markers: 

1944 self.add_marker(marker) 

1945 

1946 def update_markers_deltat_max(self): 

1947 if self.markers: 

1948 self.markers_deltat_max = max( 

1949 marker.tmax - marker.tmin for marker in self.markers) 

1950 

1951 def remove_marker(self, marker): 

1952 ''' 

1953 Remove a ``marker`` from the :py:class:`PileViewer`. 

1954 

1955 :param marker: 

1956 :py:class:`~pyrocko.gui.snuffler.marker.Marker` (or subclass) 

1957 instance 

1958 ''' 

1959 

1960 if marker is self.active_event_marker: 

1961 self.deactivate_event_marker() 

1962 

1963 try: 

1964 i = self.markers.index(marker) 

1965 self.begin_markers_remove.emit(i, i) 

1966 self.markers.remove_at(i) 

1967 self.end_markers_remove.emit() 

1968 except ValueError: 

1969 pass 

1970 

1971 def remove_markers(self, markers): 

1972 ''' 

1973 Remove a list of ``markers`` from the :py:class:`PileViewer`. 

1974 

1975 :param markers: 

1976 list of :py:class:`~pyrocko.gui.snuffler.marker.Marker` (or 

1977 subclass) instances 

1978 ''' 

1979 

1980 if markers is self.markers: 

1981 markers = list(markers) 

1982 

1983 for marker in markers: 

1984 self.remove_marker(marker) 

1985 

1986 self.update_markers_deltat_max() 

1987 

1988 def remove_selected_markers(self): 

1989 def delete_segment(istart, iend): 

1990 self.begin_markers_remove.emit(istart, iend-1) 

1991 for _ in range(iend - istart): 

1992 self.markers.remove_at(istart) 

1993 

1994 self.end_markers_remove.emit() 

1995 

1996 istart = None 

1997 ipos = 0 

1998 markers = self.markers 

1999 nmarkers = len(self.markers) 

2000 while ipos < nmarkers: 

2001 marker = markers[ipos] 

2002 if marker.is_selected(): 

2003 if marker is self.active_event_marker: 

2004 self.deactivate_event_marker() 

2005 

2006 if istart is None: 

2007 istart = ipos 

2008 else: 

2009 if istart is not None: 

2010 delete_segment(istart, ipos) 

2011 nmarkers -= ipos - istart 

2012 ipos = istart - 1 

2013 istart = None 

2014 

2015 ipos += 1 

2016 

2017 if istart is not None: 

2018 delete_segment(istart, ipos) 

2019 

2020 self.update_markers_deltat_max() 

2021 

2022 def selected_markers(self): 

2023 return [marker for marker in self.markers if marker.is_selected()] 

2024 

2025 def iter_selected_markers(self): 

2026 for marker in self.markers: 

2027 if marker.is_selected(): 

2028 yield marker 

2029 

2030 def get_markers(self): 

2031 return self.markers 

2032 

2033 def mousePressEvent(self, mouse_ev): 

2034 '' 

2035 self.show_all = False 

2036 point = self.mapFromGlobal(mouse_ev.globalPos()) 

2037 

2038 if mouse_ev.button() == qc.Qt.LeftButton: 

2039 marker = self.marker_under_cursor(point.x(), point.y()) 

2040 if self.picking: 

2041 if self.picking_down is None: 

2042 self.picking_down = ( 

2043 self.time_projection.rev(mouse_ev.x()), 

2044 mouse_ev.y()) 

2045 

2046 elif marker is not None: 

2047 if not (mouse_ev.modifiers() & qc.Qt.ShiftModifier): 

2048 self.deselect_all() 

2049 marker.selected = True 

2050 self.emit_selected_markers() 

2051 self.update() 

2052 else: 

2053 self.track_start = mouse_ev.x(), mouse_ev.y() 

2054 self.track_trange = self.tmin, self.tmax 

2055 

2056 if mouse_ev.button() == qc.Qt.RightButton \ 

2057 and isinstance(self.menu, qw.QMenu): 

2058 self.menu.exec_(qg.QCursor.pos()) 

2059 self.update_status() 

2060 

2061 def mouseReleaseEvent(self, mouse_ev): 

2062 '' 

2063 if self.ignore_releases: 

2064 self.ignore_releases -= 1 

2065 return 

2066 

2067 if self.picking: 

2068 self.stop_picking(mouse_ev.x(), mouse_ev.y()) 

2069 self.emit_selected_markers() 

2070 

2071 if self.track_start: 

2072 self.update() 

2073 

2074 self.track_start = None 

2075 self.track_trange = None 

2076 self.update_status() 

2077 

2078 def mouseDoubleClickEvent(self, mouse_ev): 

2079 '' 

2080 self.show_all = False 

2081 self.start_picking(None) 

2082 self.ignore_releases = 1 

2083 

2084 def mouseMoveEvent(self, mouse_ev): 

2085 '' 

2086 point = self.mapFromGlobal(mouse_ev.globalPos()) 

2087 

2088 if self.picking: 

2089 self.update_picking(point.x(), point.y()) 

2090 

2091 elif self.track_start is not None: 

2092 x0, y0 = self.track_start 

2093 dx = (point.x() - x0)/float(self.width()) 

2094 dy = (point.y() - y0)/float(self.height()) 

2095 if self.ypart(y0) == 1: 

2096 dy = 0 

2097 

2098 tmin0, tmax0 = self.track_trange 

2099 

2100 scale = math.exp(-dy*5.) 

2101 dtr = scale*(tmax0-tmin0) - (tmax0-tmin0) 

2102 frac = x0/float(self.width()) 

2103 dt = dx*(tmax0-tmin0)*scale 

2104 

2105 self.interrupt_following() 

2106 self.set_time_range( 

2107 tmin0 - dt - dtr*frac, 

2108 tmax0 - dt + dtr*(1.-frac)) 

2109 

2110 self.update() 

2111 else: 

2112 self.hoovering(point.x(), point.y()) 

2113 

2114 self.update_status() 

2115 

2116 def nslc_ids_under_cursor(self, x, y): 

2117 ftrack = self.track_to_screen.rev(y) 

2118 nslc_ids = self.get_nslc_ids_for_track(ftrack) 

2119 return nslc_ids 

2120 

2121 def marker_under_cursor(self, x, y): 

2122 mouset = self.time_projection.rev(x) 

2123 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width() 

2124 relevant_nslc_ids = None 

2125 for marker in self.markers: 

2126 if marker.kind not in self.visible_marker_kinds: 

2127 continue 

2128 

2129 if (abs(mouset-marker.tmin) < deltat or 

2130 abs(mouset-marker.tmax) < deltat): 

2131 

2132 if relevant_nslc_ids is None: 

2133 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y) 

2134 

2135 marker_nslc_ids = marker.get_nslc_ids() 

2136 if not marker_nslc_ids: 

2137 return marker 

2138 

2139 for nslc_id in marker_nslc_ids: 

2140 if nslc_id in relevant_nslc_ids: 

2141 return marker 

2142 

2143 def hoovering(self, x, y): 

2144 mouset = self.time_projection.rev(x) 

2145 deltat = (self.tmax-self.tmin)*self.click_tolerance/self.width() 

2146 needupdate = False 

2147 haveone = False 

2148 relevant_nslc_ids = self.nslc_ids_under_cursor(x, y) 

2149 for marker in self.markers: 

2150 if marker.kind not in self.visible_marker_kinds: 

2151 continue 

2152 

2153 state = abs(mouset-marker.tmin) < deltat or \ 

2154 abs(mouset-marker.tmax) < deltat and not haveone 

2155 

2156 if state: 

2157 xstate = False 

2158 

2159 marker_nslc_ids = marker.get_nslc_ids() 

2160 if not marker_nslc_ids: 

2161 xstate = True 

2162 

2163 for nslc in relevant_nslc_ids: 

2164 if marker.match_nslc(nslc): 

2165 xstate = True 

2166 

2167 state = xstate 

2168 

2169 if state: 

2170 haveone = True 

2171 oldstate = marker.is_alerted() 

2172 if oldstate != state: 

2173 needupdate = True 

2174 marker.set_alerted(state) 

2175 if state: 

2176 self.window().status_messages.set( 

2177 'marker', marker.hoover_message()) 

2178 

2179 if needupdate: 

2180 self.update() 

2181 

2182 self.update_status() 

2183 

2184 def event(self, event): 

2185 '' 

2186 if event.type() == qc.QEvent.KeyPress: 

2187 self.keyPressEvent(event) 

2188 return True 

2189 else: 

2190 return base.event(self, event) 

2191 

2192 def keyPressEvent(self, key_event): 

2193 '' 

2194 self.show_all = False 

2195 dt = self.tmax - self.tmin 

2196 tmid = (self.tmin + self.tmax) / 2. 

2197 

2198 key = key_event.key() 

2199 try: 

2200 keytext = str(key_event.text()) 

2201 except UnicodeEncodeError: 

2202 return 

2203 

2204 if key == qc.Qt.Key_Space: 

2205 self.interrupt_following() 

2206 self.set_time_range(self.tmin+dt, self.tmax+dt) 

2207 

2208 elif key == qc.Qt.Key_Up: 

2209 for m in self.selected_markers(): 

2210 if isinstance(m, PhaseMarker): 

2211 if key_event.modifiers() & qc.Qt.ShiftModifier: 

2212 p = 0 

2213 else: 

2214 p = 1 if m.get_polarity() != 1 else None 

2215 m.set_polarity(p) 

2216 

2217 elif key == qc.Qt.Key_Down: 

2218 for m in self.selected_markers(): 

2219 if isinstance(m, PhaseMarker): 

2220 if key_event.modifiers() & qc.Qt.ShiftModifier: 

2221 p = 0 

2222 else: 

2223 p = -1 if m.get_polarity() != -1 else None 

2224 m.set_polarity(p) 

2225 

2226 elif key == qc.Qt.Key_B: 

2227 dt = self.tmax - self.tmin 

2228 self.interrupt_following() 

2229 self.set_time_range(self.tmin-dt, self.tmax-dt) 

2230 

2231 elif key in (qc.Qt.Key_Tab, qc.Qt.Key_Backtab): 

2232 self.interrupt_following() 

2233 

2234 tgo = None 

2235 

2236 class TraceDummy(object): 

2237 def __init__(self, marker): 

2238 self._marker = marker 

2239 

2240 @property 

2241 def nslc_id(self): 

2242 return self._marker.one_nslc() 

2243 

2244 def marker_to_itrack(marker): 

2245 try: 

2246 return self.key_to_row.get( 

2247 self.gather(TraceDummy(marker)), -1) 

2248 

2249 except MarkerOneNSLCRequired: 

2250 return -1 

2251 

2252 emarker, pmarkers = self.get_active_markers() 

2253 pmarkers = [ 

2254 m for m in pmarkers if m.kind in self.visible_marker_kinds] 

2255 pmarkers.sort(key=lambda m: ( 

2256 marker_to_itrack(m), (m.tmin + m.tmax) / 2.0)) 

2257 

2258 if key == qc.Qt.Key_Backtab: 

2259 pmarkers.reverse() 

2260 

2261 smarkers = self.selected_markers() 

2262 iselected = [] 

2263 for sm in smarkers: 

2264 try: 

2265 iselected.append(pmarkers.index(sm)) 

2266 except ValueError: 

2267 pass 

2268 

2269 if iselected: 

2270 icurrent = max(iselected) + 1 

2271 else: 

2272 icurrent = 0 

2273 

2274 if icurrent < len(pmarkers): 

2275 self.deselect_all() 

2276 cmarker = pmarkers[icurrent] 

2277 cmarker.selected = True 

2278 tgo = cmarker.tmin 

2279 if not self.tmin < tgo < self.tmax: 

2280 self.set_time_range(tgo-dt/2., tgo+dt/2.) 

2281 

2282 itrack = marker_to_itrack(cmarker) 

2283 if itrack != -1: 

2284 if itrack < self.shown_tracks_range[0]: 

2285 self.scroll_tracks( 

2286 - (self.shown_tracks_range[0] - itrack)) 

2287 elif self.shown_tracks_range[1] <= itrack: 

2288 self.scroll_tracks( 

2289 itrack - self.shown_tracks_range[1]+1) 

2290 

2291 if itrack not in self.track_to_nslc_ids: 

2292 self.go_to_selection() 

2293 

2294 elif keytext in ('p', 'n', 'P', 'N'): 

2295 smarkers = self.selected_markers() 

2296 tgo = None 

2297 dir = str(keytext) 

2298 if smarkers: 

2299 tmid = smarkers[0].tmin 

2300 for smarker in smarkers: 

2301 if dir == 'n': 

2302 tmid = max(smarker.tmin, tmid) 

2303 else: 

2304 tmid = min(smarker.tmin, tmid) 

2305 

2306 tgo = tmid 

2307 

2308 if dir.lower() == 'n': 

2309 for marker in sorted( 

2310 self.markers, 

2311 key=operator.attrgetter('tmin')): 

2312 

2313 t = marker.tmin 

2314 if t > tmid and \ 

2315 marker.kind in self.visible_marker_kinds and \ 

2316 (dir == 'n' or 

2317 isinstance(marker, EventMarker)): 

2318 

2319 self.deselect_all() 

2320 marker.selected = True 

2321 tgo = t 

2322 break 

2323 else: 

2324 for marker in sorted( 

2325 self.markers, 

2326 key=operator.attrgetter('tmin'), 

2327 reverse=True): 

2328 

2329 t = marker.tmin 

2330 if t < tmid and \ 

2331 marker.kind in self.visible_marker_kinds and \ 

2332 (dir == 'p' or 

2333 isinstance(marker, EventMarker)): 

2334 self.deselect_all() 

2335 marker.selected = True 

2336 tgo = t 

2337 break 

2338 

2339 if tgo is not None: 

2340 self.interrupt_following() 

2341 self.set_time_range(tgo-dt/2., tgo+dt/2.) 

2342 

2343 elif keytext == 'r': 

2344 if self.pile.reload_modified(): 

2345 self.reloaded = True 

2346 

2347 elif keytext == 'R': 

2348 self.setup_snufflings() 

2349 

2350 elif key == qc.Qt.Key_Backspace: 

2351 self.remove_selected_markers() 

2352 

2353 elif keytext == 'a': 

2354 for marker in self.markers: 

2355 if ((self.tmin <= marker.tmin <= self.tmax or 

2356 self.tmin <= marker.tmax <= self.tmax) and 

2357 marker.kind in self.visible_marker_kinds): 

2358 marker.selected = True 

2359 else: 

2360 marker.selected = False 

2361 

2362 elif keytext == 'A': 

2363 for marker in self.markers: 

2364 if marker.kind in self.visible_marker_kinds: 

2365 marker.selected = True 

2366 

2367 elif keytext == 'd': 

2368 self.deselect_all() 

2369 

2370 elif keytext == 'E': 

2371 self.deactivate_event_marker() 

2372 

2373 elif keytext == 'e': 

2374 markers = self.selected_markers() 

2375 event_markers_in_spe = [ 

2376 marker for marker in markers 

2377 if not isinstance(marker, PhaseMarker)] 

2378 

2379 phase_markers = [ 

2380 marker for marker in markers 

2381 if isinstance(marker, PhaseMarker)] 

2382 

2383 if len(event_markers_in_spe) == 1: 

2384 event_marker = event_markers_in_spe[0] 

2385 if not isinstance(event_marker, EventMarker): 

2386 nslcs = list(event_marker.nslc_ids) 

2387 lat, lon = 0.0, 0.0 

2388 old = self.get_active_event() 

2389 if len(nslcs) == 1: 

2390 lat, lon = self.station_latlon(NSLC(*nslcs[0])) 

2391 elif old is not None: 

2392 lat, lon = old.lat, old.lon 

2393 

2394 event_marker.convert_to_event_marker(lat, lon) 

2395 

2396 self.set_active_event_marker(event_marker) 

2397 event = event_marker.get_event() 

2398 for marker in phase_markers: 

2399 marker.set_event(event) 

2400 

2401 else: 

2402 for marker in event_markers_in_spe: 

2403 marker.convert_to_event_marker() 

2404 

2405 elif keytext in ('0', '1', '2', '3', '4', '5', '6', '7'): 

2406 for marker in self.selected_markers(): 

2407 marker.set_kind(int(keytext)) 

2408 self.emit_selected_markers() 

2409 

2410 elif key in fkey_map: 

2411 self.handle_fkeys(key) 

2412 

2413 elif key == qc.Qt.Key_Escape: 

2414 if self.picking: 

2415 self.stop_picking(0, 0, abort=True) 

2416 

2417 elif key == qc.Qt.Key_PageDown: 

2418 self.scroll_tracks( 

2419 self.shown_tracks_range[1]-self.shown_tracks_range[0]) 

2420 

2421 elif key == qc.Qt.Key_PageUp: 

2422 self.scroll_tracks( 

2423 self.shown_tracks_range[0]-self.shown_tracks_range[1]) 

2424 

2425 elif key == qc.Qt.Key_Plus: 

2426 self.zoom_tracks(0., 1.) 

2427 

2428 elif key == qc.Qt.Key_Minus: 

2429 self.zoom_tracks(0., -1.) 

2430 

2431 elif key == qc.Qt.Key_Equal: 

2432 ntracks_shown = self.shown_tracks_range[1] - \ 

2433 self.shown_tracks_range[0] 

2434 dtracks = self.initial_ntracks_shown_max - ntracks_shown 

2435 self.zoom_tracks(0., dtracks) 

2436 

2437 elif key == qc.Qt.Key_Colon: 

2438 self.want_input.emit() 

2439 

2440 elif keytext == 'f': 

2441 self.toggle_fullscreen() 

2442 

2443 elif keytext == 'g': 

2444 self.go_to_selection() 

2445 

2446 elif keytext == 'G': 

2447 self.go_to_selection(tight=True) 

2448 

2449 elif keytext == 'm': 

2450 self.toggle_marker_editor() 

2451 

2452 elif keytext == 'c': 

2453 self.toggle_main_controls() 

2454 

2455 elif key_event.key() in (qc.Qt.Key_Left, qc.Qt.Key_Right): 

2456 dir = 1 

2457 amount = 1 

2458 if key_event.key() == qc.Qt.Key_Left: 

2459 dir = -1 

2460 if key_event.modifiers() & qc.Qt.ShiftModifier: 

2461 amount = 10 

2462 self.nudge_selected_markers(dir*amount) 

2463 else: 

2464 super().keyPressEvent(key_event) 

2465 

2466 if keytext != '' and keytext in 'degaApPnN': 

2467 self.emit_selected_markers() 

2468 

2469 self.update() 

2470 self.update_status() 

2471 

2472 def handle_fkeys(self, key): 

2473 self.set_phase_kind( 

2474 self.selected_markers(), 

2475 fkey_map[key] + 1) 

2476 self.emit_selected_markers() 

2477 

2478 def emit_selected_markers(self): 

2479 ibounds = [] 

2480 last_selected = False 

2481 for imarker, marker in enumerate(self.markers): 

2482 this_selected = marker.is_selected() 

2483 if this_selected != last_selected: 

2484 ibounds.append(imarker) 

2485 

2486 last_selected = this_selected 

2487 

2488 if last_selected: 

2489 ibounds.append(len(self.markers)) 

2490 

2491 chunks = list(zip(ibounds[::2], ibounds[1::2])) 

2492 self.n_selected_markers = sum( 

2493 chunk[1] - chunk[0] for chunk in chunks) 

2494 self.marker_selection_changed.emit(chunks) 

2495 

2496 def toggle_marker_editor(self): 

2497 self.panel_parent.toggle_marker_editor() 

2498 

2499 def toggle_main_controls(self): 

2500 self.panel_parent.toggle_main_controls() 

2501 

2502 def nudge_selected_markers(self, npixels): 

2503 a, b = self.time_projection.ur 

2504 c, d = self.time_projection.xr 

2505 for marker in self.selected_markers(): 

2506 if not isinstance(marker, EventMarker): 

2507 marker.tmin += npixels * (d-c)/b 

2508 marker.tmax += npixels * (d-c)/b 

2509 

2510 def toggle_fullscreen(self): 

2511 if self.window().windowState() & qc.Qt.WindowFullScreen or \ 

2512 self.window().windowState() & qc.Qt.WindowMaximized: 

2513 self.window().showNormal() 

2514 else: 

2515 if is_macos: 

2516 self.window().showMaximized() 

2517 else: 

2518 self.window().showFullScreen() 

2519 

2520 def about(self): 

2521 fn = pyrocko.util.data_file('snuffler.png') 

2522 with open(pyrocko.util.data_file('snuffler_about.html')) as f: 

2523 txt = f.read() 

2524 label = qw.QLabel(txt % {'logo': fn}) 

2525 label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter) 

2526 self.show_doc('About', [label], target='tab') 

2527 

2528 def help(self): 

2529 class MyScrollArea(qw.QScrollArea): 

2530 

2531 def sizeHint(self): 

2532 s = qc.QSize() 

2533 s.setWidth(self.widget().sizeHint().width()) 

2534 s.setHeight(self.widget().sizeHint().height()) 

2535 return s 

2536 

2537 with open(pyrocko.util.data_file( 

2538 'snuffler_help.html')) as f: 

2539 hcheat = qw.QLabel(f.read()) 

2540 

2541 with open(pyrocko.util.data_file( 

2542 'snuffler_help_epilog.html')) as f: 

2543 hepilog = qw.QLabel(f.read()) 

2544 

2545 for h in [hcheat, hepilog]: 

2546 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignHCenter) 

2547 h.setWordWrap(True) 

2548 

2549 self.show_doc('Help', [hcheat, hepilog], target='panel') 

2550 

2551 def show_doc(self, name, labels, target='panel'): 

2552 scroller = qw.QScrollArea() 

2553 frame = qw.QFrame(scroller) 

2554 frame.setLineWidth(0) 

2555 layout = qw.QVBoxLayout() 

2556 layout.setContentsMargins(0, 0, 0, 0) 

2557 layout.setSpacing(0) 

2558 frame.setLayout(layout) 

2559 scroller.setWidget(frame) 

2560 scroller.setWidgetResizable(True) 

2561 frame.setBackgroundRole(qg.QPalette.Base) 

2562 for h in labels: 

2563 h.setParent(frame) 

2564 h.setMargin(3) 

2565 h.setTextInteractionFlags( 

2566 qc.Qt.LinksAccessibleByMouse | qc.Qt.TextSelectableByMouse) 

2567 h.setBackgroundRole(qg.QPalette.Base) 

2568 layout.addWidget(h) 

2569 h.linkActivated.connect( 

2570 self.open_link) 

2571 

2572 if self.panel_parent is not None: 

2573 if target == 'panel': 

2574 self.panel_parent.add_panel( 

2575 name, scroller, True, volatile=False) 

2576 else: 

2577 self.panel_parent.add_tab(name, scroller) 

2578 

2579 def open_link(self, link): 

2580 qg.QDesktopServices.openUrl(qc.QUrl(link)) 

2581 

2582 def wheelEvent(self, wheel_event): 

2583 '' 

2584 self.wheel_pos += wheel_event.angleDelta().y() 

2585 

2586 n = self.wheel_pos // 120 

2587 self.wheel_pos = self.wheel_pos % 120 

2588 if n == 0: 

2589 return 

2590 

2591 amount = max( 

2592 1., 

2593 abs(self.shown_tracks_range[0]-self.shown_tracks_range[1])/5.) 

2594 wdelta = amount * n 

2595 

2596 trmin, trmax = self.track_to_screen.get_in_range() 

2597 anchor = (self.track_to_screen.rev(wheel_event.y())-trmin) \ 

2598 / (trmax-trmin) 

2599 

2600 if wheel_event.modifiers() & qc.Qt.ControlModifier: 

2601 self.zoom_tracks(anchor, wdelta) 

2602 else: 

2603 self.scroll_tracks(-wdelta) 

2604 

2605 def dragEnterEvent(self, event): 

2606 '' 

2607 if event.mimeData().hasUrls(): 

2608 if any(url.toLocalFile() for url in event.mimeData().urls()): 

2609 event.setDropAction(qc.Qt.LinkAction) 

2610 event.accept() 

2611 

2612 def dropEvent(self, event): 

2613 '' 

2614 if event.mimeData().hasUrls(): 

2615 paths = list( 

2616 str(url.toLocalFile()) for url in event.mimeData().urls()) 

2617 event.acceptProposedAction() 

2618 self.load(paths) 

2619 

2620 def get_phase_name(self, kind): 

2621 return self.config.get_phase_name(kind) 

2622 

2623 def set_phase_kind(self, markers, kind): 

2624 phasename = self.get_phase_name(kind) 

2625 

2626 for marker in markers: 

2627 if isinstance(marker, PhaseMarker): 

2628 if kind == 10: 

2629 marker.convert_to_marker() 

2630 else: 

2631 marker.set_phasename(phasename) 

2632 marker.set_event(self.get_active_event()) 

2633 

2634 elif isinstance(marker, EventMarker): 

2635 pass 

2636 

2637 else: 

2638 if kind != 10: 

2639 event = self.get_active_event() 

2640 marker.convert_to_phase_marker( 

2641 event, phasename, None, False) 

2642 

2643 def set_ntracks(self, ntracks): 

2644 if self.ntracks != ntracks: 

2645 self.ntracks = ntracks 

2646 if self.shown_tracks_range is not None: 

2647 low, high = self.shown_tracks_range 

2648 else: 

2649 low, high = 0, self.ntracks 

2650 

2651 self.tracks_range_changed.emit(self.ntracks, low, high) 

2652 

2653 def set_tracks_range(self, range, start=None): 

2654 

2655 low, high = range 

2656 low = min(self.ntracks-1, low) 

2657 high = min(self.ntracks, high) 

2658 low = max(0, low) 

2659 high = max(1, high) 

2660 

2661 if start is None: 

2662 start = float(low) 

2663 

2664 if self.shown_tracks_range != (low, high): 

2665 self.shown_tracks_range = low, high 

2666 self.shown_tracks_start = start 

2667 

2668 self.tracks_range_changed.emit(self.ntracks, low, high) 

2669 

2670 def scroll_tracks(self, shift): 

2671 shown = self.shown_tracks_range 

2672 shiftmin = -shown[0] 

2673 shiftmax = self.ntracks-shown[1] 

2674 shift = max(shiftmin, shift) 

2675 shift = min(shiftmax, shift) 

2676 shown = shown[0] + shift, shown[1] + shift 

2677 

2678 self.set_tracks_range((int(shown[0]), int(shown[1]))) 

2679 

2680 self.update() 

2681 

2682 def zoom_tracks(self, anchor, delta): 

2683 ntracks_shown = self.shown_tracks_range[1] \ 

2684 - self.shown_tracks_range[0] 

2685 

2686 if (ntracks_shown == 1 and delta <= 0) or \ 

2687 (ntracks_shown == self.ntracks and delta >= 0): 

2688 return 

2689 

2690 ntracks_shown += int(round(delta)) 

2691 ntracks_shown = min(max(1, ntracks_shown), self.ntracks) 

2692 

2693 u = self.shown_tracks_start 

2694 nu = max(0., u-anchor*delta) 

2695 nv = nu + ntracks_shown 

2696 if nv > self.ntracks: 

2697 nu -= nv - self.ntracks 

2698 nv -= nv - self.ntracks 

2699 

2700 self.set_tracks_range((int(round(nu)), int(round(nv))), nu) 

2701 

2702 self.ntracks_shown_max = self.shown_tracks_range[1] \ 

2703 - self.shown_tracks_range[0] 

2704 

2705 self.update() 

2706 

2707 def content_time_range(self): 

2708 pile = self.get_pile() 

2709 tmin, tmax = pile.get_tmin(), pile.get_tmax() 

2710 if tmin is None: 

2711 tmin = initial_time_range[0] 

2712 if tmax is None: 

2713 tmax = initial_time_range[1] 

2714 

2715 return tmin, tmax 

2716 

2717 def content_deltat_range(self): 

2718 pile = self.get_pile() 

2719 

2720 deltatmin, deltatmax = pile.get_deltatmin(), pile.get_deltatmax() 

2721 

2722 if deltatmin is None or deltatmin == 0.0: 

2723 deltatmin = 0.001 

2724 

2725 if deltatmax is None: 

2726 deltatmax = 1000.0 

2727 

2728 return deltatmin, deltatmax 

2729 

2730 def make_good_looking_time_range(self, tmin, tmax, tight=False): 

2731 if tmax < tmin: 

2732 tmin, tmax = tmax, tmin 

2733 

2734 deltatmin = self.content_deltat_range()[0] 

2735 dt = deltatmin * self.visible_length * 0.95 

2736 

2737 if dt == 0.0: 

2738 dt = 1.0 

2739 

2740 if tight: 

2741 if tmax != tmin: 

2742 dtm = tmax - tmin 

2743 tmin -= dtm*0.1 

2744 tmax += dtm*0.1 

2745 return tmin, tmax 

2746 else: 

2747 tcenter = (tmin + tmax) / 2. 

2748 tmin = tcenter - 0.5*dt 

2749 tmax = tcenter + 0.5*dt 

2750 return tmin, tmax 

2751 

2752 if tmax-tmin < dt: 

2753 vmin, vmax = self.get_time_range() 

2754 dt = min(vmax - vmin, dt) 

2755 

2756 tcenter = (tmin+tmax)/2. 

2757 etmin, etmax = tmin, tmax 

2758 tmin = min(etmin, tcenter - 0.5*dt) 

2759 tmax = max(etmax, tcenter + 0.5*dt) 

2760 dtm = tmax-tmin 

2761 if etmin == tmin: 

2762 tmin -= dtm*0.1 

2763 if etmax == tmax: 

2764 tmax += dtm*0.1 

2765 

2766 else: 

2767 dtm = tmax-tmin 

2768 tmin -= dtm*0.1 

2769 tmax += dtm*0.1 

2770 

2771 return tmin, tmax 

2772 

2773 def go_to_selection(self, tight=False): 

2774 markers = self.selected_markers() 

2775 if markers: 

2776 tmax, tmin = self.content_time_range() 

2777 for marker in markers: 

2778 tmin = min(tmin, marker.tmin) 

2779 tmax = max(tmax, marker.tmax) 

2780 

2781 else: 

2782 if tight: 

2783 vmin, vmax = self.get_time_range() 

2784 tmin = tmax = (vmin + vmax) / 2. 

2785 else: 

2786 tmin, tmax = self.content_time_range() 

2787 

2788 tmin, tmax = self.make_good_looking_time_range( 

2789 tmin, tmax, tight=tight) 

2790 

2791 self.interrupt_following() 

2792 self.set_time_range(tmin, tmax) 

2793 self.update() 

2794 

2795 def go_to_time(self, t, tlen=None): 

2796 tmax = t 

2797 if tlen is not None: 

2798 tmax = t+tlen 

2799 tmin, tmax = self.make_good_looking_time_range(t, tmax) 

2800 self.interrupt_following() 

2801 self.set_time_range(tmin, tmax) 

2802 self.update() 

2803 

2804 def go_to_event_by_name(self, name): 

2805 for marker in self.markers: 

2806 if isinstance(marker, EventMarker): 

2807 event = marker.get_event() 

2808 if event.name and event.name.lower() == name.lower(): 

2809 tmin, tmax = self.make_good_looking_time_range( 

2810 event.time, event.time) 

2811 

2812 self.interrupt_following() 

2813 self.set_time_range(tmin, tmax) 

2814 

2815 def printit(self): 

2816 from ..qt_compat import qprint 

2817 printer = qprint.QPrinter() 

2818 printer.setOrientation(qprint.QPrinter.Landscape) 

2819 

2820 dialog = qprint.QPrintDialog(printer, self) 

2821 dialog.setWindowTitle('Print') 

2822 

2823 if dialog.exec_() != qw.QDialog.Accepted: 

2824 return 

2825 

2826 painter = qg.QPainter() 

2827 painter.begin(printer) 

2828 page = printer.pageRect() 

2829 self.drawit( 

2830 painter, printmode=False, w=page.width(), h=page.height()) 

2831 

2832 painter.end() 

2833 

2834 def savesvg(self, fn=None): 

2835 

2836 if not fn: 

2837 fn, _ = qw.QFileDialog.getSaveFileName( 

2838 self, 

2839 'Save as SVG|PNG', 

2840 os.path.expanduser(os.path.join('~', 'untitled.svg')), 

2841 'SVG|PNG (*.svg *.png)', 

2842 options=qfiledialog_options) 

2843 

2844 if fn == '': 

2845 return 

2846 

2847 fn = str(fn) 

2848 

2849 if fn.lower().endswith('.svg'): 

2850 try: 

2851 w, h = 842, 595 

2852 margin = 0.025 

2853 m = max(w, h)*margin 

2854 

2855 generator = qsvg.QSvgGenerator() 

2856 generator.setFileName(fn) 

2857 generator.setSize(qc.QSize(w, h)) 

2858 generator.setViewBox(qc.QRectF(-m, -m, w+2*m, h+2*m)) 

2859 

2860 painter = qg.QPainter() 

2861 painter.begin(generator) 

2862 self.drawit(painter, printmode=False, w=w, h=h) 

2863 painter.end() 

2864 

2865 except Exception as e: 

2866 self.fail('Failed to write SVG file: %s' % str(e)) 

2867 

2868 elif fn.lower().endswith('.png'): 

2869 pixmap = self.grab() 

2870 

2871 try: 

2872 pixmap.save(fn) 

2873 

2874 except Exception as e: 

2875 self.fail('Failed to write PNG file: %s' % str(e)) 

2876 

2877 else: 

2878 self.fail( 

2879 'Unsupported file type: filename must end with ".svg" or ' 

2880 '".png".') 

2881 

2882 def paintEvent(self, paint_ev): 

2883 ''' 

2884 Called by QT whenever widget needs to be painted. 

2885 ''' 

2886 # Prevent a problem on macos with QOpenGLWidget, where paintEvent 

2887 # was called twice (by different threads?), causing segfaults. 

2888 if self.in_paint_event: 

2889 logger.warning('Blocking reentrant call to paintEvent().') 

2890 return 

2891 

2892 self.in_paint_event = True 

2893 

2894 painter = qg.QPainter(self) 

2895 

2896 if self.menuitem_antialias.isChecked(): 

2897 painter.setRenderHint(qg.QPainter.Antialiasing) 

2898 

2899 self.drawit(painter) 

2900 

2901 logger.debug( 

2902 'Time spent drawing: ' 

2903 ' user:%.3f sys:%.3f children_user:%.3f' 

2904 ' childred_sys:%.3f elapsed:%.3f' % 

2905 (self.timer_draw - self.timer_cutout)) 

2906 

2907 logger.debug( 

2908 'Time spent processing:' 

2909 ' user:%.3f sys:%.3f children_user:%.3f' 

2910 ' childred_sys:%.3f elapsed:%.3f' % 

2911 self.timer_cutout.get()) 

2912 

2913 self.time_spent_painting = self.timer_draw.get()[-1] 

2914 self.time_last_painted = time.time() 

2915 self.in_paint_event = False 

2916 

2917 def determine_box_styles(self): 

2918 

2919 traces = list(self.pile.iter_traces()) 

2920 traces.sort(key=operator.attrgetter('full_id')) 

2921 istyle = 0 

2922 trace_styles = {} 

2923 for itr, tr in enumerate(traces): 

2924 if itr > 0: 

2925 other = traces[itr-1] 

2926 if not ( 

2927 other.nslc_id == tr.nslc_id 

2928 and other.deltat == tr.deltat 

2929 and abs(other.tmax - tr.tmin) 

2930 < gap_lap_tolerance*tr.deltat): 

2931 

2932 istyle += 1 

2933 

2934 trace_styles[tr.full_id, tr.deltat] = istyle 

2935 

2936 self.trace_styles = trace_styles 

2937 

2938 def draw_trace_boxes(self, p, time_projection, track_projections): 

2939 

2940 for v_projection in track_projections.values(): 

2941 v_projection.set_in_range(0., 1.) 

2942 

2943 def selector(x): 

2944 return x.overlaps(*time_projection.get_in_range()) 

2945 

2946 if self.trace_filter is not None: 

2947 def tselector(x): 

2948 return selector(x) and self.trace_filter(x) 

2949 

2950 else: 

2951 tselector = selector 

2952 

2953 traces = list(self.pile.iter_traces( 

2954 group_selector=selector, trace_selector=tselector)) 

2955 

2956 traces.sort(key=operator.attrgetter('full_id')) 

2957 

2958 def drawbox(itrack, istyle, traces): 

2959 v_projection = track_projections[itrack] 

2960 dvmin = v_projection(0.) 

2961 dvmax = v_projection(1.) 

2962 dtmin = time_projection.clipped(traces[0].tmin, 0) 

2963 dtmax = time_projection.clipped(traces[-1].tmax, 1) 

2964 

2965 style = box_styles[istyle % len(box_styles)] 

2966 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin) 

2967 p.fillRect(rect, style.fill_brush) 

2968 p.setPen(style.frame_pen) 

2969 p.drawRect(rect) 

2970 

2971 traces_by_style = {} 

2972 for itr, tr in enumerate(traces): 

2973 gt = self.gather(tr) 

2974 if gt not in self.key_to_row: 

2975 continue 

2976 

2977 itrack = self.key_to_row[gt] 

2978 if itrack not in track_projections: 

2979 continue 

2980 

2981 istyle = self.trace_styles.get((tr.full_id, tr.deltat), 0) 

2982 

2983 if len(traces) < 500: 

2984 drawbox(itrack, istyle, [tr]) 

2985 else: 

2986 if (itrack, istyle) not in traces_by_style: 

2987 traces_by_style[itrack, istyle] = [] 

2988 traces_by_style[itrack, istyle].append(tr) 

2989 

2990 for (itrack, istyle), traces in traces_by_style.items(): 

2991 drawbox(itrack, istyle, traces) 

2992 

2993 def draw_visible_markers( 

2994 self, p, vcenter_projection, primary_pen): 

2995 

2996 try: 

2997 markers = self.markers.with_key_in_limited( 

2998 self.tmin - self.markers_deltat_max, self.tmax, 2000) 

2999 

3000 except pyrocko.pile.TooMany: 

3001 tmin = self.markers[0].tmin 

3002 tmax = self.markers[-1].tmax 

3003 umin_view, umax_view = self.time_projection.get_out_range() 

3004 umin = max(umin_view, self.time_projection(tmin)) 

3005 umax = min(umax_view, self.time_projection(tmax)) 

3006 v0, _ = vcenter_projection.get_out_range() 

3007 label_bg = qg.QBrush(qg.QColor(255, 255, 255)) 

3008 

3009 p.save() 

3010 

3011 pen = qg.QPen(primary_pen) 

3012 pen.setWidth(2) 

3013 pen.setStyle(qc.Qt.DotLine) 

3014 # pat = [5., 3.] 

3015 # pen.setDashPattern(pat) 

3016 p.setPen(pen) 

3017 

3018 if self.n_selected_markers == len(self.markers): 

3019 s_selected = ' (all selected)' 

3020 elif self.n_selected_markers > 0: 

3021 s_selected = ' (%i selected)' % self.n_selected_markers 

3022 else: 

3023 s_selected = '' 

3024 

3025 draw_label( 

3026 p, umin+10., v0-10., 

3027 '%i Markers' % len(self.markers) + s_selected, 

3028 label_bg, 'LB') 

3029 

3030 line = qc.QLineF(umin, v0, umax, v0) 

3031 p.drawLine(line) 

3032 p.restore() 

3033 

3034 return 

3035 

3036 for marker in markers: 

3037 if marker.tmin < self.tmax and self.tmin < marker.tmax \ 

3038 and marker.kind in self.visible_marker_kinds: 

3039 

3040 marker.draw( 

3041 p, self.time_projection, vcenter_projection, 

3042 with_label=True) 

3043 

3044 def get_squirrel(self): 

3045 try: 

3046 return self.pile._squirrel 

3047 except AttributeError: 

3048 return None 

3049 

3050 def draw_coverage(self, p, time_projection, track_projections): 

3051 sq = self.get_squirrel() 

3052 if sq is None: 

3053 return 

3054 

3055 def drawbox(itrack, tmin, tmax, style): 

3056 v_projection = track_projections[itrack] 

3057 dvmin = v_projection(0.) 

3058 dvmax = v_projection(1.) 

3059 dtmin = time_projection.clipped(tmin, 0) 

3060 dtmax = time_projection.clipped(tmax, 1) 

3061 

3062 rect = qc.QRectF(dtmin, dvmin, float(dtmax-dtmin), dvmax-dvmin) 

3063 p.fillRect(rect, style.fill_brush) 

3064 p.setPen(style.frame_pen) 

3065 p.drawRect(rect) 

3066 

3067 pattern_list = [] 

3068 pattern_to_itrack = {} 

3069 for key in self.track_keys: 

3070 itrack = self.key_to_row[key] 

3071 if itrack not in track_projections: 

3072 continue 

3073 

3074 pattern = self.track_patterns[itrack] 

3075 pattern_to_itrack[tuple(pattern)] = itrack 

3076 pattern_list.append(tuple(pattern)) 

3077 

3078 vmin, vmax = self.get_time_range() 

3079 

3080 for kind in ['waveform', 'waveform_promise']: 

3081 for coverage in sq.get_coverage( 

3082 kind, vmin, vmax, pattern_list, limit=500): 

3083 itrack = pattern_to_itrack[coverage.pattern.nslc] 

3084 

3085 if coverage.changes is None: 

3086 drawbox( 

3087 itrack, coverage.tmin, coverage.tmax, 

3088 box_styles_coverage[kind][0]) 

3089 else: 

3090 t = None 

3091 pcount = 0 

3092 for tb, count in coverage.changes: 

3093 if t is not None and tb > t: 

3094 if pcount > 0: 

3095 drawbox( 

3096 itrack, t, tb, 

3097 box_styles_coverage[kind][ 

3098 min(len(box_styles_coverage)-1, 

3099 pcount)]) 

3100 

3101 t = tb 

3102 pcount = count 

3103 

3104 def drawit(self, p, printmode=False, w=None, h=None): 

3105 ''' 

3106 This performs the actual drawing. 

3107 ''' 

3108 

3109 self.timer_draw.start() 

3110 show_boxes = self.menuitem_showboxes.isChecked() 

3111 sq = self.get_squirrel() 

3112 

3113 if self.gather is None: 

3114 self.set_gathering() 

3115 

3116 if self.pile_has_changed: 

3117 

3118 if not self.sortingmode_change_delayed(): 

3119 self.sortingmode_change() 

3120 

3121 if show_boxes and sq is None: 

3122 self.determine_box_styles() 

3123 

3124 self.pile_has_changed = False 

3125 

3126 if h is None: 

3127 h = float(self.height()) 

3128 if w is None: 

3129 w = float(self.width()) 

3130 

3131 if printmode: 

3132 primary_color = (0, 0, 0) 

3133 else: 

3134 primary_color = pyrocko.plot.tango_colors['aluminium5'] 

3135 

3136 primary_pen = qg.QPen(qg.QColor(*primary_color)) 

3137 

3138 ax_h = float(self.ax_height) 

3139 

3140 vbottom_ax_projection = Projection() 

3141 vtop_ax_projection = Projection() 

3142 vcenter_projection = Projection() 

3143 

3144 self.time_projection.set_out_range(0., w) 

3145 vbottom_ax_projection.set_out_range(h-ax_h, h) 

3146 vtop_ax_projection.set_out_range(0., ax_h) 

3147 vcenter_projection.set_out_range(ax_h, h-ax_h) 

3148 vcenter_projection.set_in_range(0., 1.) 

3149 self.track_to_screen.set_out_range(ax_h, h-ax_h) 

3150 

3151 self.track_to_screen.set_in_range(*self.shown_tracks_range) 

3152 track_projections = {} 

3153 for i in range(*self.shown_tracks_range): 

3154 proj = Projection() 

3155 proj.set_out_range( 

3156 self.track_to_screen(i+0.05), 

3157 self.track_to_screen(i+1.-0.05)) 

3158 

3159 track_projections[i] = proj 

3160 

3161 if self.tmin > self.tmax: 

3162 return 

3163 

3164 self.time_projection.set_in_range(self.tmin, self.tmax) 

3165 vbottom_ax_projection.set_in_range(0, ax_h) 

3166 

3167 self.tax.drawit(p, self.time_projection, vbottom_ax_projection) 

3168 

3169 yscaler = pyrocko.plot.AutoScaler() 

3170 

3171 p.setPen(primary_pen) 

3172 

3173 font = qg.QFont() 

3174 font.setBold(True) 

3175 

3176 axannotfont = qg.QFont() 

3177 axannotfont.setBold(True) 

3178 axannotfont.setPointSize(8) 

3179 

3180 processed_traces = self.prepare_cutout2( 

3181 self.tmin, self.tmax, 

3182 trace_selector=self.trace_selector, 

3183 degap=self.menuitem_degap.isChecked(), 

3184 demean=self.menuitem_demean.isChecked()) 

3185 

3186 if not printmode and show_boxes: 

3187 if (self.view_mode is ViewMode.Wiggle) \ 

3188 or (self.view_mode is ViewMode.Waterfall 

3189 and not processed_traces): 

3190 

3191 if sq is None: 

3192 self.draw_trace_boxes( 

3193 p, self.time_projection, track_projections) 

3194 

3195 else: 

3196 self.draw_coverage( 

3197 p, self.time_projection, track_projections) 

3198 

3199 p.setFont(font) 

3200 label_bg = qg.QBrush(qg.QColor(255, 255, 255, 100)) 

3201 

3202 color_lookup = dict( 

3203 [(k, i) for (i, k) in enumerate(self.color_keys)]) 

3204 

3205 self.track_to_nslc_ids = {} 

3206 nticks = 0 

3207 annot_labels = [] 

3208 

3209 if self.view_mode is ViewMode.Waterfall and processed_traces: 

3210 waterfall = self.waterfall 

3211 waterfall.set_time_range(self.tmin, self.tmax) 

3212 waterfall.set_traces(processed_traces) 

3213 waterfall.set_cmap(self.waterfall_cmap) 

3214 waterfall.set_integrate(self.waterfall_integrate) 

3215 waterfall.set_clip( 

3216 self.waterfall_clip_min, self.waterfall_clip_max) 

3217 waterfall.show_absolute_values( 

3218 self.waterfall_show_absolute) 

3219 

3220 rect = qc.QRectF( 

3221 0, self.ax_height, 

3222 self.width(), self.height() - self.ax_height*2 

3223 ) 

3224 waterfall.draw_waterfall(p, rect=rect) 

3225 

3226 elif self.view_mode is ViewMode.Wiggle and processed_traces: 

3227 show_scales = self.menuitem_showscalerange.isChecked() \ 

3228 or self.menuitem_showscaleaxis.isChecked() 

3229 

3230 fm = qg.QFontMetrics(axannotfont, p.device()) 

3231 trackheight = self.track_to_screen(1.-0.05) \ 

3232 - self.track_to_screen(0.05) 

3233 

3234 nlinesavail = trackheight/float(fm.lineSpacing()) 

3235 

3236 nticks = max(3, min(nlinesavail * 0.5, 15)) \ 

3237 if self.menuitem_showscaleaxis.isChecked() \ 

3238 else 15 

3239 

3240 yscaler = pyrocko.plot.AutoScaler( 

3241 no_exp_interval=(-3, 2), approx_ticks=nticks, 

3242 snap=show_scales 

3243 and not self.menuitem_showscaleaxis.isChecked()) 

3244 

3245 data_ranges = pyrocko.trace.minmax( 

3246 processed_traces, 

3247 key=self.scaling_key, 

3248 mode=self.scaling_base[0], 

3249 outer_mode=self.scaling_base[1]) 

3250 

3251 if not self.menuitem_fixscalerange.isChecked(): 

3252 self.old_data_ranges = data_ranges 

3253 else: 

3254 data_ranges.update(self.old_data_ranges) 

3255 

3256 self.apply_scaling_hooks(data_ranges) 

3257 

3258 trace_to_itrack = {} 

3259 track_scaling_keys = {} 

3260 track_scaling_colors = {} 

3261 for trace in processed_traces: 

3262 gt = self.gather(trace) 

3263 if gt not in self.key_to_row: 

3264 continue 

3265 

3266 itrack = self.key_to_row[gt] 

3267 if itrack not in track_projections: 

3268 continue 

3269 

3270 trace_to_itrack[trace] = itrack 

3271 

3272 if itrack not in self.track_to_nslc_ids: 

3273 self.track_to_nslc_ids[itrack] = set() 

3274 

3275 self.track_to_nslc_ids[itrack].add(trace.nslc_id) 

3276 

3277 if itrack not in track_scaling_keys: 

3278 track_scaling_keys[itrack] = set() 

3279 

3280 scaling_key = self.scaling_key(trace) 

3281 track_scaling_keys[itrack].add(scaling_key) 

3282 

3283 color = pyrocko.plot.color( 

3284 color_lookup[self.color_gather(trace)]) 

3285 

3286 k = itrack, scaling_key 

3287 if k not in track_scaling_colors \ 

3288 and self.menuitem_colortraces.isChecked(): 

3289 track_scaling_colors[k] = color 

3290 else: 

3291 track_scaling_colors[k] = primary_color 

3292 

3293 # y axes, zero lines 

3294 trace_projections = {} 

3295 for itrack in list(track_projections.keys()): 

3296 if itrack not in track_scaling_keys: 

3297 continue 

3298 uoff = 0 

3299 for scaling_key in track_scaling_keys[itrack]: 

3300 data_range = data_ranges[scaling_key] 

3301 dymin, dymax = data_range 

3302 ymin, ymax, yinc = yscaler.make_scale( 

3303 (dymin/self.gain, dymax/self.gain)) 

3304 iexp = yscaler.make_exp(yinc) 

3305 factor = 10**iexp 

3306 trace_projection = track_projections[itrack].copy() 

3307 trace_projection.set_in_range(ymax, ymin) 

3308 trace_projections[itrack, scaling_key] = \ 

3309 trace_projection 

3310 umin, umax = self.time_projection.get_out_range() 

3311 vmin, vmax = trace_projection.get_out_range() 

3312 umax_zeroline = umax 

3313 uoffnext = uoff 

3314 

3315 if show_scales: 

3316 pen = qg.QPen(primary_pen) 

3317 k = itrack, scaling_key 

3318 if k in track_scaling_colors: 

3319 c = qg.QColor(*track_scaling_colors[ 

3320 itrack, scaling_key]) 

3321 

3322 pen.setColor(c) 

3323 

3324 p.setPen(pen) 

3325 if nlinesavail > 3: 

3326 if self.menuitem_showscaleaxis.isChecked(): 

3327 ymin_annot = math.ceil(ymin/yinc)*yinc 

3328 ny_annot = int( 

3329 math.floor(ymax/yinc) 

3330 - math.ceil(ymin/yinc)) + 1 

3331 

3332 for iy_annot in range(ny_annot): 

3333 y = ymin_annot + iy_annot*yinc 

3334 v = trace_projection(y) 

3335 line = qc.QLineF( 

3336 umax-10-uoff, v, umax-uoff, v) 

3337 

3338 p.drawLine(line) 

3339 if iy_annot == ny_annot - 1 \ 

3340 and iexp != 0: 

3341 sexp = ' &times; ' \ 

3342 '10<sup>%i</sup>' % iexp 

3343 else: 

3344 sexp = '' 

3345 

3346 snum = num_to_html(y/factor) 

3347 lab = Label( 

3348 p, 

3349 umax-20-uoff, 

3350 v, '%s%s' % (snum, sexp), 

3351 label_bg=None, 

3352 anchor='MR', 

3353 font=axannotfont, 

3354 color=c) 

3355 

3356 uoffnext = max( 

3357 lab.rect.width()+30., uoffnext) 

3358 

3359 annot_labels.append(lab) 

3360 if y == 0.: 

3361 umax_zeroline = \ 

3362 umax - 20 \ 

3363 - lab.rect.width() - 10 \ 

3364 - uoff 

3365 else: 

3366 if not show_boxes: 

3367 qpoints = make_QPolygonF( 

3368 [umax-20-uoff, 

3369 umax-10-uoff, 

3370 umax-10-uoff, 

3371 umax-20-uoff], 

3372 [vmax, vmax, vmin, vmin]) 

3373 p.drawPolyline(qpoints) 

3374 

3375 snum = num_to_html(ymin) 

3376 labmin = Label( 

3377 p, umax-15-uoff, vmax, snum, 

3378 label_bg=None, 

3379 anchor='BR', 

3380 font=axannotfont, 

3381 color=c) 

3382 

3383 annot_labels.append(labmin) 

3384 snum = num_to_html(ymax) 

3385 labmax = Label( 

3386 p, umax-15-uoff, vmin, snum, 

3387 label_bg=None, 

3388 anchor='TR', 

3389 font=axannotfont, 

3390 color=c) 

3391 

3392 annot_labels.append(labmax) 

3393 

3394 for lab in (labmin, labmax): 

3395 uoffnext = max( 

3396 lab.rect.width()+10., uoffnext) 

3397 

3398 if self.menuitem_showzeroline.isChecked(): 

3399 v = trace_projection(0.) 

3400 if vmin <= v <= vmax: 

3401 line = qc.QLineF(umin, v, umax_zeroline, v) 

3402 p.drawLine(line) 

3403 

3404 uoff = uoffnext 

3405 

3406 p.setFont(font) 

3407 p.setPen(primary_pen) 

3408 for trace in processed_traces: 

3409 if self.view_mode is not ViewMode.Wiggle: 

3410 break 

3411 

3412 if trace not in trace_to_itrack: 

3413 continue 

3414 

3415 itrack = trace_to_itrack[trace] 

3416 scaling_key = self.scaling_key(trace) 

3417 trace_projection = trace_projections[ 

3418 itrack, scaling_key] 

3419 

3420 vdata = trace_projection(trace.get_ydata()) 

3421 

3422 udata_min = float(self.time_projection(trace.tmin)) 

3423 udata_max = float(self.time_projection( 

3424 trace.tmin+trace.deltat*(vdata.size-1))) 

3425 udata = num.linspace(udata_min, udata_max, vdata.size) 

3426 

3427 qpoints = make_QPolygonF(udata, vdata) 

3428 

3429 umin, umax = self.time_projection.get_out_range() 

3430 vmin, vmax = trace_projection.get_out_range() 

3431 

3432 trackrect = qc.QRectF(umin, vmin, umax-umin, vmax-vmin) 

3433 

3434 if self.menuitem_cliptraces.isChecked(): 

3435 p.setClipRect(trackrect) 

3436 

3437 if self.menuitem_colortraces.isChecked(): 

3438 color = pyrocko.plot.color( 

3439 color_lookup[self.color_gather(trace)]) 

3440 pen = qg.QPen(qg.QColor(*color), 1) 

3441 p.setPen(pen) 

3442 

3443 p.drawPolyline(qpoints) 

3444 

3445 if self.floating_marker: 

3446 self.floating_marker.draw_trace( 

3447 self, p, trace, 

3448 self.time_projection, trace_projection, 1.0) 

3449 

3450 for marker in self.markers.with_key_in( 

3451 self.tmin - self.markers_deltat_max, 

3452 self.tmax): 

3453 

3454 if marker.tmin < self.tmax \ 

3455 and self.tmin < marker.tmax \ 

3456 and marker.kind \ 

3457 in self.visible_marker_kinds: 

3458 marker.draw_trace( 

3459 self, p, trace, self.time_projection, 

3460 trace_projection, 1.0) 

3461 

3462 p.setPen(primary_pen) 

3463 

3464 if self.menuitem_cliptraces.isChecked(): 

3465 p.setClipRect(0, 0, int(w), int(h)) 

3466 

3467 if self.floating_marker: 

3468 self.floating_marker.draw( 

3469 p, self.time_projection, vcenter_projection) 

3470 

3471 self.draw_visible_markers( 

3472 p, vcenter_projection, primary_pen) 

3473 

3474 p.setPen(primary_pen) 

3475 while font.pointSize() > 2: 

3476 fm = qg.QFontMetrics(font, p.device()) 

3477 trackheight = self.track_to_screen(1.-0.05) \ 

3478 - self.track_to_screen(0.05) 

3479 nlinesavail = trackheight/float(fm.lineSpacing()) 

3480 if nlinesavail > 1: 

3481 break 

3482 

3483 font.setPointSize(font.pointSize()-1) 

3484 

3485 p.setFont(font) 

3486 mouse_pos = self.mapFromGlobal(qg.QCursor.pos()) 

3487 

3488 for key in self.track_keys: 

3489 itrack = self.key_to_row[key] 

3490 if itrack in track_projections: 

3491 plabel = ' '.join( 

3492 [str(x) for x in key if x is not None]) 

3493 lx = 10 

3494 ly = self.track_to_screen(itrack+0.5) 

3495 

3496 if p.font().pointSize() >= MIN_LABEL_SIZE_PT: 

3497 draw_label(p, lx, ly, plabel, label_bg, 'ML') 

3498 continue 

3499 

3500 contains_cursor = \ 

3501 self.track_to_screen(itrack) \ 

3502 < mouse_pos.y() \ 

3503 < self.track_to_screen(itrack+1) 

3504 

3505 if not contains_cursor: 

3506 continue 

3507 

3508 font_large = p.font() 

3509 font_large.setPointSize(MIN_LABEL_SIZE_PT) 

3510 p.setFont(font_large) 

3511 draw_label(p, lx, ly, plabel, label_bg, 'ML') 

3512 p.setFont(font) 

3513 

3514 for lab in annot_labels: 

3515 lab.draw() 

3516 

3517 self.timer_draw.stop() 

3518 

3519 def see_data_params(self): 

3520 

3521 min_deltat = self.content_deltat_range()[0] 

3522 

3523 # determine padding and downampling requirements 

3524 if self.lowpass is not None: 

3525 deltat_target = 1./self.lowpass * 0.25 

3526 ndecimate = min( 

3527 50, 

3528 max(1, int(round(deltat_target / min_deltat)))) 

3529 tpad = 1./self.lowpass * 2. 

3530 else: 

3531 ndecimate = 1 

3532 tpad = min_deltat*5. 

3533 

3534 if self.highpass is not None: 

3535 tpad = max(1./self.highpass * 2., tpad) 

3536 

3537 nsee_points_per_trace = 5000*10 

3538 tsee = ndecimate*nsee_points_per_trace*min_deltat 

3539 

3540 return ndecimate, tpad, tsee 

3541 

3542 def clean_update(self): 

3543 self.cached_processed_traces = None 

3544 self.update() 

3545 

3546 def get_adequate_tpad(self): 

3547 tpad = 0. 

3548 for f in [self.highpass, self.lowpass]: 

3549 if f is not None: 

3550 tpad = max(tpad, 2.0/f) 

3551 

3552 for snuffling in self.snufflings: 

3553 if snuffling._post_process_hook_enabled \ 

3554 or snuffling._pre_process_hook_enabled: 

3555 

3556 tpad = max(tpad, snuffling.get_tpad()) 

3557 

3558 return tpad 

3559 

3560 def prepare_cutout2( 

3561 self, tmin, tmax, trace_selector=None, degap=True, 

3562 demean=True, nmax=6000): 

3563 

3564 if self.pile.is_empty(): 

3565 return [] 

3566 

3567 nmax = self.visible_length 

3568 

3569 self.timer_cutout.start() 

3570 

3571 tsee = tmax-tmin 

3572 min_deltat_wo_decimate = tsee/nmax 

3573 min_deltat_w_decimate = min_deltat_wo_decimate / 32. 

3574 

3575 min_deltat_allow = min_deltat_wo_decimate 

3576 if self.lowpass is not None: 

3577 target_deltat_lp = 0.25/self.lowpass 

3578 if target_deltat_lp > min_deltat_wo_decimate: 

3579 min_deltat_allow = min_deltat_w_decimate 

3580 

3581 min_deltat_allow = math.exp( 

3582 int(math.floor(math.log(min_deltat_allow)))) 

3583 

3584 tmin_ = tmin 

3585 tmax_ = tmax 

3586 

3587 # fetch more than needed? 

3588 if self.menuitem_liberal_fetch.isChecked(): 

3589 tlen = pyrocko.trace.nextpow2((tmax-tmin)*1.5) 

3590 tmin = math.floor(tmin/tlen) * tlen 

3591 tmax = math.ceil(tmax/tlen) * tlen 

3592 

3593 fft_filtering = self.menuitem_fft_filtering.isChecked() 

3594 lphp = self.menuitem_lphp.isChecked() 

3595 ads = self.menuitem_allowdownsampling.isChecked() 

3596 

3597 tpad = self.get_adequate_tpad() 

3598 tpad = min(tpad, tsee*3) 

3599 

3600 # state vector to decide if cached traces can be used 

3601 vec = ( 

3602 tmin, tmax, tpad, trace_selector, degap, demean, self.lowpass, 

3603 self.highpass, fft_filtering, lphp, 

3604 min_deltat_allow, self.rotate, self.shown_tracks_range, 

3605 ads, self.pile.get_update_count()) 

3606 

3607 if (self.cached_vec 

3608 and self.cached_vec[0] <= vec[0] 

3609 and vec[1] <= self.cached_vec[1] 

3610 and vec[2:] == self.cached_vec[2:] 

3611 and not (self.reloaded or self.menuitem_watch.isChecked()) 

3612 and self.cached_processed_traces is not None): 

3613 

3614 logger.debug('Using cached traces') 

3615 processed_traces = self.cached_processed_traces 

3616 

3617 else: 

3618 processed_traces = [] 

3619 if self.pile.deltatmax >= min_deltat_allow: 

3620 

3621 if isinstance(self.pile, pyrocko.pile.Pile): 

3622 def group_selector(gr): 

3623 return gr.deltatmax >= min_deltat_allow 

3624 

3625 kwargs = dict(group_selector=group_selector) 

3626 else: 

3627 kwargs = {} 

3628 

3629 if trace_selector is not None: 

3630 def trace_selectorx(tr): 

3631 return tr.deltat >= min_deltat_allow \ 

3632 and trace_selector(tr) 

3633 else: 

3634 def trace_selectorx(tr): 

3635 return tr.deltat >= min_deltat_allow 

3636 

3637 for traces in self.pile.chopper( 

3638 tmin=tmin, tmax=tmax, tpad=tpad, 

3639 want_incomplete=True, 

3640 degap=degap, 

3641 maxgap=gap_lap_tolerance, 

3642 maxlap=gap_lap_tolerance, 

3643 keep_current_files_open=True, 

3644 trace_selector=trace_selectorx, 

3645 accessor_id=id(self), 

3646 snap=(math.floor, math.ceil), 

3647 include_last=True, **kwargs): 

3648 

3649 if demean: 

3650 for tr in traces: 

3651 if (tr.meta and tr.meta.get('tabu', False)): 

3652 continue 

3653 y = tr.get_ydata() 

3654 tr.set_ydata(y - num.mean(y)) 

3655 

3656 traces = self.pre_process_hooks(traces) 

3657 

3658 for trace in traces: 

3659 

3660 if not (trace.meta 

3661 and trace.meta.get('tabu', False)): 

3662 

3663 if fft_filtering: 

3664 but = pyrocko.response.ButterworthResponse 

3665 multres = pyrocko.response.MultiplyResponse 

3666 if self.lowpass is not None \ 

3667 or self.highpass is not None: 

3668 

3669 it = num.arange( 

3670 trace.data_len(), dtype=float) 

3671 detr_data, m, b = detrend( 

3672 it, trace.get_ydata()) 

3673 

3674 trace.set_ydata(detr_data) 

3675 

3676 freqs, fdata = trace.spectrum( 

3677 pad_to_pow2=True, tfade=None) 

3678 

3679 nfreqs = fdata.size 

3680 

3681 key = (trace.deltat, nfreqs) 

3682 

3683 if key not in self.tf_cache: 

3684 resps = [] 

3685 if self.lowpass is not None: 

3686 resps.append(but( 

3687 order=4, 

3688 corner=self.lowpass, 

3689 type='low')) 

3690 

3691 if self.highpass is not None: 

3692 resps.append(but( 

3693 order=4, 

3694 corner=self.highpass, 

3695 type='high')) 

3696 

3697 resp = multres(resps) 

3698 self.tf_cache[key] = \ 

3699 resp.evaluate(freqs) 

3700 

3701 filtered_data = num.fft.irfft( 

3702 fdata*self.tf_cache[key] 

3703 )[:trace.data_len()] 

3704 

3705 retrended_data = retrend( 

3706 it, filtered_data, m, b) 

3707 

3708 trace.set_ydata(retrended_data) 

3709 

3710 else: 

3711 

3712 if ads and self.lowpass is not None: 

3713 while trace.deltat \ 

3714 < min_deltat_wo_decimate: 

3715 

3716 trace.downsample(2, demean=False) 

3717 

3718 fmax = 0.5/trace.deltat 

3719 if not lphp and ( 

3720 self.lowpass is not None 

3721 and self.highpass is not None 

3722 and self.lowpass < fmax 

3723 and self.highpass < fmax 

3724 and self.highpass < self.lowpass): 

3725 

3726 trace.bandpass( 

3727 2, self.highpass, self.lowpass) 

3728 else: 

3729 if self.lowpass is not None: 

3730 if self.lowpass < 0.5/trace.deltat: 

3731 trace.lowpass( 

3732 4, self.lowpass, 

3733 demean=False) 

3734 

3735 if self.highpass is not None: 

3736 if self.lowpass is None \ 

3737 or self.highpass \ 

3738 < self.lowpass: 

3739 

3740 if self.highpass < \ 

3741 0.5/trace.deltat: 

3742 trace.highpass( 

3743 4, self.highpass, 

3744 demean=False) 

3745 

3746 processed_traces.append(trace) 

3747 

3748 if self.rotate != 0.0: 

3749 phi = self.rotate/180.*math.pi 

3750 cphi = math.cos(phi) 

3751 sphi = math.sin(phi) 

3752 for a in processed_traces: 

3753 for b in processed_traces: 

3754 if (a.network == b.network 

3755 and a.station == b.station 

3756 and a.location == b.location 

3757 and ((a.channel.lower().endswith('n') 

3758 and b.channel.lower().endswith('e')) 

3759 or (a.channel.endswith('1') 

3760 and b.channel.endswith('2'))) 

3761 and abs(a.deltat-b.deltat) < a.deltat*0.001 

3762 and abs(a.tmin-b.tmin) < a.deltat*0.01 and 

3763 len(a.get_ydata()) == len(b.get_ydata())): 

3764 

3765 aydata = a.get_ydata()*cphi+b.get_ydata()*sphi 

3766 bydata = -a.get_ydata()*sphi+b.get_ydata()*cphi 

3767 a.set_ydata(aydata) 

3768 b.set_ydata(bydata) 

3769 

3770 processed_traces = self.post_process_hooks(processed_traces) 

3771 

3772 self.cached_processed_traces = processed_traces 

3773 self.cached_vec = vec 

3774 

3775 chopped_traces = [] 

3776 for trace in processed_traces: 

3777 chop_tmin = tmin_ - trace.deltat*4 

3778 chop_tmax = tmax_ + trace.deltat*4 

3779 

3780 try: 

3781 ctrace = trace.chop( 

3782 chop_tmin, chop_tmax, 

3783 inplace=False) 

3784 

3785 except pyrocko.trace.NoData: 

3786 continue 

3787 

3788 if ctrace.data_len() < 2: 

3789 continue 

3790 

3791 chopped_traces.append(ctrace) 

3792 

3793 self.timer_cutout.stop() 

3794 return chopped_traces 

3795 

3796 def pre_process_hooks(self, traces): 

3797 for snuffling in self.snufflings: 

3798 if snuffling._pre_process_hook_enabled: 

3799 traces = snuffling.pre_process_hook(traces) 

3800 

3801 return traces 

3802 

3803 def post_process_hooks(self, traces): 

3804 for snuffling in self.snufflings: 

3805 if snuffling._post_process_hook_enabled: 

3806 traces = snuffling.post_process_hook(traces) 

3807 

3808 return traces 

3809 

3810 def visible_length_change(self, ignore=None): 

3811 for menuitem, vlen in self.menuitems_visible_length: 

3812 if menuitem.isChecked(): 

3813 self.visible_length = vlen 

3814 

3815 def scaling_base_change(self, ignore=None): 

3816 for menuitem, scaling_base in self.menuitems_scaling_base: 

3817 if menuitem.isChecked(): 

3818 self.scaling_base = scaling_base 

3819 

3820 def scalingmode_change(self, ignore=None): 

3821 for menuitem, scaling_key in self.menuitems_scaling: 

3822 if menuitem.isChecked(): 

3823 self.scaling_key = scaling_key 

3824 self.update() 

3825 

3826 def apply_scaling_hooks(self, data_ranges): 

3827 for k in sorted(self.scaling_hooks.keys()): 

3828 hook = self.scaling_hooks[k] 

3829 hook(data_ranges) 

3830 

3831 def viewmode_change(self, ignore=True): 

3832 for item, mode in self.menuitems_viewmode: 

3833 if item.isChecked(): 

3834 self.view_mode = mode 

3835 break 

3836 else: 

3837 raise AttributeError('unknown view mode') 

3838 

3839 items_waterfall_disabled = ( 

3840 self.menuitem_showscaleaxis, 

3841 self.menuitem_showscalerange, 

3842 self.menuitem_showzeroline, 

3843 self.menuitem_colortraces, 

3844 self.menuitem_cliptraces, 

3845 *(itm[0] for itm in self.menuitems_visible_length) 

3846 ) 

3847 

3848 if self.view_mode is ViewMode.Waterfall: 

3849 self.parent().show_colorbar_ctrl(True) 

3850 self.parent().show_gain_ctrl(False) 

3851 

3852 for item in items_waterfall_disabled: 

3853 item.setDisabled(True) 

3854 

3855 self.visible_length = 180. 

3856 else: 

3857 self.parent().show_colorbar_ctrl(False) 

3858 self.parent().show_gain_ctrl(True) 

3859 

3860 for item in items_waterfall_disabled: 

3861 item.setDisabled(False) 

3862 

3863 self.visible_length_change() 

3864 self.update() 

3865 

3866 def set_scaling_hook(self, k, hook): 

3867 self.scaling_hooks[k] = hook 

3868 

3869 def remove_scaling_hook(self, k): 

3870 del self.scaling_hooks[k] 

3871 

3872 def remove_scaling_hooks(self): 

3873 self.scaling_hooks = {} 

3874 

3875 def s_sortingmode_change(self, ignore=None): 

3876 for menuitem, valfunc in self.menuitems_ssorting: 

3877 if menuitem.isChecked(): 

3878 self._ssort = valfunc 

3879 

3880 self.sortingmode_change() 

3881 

3882 def sortingmode_change(self, ignore=None): 

3883 for menuitem, (gather, color) in self.menuitems_sorting: 

3884 if menuitem.isChecked(): 

3885 self.set_gathering(gather, color) 

3886 

3887 self.sortingmode_change_time = time.time() 

3888 

3889 def lowpass_change(self, value, ignore=None): 

3890 self.lowpass = value 

3891 self.passband_check() 

3892 self.tf_cache = {} 

3893 self.update() 

3894 self.frequency_filter_changed.emit( 

3895 self.lowpass or 0.0, self.highpass or 0.0) 

3896 

3897 def highpass_change(self, value, ignore=None): 

3898 self.highpass = value 

3899 self.passband_check() 

3900 self.tf_cache = {} 

3901 self.update() 

3902 self.frequency_filter_changed.emit( 

3903 self.lowpass or 0.0, self.highpass or 0.0) 

3904 

3905 def passband_check(self): 

3906 if self.highpass and self.lowpass \ 

3907 and self.highpass >= self.lowpass: 

3908 

3909 self.window().status_messages.set( 

3910 'filter_error', 

3911 'Corner frequency of highpass greater than ' 

3912 'corner frequency of lowpass. Highpass deactivated.') 

3913 else: 

3914 self.window().status_messages.clear('filter_error') 

3915 

3916 def gain_change(self, value, ignore): 

3917 self.gain = value 

3918 self.update() 

3919 

3920 def rot_change(self, value, ignore): 

3921 self.rotate = value 

3922 self.update() 

3923 

3924 def waterfall_cmap_change(self, cmap): 

3925 self.waterfall_cmap = cmap 

3926 self.update() 

3927 

3928 def waterfall_clip_change(self, clip_min, clip_max): 

3929 self.waterfall_clip_min = clip_min 

3930 self.waterfall_clip_max = clip_max 

3931 self.update() 

3932 

3933 def waterfall_show_absolute_change(self, toggle): 

3934 self.waterfall_show_absolute = toggle 

3935 self.update() 

3936 

3937 def waterfall_set_integrate(self, toggle): 

3938 self.waterfall_integrate = toggle 

3939 self.update() 

3940 

3941 def set_selected_markers(self, markers): 

3942 ''' 

3943 Set a list of markers selected 

3944 

3945 :param markers: list of markers 

3946 ''' 

3947 self.deselect_all() 

3948 for m in markers: 

3949 m.selected = True 

3950 

3951 self.update() 

3952 

3953 def deselect_all(self): 

3954 for marker in self.markers: 

3955 marker.selected = False 

3956 

3957 def animate_picking(self): 

3958 point = self.mapFromGlobal(qg.QCursor.pos()) 

3959 self.update_picking(point.x(), point.y(), doshift=True) 

3960 

3961 def get_nslc_ids_for_track(self, ftrack): 

3962 itrack = int(ftrack) 

3963 return self.track_to_nslc_ids.get(itrack, []) 

3964 

3965 def stop_picking(self, x, y, abort=False): 

3966 if self.picking: 

3967 self.update_picking(x, y, doshift=False) 

3968 self.picking = None 

3969 self.picking_down = None 

3970 self.picking_timer.stop() 

3971 self.picking_timer = None 

3972 if not abort: 

3973 self.add_marker(self.floating_marker) 

3974 self.floating_marker.selected = True 

3975 self.emit_selected_markers() 

3976 

3977 self.floating_marker = None 

3978 

3979 def start_picking(self, ignore): 

3980 

3981 if not self.picking: 

3982 self.deselect_all() 

3983 self.picking = qw.QRubberBand(qw.QRubberBand.Rectangle) 

3984 point = self.mapFromGlobal(qg.QCursor.pos()) 

3985 

3986 gpoint = self.mapToGlobal(qc.QPoint(point.x(), 0)) 

3987 self.picking.setGeometry( 

3988 gpoint.x(), gpoint.y(), 1, self.height()) 

3989 t = self.time_projection.rev(point.x()) 

3990 

3991 ftrack = self.track_to_screen.rev(point.y()) 

3992 nslc_ids = self.get_nslc_ids_for_track(ftrack) 

3993 self.floating_marker = Marker(nslc_ids, t, t) 

3994 self.floating_marker.selected = True 

3995 

3996 self.picking_timer = qc.QTimer() 

3997 self.picking_timer.timeout.connect( 

3998 self.animate_picking) 

3999 

4000 self.picking_timer.setInterval(50) 

4001 self.picking_timer.start() 

4002 

4003 def update_picking(self, x, y, doshift=False): 

4004 if self.picking: 

4005 mouset = self.time_projection.rev(x) 

4006 dt = 0.0 

4007 if mouset < self.tmin or mouset > self.tmax: 

4008 if mouset < self.tmin: 

4009 dt = -(self.tmin - mouset) 

4010 else: 

4011 dt = mouset - self.tmax 

4012 ddt = self.tmax-self.tmin 

4013 dt = max(dt, -ddt/10.) 

4014 dt = min(dt, ddt/10.) 

4015 

4016 x0 = x 

4017 if self.picking_down is not None: 

4018 x0 = self.time_projection(self.picking_down[0]) 

4019 

4020 w = abs(x-x0) 

4021 x0 = min(x0, x) 

4022 

4023 tmin, tmax = ( 

4024 self.time_projection.rev(x0), 

4025 self.time_projection.rev(x0+w)) 

4026 

4027 tmin, tmax = ( 

4028 max(working_system_time_range[0], tmin), 

4029 min(working_system_time_range[1], tmax)) 

4030 

4031 p1 = self.mapToGlobal(qc.QPoint(int(round(x0)), 0)) 

4032 

4033 self.picking.setGeometry( 

4034 p1.x(), p1.y(), int(round(max(w, 1))), self.height()) 

4035 

4036 ftrack = self.track_to_screen.rev(y) 

4037 nslc_ids = self.get_nslc_ids_for_track(ftrack) 

4038 self.floating_marker.set(nslc_ids, tmin, tmax) 

4039 

4040 if dt != 0.0 and doshift: 

4041 self.interrupt_following() 

4042 self.set_time_range(self.tmin+dt, self.tmax+dt) 

4043 

4044 self.update() 

4045 

4046 def update_status(self): 

4047 

4048 point = self.mapFromGlobal(qg.QCursor.pos()) 

4049 

4050 mouse_t = self.time_projection.rev(point.x()) 

4051 if not is_working_time(mouse_t): 

4052 return 

4053 

4054 if self.floating_marker: 

4055 tmi, tma = ( 

4056 self.floating_marker.tmin, 

4057 self.floating_marker.tmax) 

4058 

4059 tt, ms = gmtime_x(tmi) 

4060 

4061 if tmi == tma: 

4062 message = mystrftime( 

4063 fmt='Pick: %Y-%m-%d %H:%M:%S .%r', 

4064 tt=tt, milliseconds=ms) 

4065 else: 

4066 srange = '%g s' % (tma-tmi) 

4067 message = mystrftime( 

4068 fmt='Start: %Y-%m-%d %H:%M:%S .%r Length: '+srange, 

4069 tt=tt, milliseconds=ms) 

4070 else: 

4071 tt, ms = gmtime_x(mouse_t) 

4072 

4073 message = mystrftime(fmt=None, tt=tt, milliseconds=ms) 

4074 

4075 sb = self.window().statusBar() 

4076 sb.clearMessage() 

4077 sb.showMessage(message) 

4078 

4079 def set_sortingmode_change_delay_time(self, dt): 

4080 self.sortingmode_change_delay_time = dt 

4081 

4082 def sortingmode_change_delayed(self): 

4083 now = time.time() 

4084 return ( 

4085 self.sortingmode_change_delay_time is not None 

4086 and now - self.sortingmode_change_time 

4087 < self.sortingmode_change_delay_time) 

4088 

4089 def set_visible_marker_kinds(self, kinds): 

4090 self.deselect_all() 

4091 self.visible_marker_kinds = tuple(kinds) 

4092 self.emit_selected_markers() 

4093 

4094 def following(self): 

4095 return self.follow_timer is not None \ 

4096 and not self.following_interrupted() 

4097 

4098 def interrupt_following(self): 

4099 self.interactive_range_change_time = time.time() 

4100 

4101 def following_interrupted(self, now=None): 

4102 if now is None: 

4103 now = time.time() 

4104 return now - self.interactive_range_change_time \ 

4105 < self.interactive_range_change_delay_time 

4106 

4107 def follow(self, tlen, interval=50, lapse=None, tmax_start=None): 

4108 if tmax_start is None: 

4109 tmax_start = time.time() 

4110 self.show_all = False 

4111 self.follow_time = tlen 

4112 self.follow_timer = qc.QTimer(self) 

4113 self.follow_timer.timeout.connect( 

4114 self.follow_update) 

4115 self.follow_timer.setInterval(interval) 

4116 self.follow_timer.start() 

4117 self.follow_started = time.time() 

4118 self.follow_lapse = lapse 

4119 self.follow_tshift = self.follow_started - tmax_start 

4120 self.interactive_range_change_time = 0.0 

4121 

4122 def unfollow(self): 

4123 if self.follow_timer is not None: 

4124 self.follow_timer.stop() 

4125 self.follow_timer = None 

4126 self.interactive_range_change_time = 0.0 

4127 

4128 def follow_update(self): 

4129 rnow = time.time() 

4130 if self.follow_lapse is None: 

4131 now = rnow 

4132 else: 

4133 now = self.follow_started + (rnow - self.follow_started) \ 

4134 * self.follow_lapse 

4135 

4136 if self.following_interrupted(rnow): 

4137 return 

4138 self.set_time_range( 

4139 now-self.follow_time-self.follow_tshift, 

4140 now-self.follow_tshift) 

4141 

4142 self.update() 

4143 

4144 def myclose(self, return_tag=''): 

4145 self.return_tag = return_tag 

4146 self.window().close() 

4147 

4148 def cleanup(self): 

4149 self.about_to_close.emit() 

4150 self.timer.stop() 

4151 if self.follow_timer is not None: 

4152 self.follow_timer.stop() 

4153 

4154 for snuffling in list(self.snufflings): 

4155 self.remove_snuffling(snuffling) 

4156 

4157 def inputline_changed(self, text): 

4158 pass 

4159 

4160 def inputline_finished(self, text): 

4161 line = str(text) 

4162 

4163 toks = line.split() 

4164 clearit, hideit, error = False, True, None 

4165 if len(toks) >= 1: 

4166 command = toks[0].lower() 

4167 

4168 try: 

4169 quick_filter_commands = { 

4170 'n': '%s.*.*.*', 

4171 's': '*.%s.*.*', 

4172 'l': '*.*.%s.*', 

4173 'c': '*.*.*.%s'} 

4174 

4175 if command in quick_filter_commands: 

4176 if len(toks) >= 2: 

4177 patterns = [ 

4178 quick_filter_commands[toks[0]] % pat 

4179 for pat in toks[1:]] 

4180 self.set_quick_filter_patterns(patterns, line) 

4181 else: 

4182 self.set_quick_filter_patterns(None) 

4183 

4184 self.update() 

4185 

4186 elif command in ('hide', 'unhide'): 

4187 if len(toks) >= 2: 

4188 patterns = [] 

4189 if len(toks) == 2: 

4190 patterns = [toks[1]] 

4191 elif len(toks) >= 3: 

4192 x = { 

4193 'n': '%s.*.*.*', 

4194 's': '*.%s.*.*', 

4195 'l': '*.*.%s.*', 

4196 'c': '*.*.*.%s'} 

4197 

4198 if toks[1] in x: 

4199 patterns.extend( 

4200 x[toks[1]] % tok for tok in toks[2:]) 

4201 

4202 for pattern in patterns: 

4203 if command == 'hide': 

4204 self.add_blacklist_pattern(pattern) 

4205 else: 

4206 self.remove_blacklist_pattern(pattern) 

4207 

4208 elif command == 'unhide' and len(toks) == 1: 

4209 self.clear_blacklist() 

4210 

4211 clearit = True 

4212 

4213 self.update() 

4214 

4215 elif command == 'markers': 

4216 if len(toks) == 2: 

4217 if toks[1] == 'all': 

4218 kinds = self.all_marker_kinds 

4219 else: 

4220 kinds = [] 

4221 for x in toks[1]: 

4222 try: 

4223 kinds.append(int(x)) 

4224 except Exception: 

4225 pass 

4226 

4227 self.set_visible_marker_kinds(kinds) 

4228 

4229 elif len(toks) == 1: 

4230 self.set_visible_marker_kinds(()) 

4231 

4232 self.update() 

4233 

4234 elif command == 'scaling': 

4235 if len(toks) == 2: 

4236 hideit = False 

4237 error = 'wrong number of arguments' 

4238 

4239 if len(toks) >= 3: 

4240 vmin, vmax = [ 

4241 pyrocko.model.float_or_none(x) 

4242 for x in toks[-2:]] 

4243 

4244 def upd(d, k, vmin, vmax): 

4245 if k in d: 

4246 if vmin is not None: 

4247 d[k] = vmin, d[k][1] 

4248 if vmax is not None: 

4249 d[k] = d[k][0], vmax 

4250 

4251 if len(toks) == 1: 

4252 self.remove_scaling_hooks() 

4253 

4254 elif len(toks) == 3: 

4255 def hook(data_ranges): 

4256 for k in data_ranges: 

4257 upd(data_ranges, k, vmin, vmax) 

4258 

4259 self.set_scaling_hook('_', hook) 

4260 

4261 elif len(toks) == 4: 

4262 pattern = toks[1] 

4263 

4264 def hook(data_ranges): 

4265 for k in pyrocko.util.match_nslcs( 

4266 pattern, list(data_ranges.keys())): 

4267 

4268 upd(data_ranges, k, vmin, vmax) 

4269 

4270 self.set_scaling_hook(pattern, hook) 

4271 

4272 elif command == 'goto': 

4273 toks2 = line.split(None, 1) 

4274 if len(toks2) == 2: 

4275 arg = toks2[1] 

4276 m = re.match( 

4277 r'^\d\d\d\d(-\d\d(-\d\d( \d\d(:\d\d' 

4278 r'(:\d\d(\.\d+)?)?)?)?)?)?$', arg) 

4279 if m: 

4280 tlen = None 

4281 if not m.group(1): 

4282 tlen = 12*32*24*60*60 

4283 elif not m.group(2): 

4284 tlen = 32*24*60*60 

4285 elif not m.group(3): 

4286 tlen = 24*60*60 

4287 elif not m.group(4): 

4288 tlen = 60*60 

4289 elif not m.group(5): 

4290 tlen = 60 

4291 

4292 supl = '1970-01-01 00:00:00' 

4293 if len(supl) > len(arg): 

4294 arg = arg + supl[-(len(supl)-len(arg)):] 

4295 t = pyrocko.util.str_to_time(arg) 

4296 self.go_to_time(t, tlen=tlen) 

4297 

4298 elif re.match(r'^\d\d:\d\d(:\d\d(\.\d+)?)?$', arg): 

4299 supl = '00:00:00' 

4300 if len(supl) > len(arg): 

4301 arg = arg + supl[-(len(supl)-len(arg)):] 

4302 tmin, tmax = self.get_time_range() 

4303 sdate = pyrocko.util.time_to_str( 

4304 tmin/2.+tmax/2., format='%Y-%m-%d') 

4305 t = pyrocko.util.str_to_time(sdate + ' ' + arg) 

4306 self.go_to_time(t) 

4307 

4308 elif arg == 'today': 

4309 self.go_to_time( 

4310 day_start( 

4311 time.time()), tlen=24*60*60) 

4312 

4313 elif arg == 'yesterday': 

4314 self.go_to_time( 

4315 day_start( 

4316 time.time()-24*60*60), tlen=24*60*60) 

4317 

4318 else: 

4319 self.go_to_event_by_name(arg) 

4320 

4321 else: 

4322 raise PileViewerMainException( 

4323 'No such command: %s' % command) 

4324 

4325 except PileViewerMainException as e: 

4326 error = str(e) 

4327 hideit = False 

4328 

4329 return clearit, hideit, error 

4330 

4331 return PileViewerMain 

4332 

4333 

4334PileViewerMain = MakePileViewerMainClass(qw.QWidget) 

4335GLPileViewerMain = MakePileViewerMainClass(qw.QOpenGLWidget) 

4336 

4337 

4338class LineEditWithAbort(qw.QLineEdit): 

4339 

4340 aborted = qc.pyqtSignal() 

4341 history_down = qc.pyqtSignal() 

4342 history_up = qc.pyqtSignal() 

4343 

4344 def keyPressEvent(self, key_event): 

4345 if key_event.key() == qc.Qt.Key_Escape: 

4346 self.aborted.emit() 

4347 elif key_event.key() == qc.Qt.Key_Down: 

4348 self.history_down.emit() 

4349 elif key_event.key() == qc.Qt.Key_Up: 

4350 self.history_up.emit() 

4351 else: 

4352 return qw.QLineEdit.keyPressEvent(self, key_event) 

4353 

4354 

4355class PileViewer(qw.QFrame): 

4356 ''' 

4357 PileViewerMain + Controls + Inputline 

4358 ''' 

4359 

4360 def __init__( 

4361 self, pile, 

4362 ntracks_shown_max=20, 

4363 marker_editor_sortable=True, 

4364 use_opengl=None, 

4365 panel_parent=None, 

4366 *args): 

4367 

4368 qw.QFrame.__init__(self, *args) 

4369 

4370 layout = qw.QGridLayout() 

4371 layout.setContentsMargins(0, 0, 0, 0) 

4372 layout.setSpacing(0) 

4373 

4374 self.menu = PileViewerMenuBar(self) 

4375 

4376 if use_opengl is None: 

4377 use_opengl = is_macos 

4378 

4379 if use_opengl: 

4380 self.viewer = GLPileViewerMain( 

4381 pile, 

4382 ntracks_shown_max=ntracks_shown_max, 

4383 panel_parent=panel_parent, 

4384 menu=self.menu) 

4385 else: 

4386 self.viewer = PileViewerMain( 

4387 pile, 

4388 ntracks_shown_max=ntracks_shown_max, 

4389 panel_parent=panel_parent, 

4390 menu=self.menu) 

4391 

4392 self.marker_editor_sortable = marker_editor_sortable 

4393 

4394 # self.setFrameShape(qw.QFrame.StyledPanel) 

4395 # self.setFrameShadow(qw.QFrame.Sunken) 

4396 

4397 self.input_area = qw.QFrame(self) 

4398 ia_layout = qw.QGridLayout() 

4399 ia_layout.setContentsMargins(11, 11, 11, 11) 

4400 self.input_area.setLayout(ia_layout) 

4401 

4402 self.inputline = LineEditWithAbort(self.input_area) 

4403 self.inputline.returnPressed.connect( 

4404 self.inputline_returnpressed) 

4405 self.inputline.editingFinished.connect( 

4406 self.inputline_finished) 

4407 self.inputline.aborted.connect( 

4408 self.inputline_aborted) 

4409 

4410 self.inputline.history_down.connect( 

4411 lambda: self.step_through_history(1)) 

4412 self.inputline.history_up.connect( 

4413 lambda: self.step_through_history(-1)) 

4414 

4415 self.inputline.textEdited.connect( 

4416 self.inputline_changed) 

4417 

4418 self.inputline.setPlaceholderText( 

4419 u"Quick commands: e.g. 'c HH?' to select channels. " 

4420 u'Use ↑ or ↓ to navigate.') 

4421 self.inputline.setFocusPolicy(qc.Qt.ClickFocus) 

4422 self.input_area.hide() 

4423 self.history = None 

4424 

4425 self.inputline_error_str = None 

4426 

4427 self.inputline_error = qw.QLabel() 

4428 self.inputline_error.hide() 

4429 

4430 ia_layout.addWidget(self.inputline, 0, 0) 

4431 ia_layout.addWidget(self.inputline_error, 1, 0) 

4432 layout.addWidget(self.input_area, 0, 0, 1, 2) 

4433 layout.addWidget(self.viewer, 1, 0) 

4434 

4435 pb = Progressbars(self) 

4436 layout.addWidget(pb, 2, 0, 1, 2) 

4437 self.progressbars = pb 

4438 

4439 scrollbar = qw.QScrollBar(qc.Qt.Vertical) 

4440 self.scrollbar = scrollbar 

4441 layout.addWidget(scrollbar, 1, 1) 

4442 self.scrollbar.valueChanged.connect( 

4443 self.scrollbar_changed) 

4444 

4445 self.block_scrollbar_changes = False 

4446 

4447 self.viewer.want_input.connect( 

4448 self.inputline_show) 

4449 self.viewer.toggle_input.connect( 

4450 self.inputline_toggle) 

4451 self.viewer.tracks_range_changed.connect( 

4452 self.tracks_range_changed) 

4453 self.viewer.pile_has_changed_signal.connect( 

4454 self.adjust_controls) 

4455 self.viewer.about_to_close.connect( 

4456 self.save_inputline_history) 

4457 

4458 self.setLayout(layout) 

4459 

4460 def cleanup(self): 

4461 self.viewer.cleanup() 

4462 

4463 def get_progressbars(self): 

4464 return self.progressbars 

4465 

4466 def inputline_show(self): 

4467 if not self.history: 

4468 self.load_inputline_history() 

4469 

4470 self.input_area.show() 

4471 self.inputline.setFocus(qc.Qt.OtherFocusReason) 

4472 self.inputline.selectAll() 

4473 

4474 def inputline_set_error(self, string): 

4475 self.inputline_error_str = string 

4476 self.inputline.setPalette(pyrocko.gui.util.get_err_palette()) 

4477 self.inputline.selectAll() 

4478 self.inputline_error.setText(string) 

4479 self.input_area.show() 

4480 self.inputline_error.show() 

4481 

4482 def inputline_clear_error(self): 

4483 if self.inputline_error_str: 

4484 self.inputline.setPalette(qw.QApplication.palette()) 

4485 self.inputline_error_str = None 

4486 self.inputline_error.clear() 

4487 self.inputline_error.hide() 

4488 

4489 def inputline_changed(self, line): 

4490 self.viewer.inputline_changed(str(line)) 

4491 self.inputline_clear_error() 

4492 

4493 def inputline_returnpressed(self): 

4494 line = str(self.inputline.text()) 

4495 clearit, hideit, error = self.viewer.inputline_finished(line) 

4496 

4497 if error: 

4498 self.inputline_set_error(error) 

4499 

4500 line = line.strip() 

4501 

4502 if line != '' and not error: 

4503 if not (len(self.history) >= 1 and line == self.history[-1]): 

4504 self.history.append(line) 

4505 

4506 if clearit: 

4507 

4508 self.inputline.blockSignals(True) 

4509 qpat, qinp = self.viewer.get_quick_filter_patterns() 

4510 if qpat is None: 

4511 self.inputline.clear() 

4512 else: 

4513 self.inputline.setText(qinp) 

4514 self.inputline.blockSignals(False) 

4515 

4516 if hideit and not error: 

4517 self.viewer.setFocus(qc.Qt.OtherFocusReason) 

4518 self.input_area.hide() 

4519 

4520 self.hist_ind = len(self.history) 

4521 

4522 def inputline_aborted(self): 

4523 ''' 

4524 Hide the input line. 

4525 ''' 

4526 self.viewer.setFocus(qc.Qt.OtherFocusReason) 

4527 self.hist_ind = len(self.history) 

4528 self.input_area.hide() 

4529 

4530 def inputline_toggle(self): 

4531 if self.input_area.isVisible(): 

4532 self.inputline_aborted() 

4533 else: 

4534 self.inputline_show() 

4535 

4536 def save_inputline_history(self): 

4537 ''' 

4538 Save input line history to "$HOME/.pyrocko/.snuffler_history.pf" 

4539 ''' 

4540 if not self.history: 

4541 return 

4542 

4543 conf = pyrocko.config 

4544 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history')) 

4545 with open(fn_hist, 'w') as f: 

4546 i = min(100, len(self.history)) 

4547 for c in self.history[-i:]: 

4548 f.write('%s\n' % c) 

4549 

4550 def load_inputline_history(self): 

4551 ''' 

4552 Load input line history from "$HOME/.pyrocko/.snuffler_history.pf" 

4553 ''' 

4554 conf = pyrocko.config 

4555 fn_hist = conf.expand(conf.make_conf_path_tmpl('.snuffler_history')) 

4556 if not os.path.exists(fn_hist): 

4557 with open(fn_hist, 'w+') as f: 

4558 f.write('\n') 

4559 

4560 with open(fn_hist, 'r') as f: 

4561 self.history = [line.strip() for line in f.readlines()] 

4562 

4563 self.hist_ind = len(self.history) 

4564 

4565 def step_through_history(self, ud=1): 

4566 ''' 

4567 Step through input line history and set the input line text. 

4568 ''' 

4569 n = len(self.history) 

4570 self.hist_ind += ud 

4571 self.hist_ind %= (n + 1) 

4572 if len(self.history) != 0 and self.hist_ind != n: 

4573 self.inputline.setText(self.history[self.hist_ind]) 

4574 else: 

4575 self.inputline.setText('') 

4576 

4577 def inputline_finished(self): 

4578 pass 

4579 

4580 def tracks_range_changed(self, ntracks, ilo, ihi): 

4581 if self.block_scrollbar_changes: 

4582 return 

4583 

4584 self.scrollbar.blockSignals(True) 

4585 self.scrollbar.setPageStep(ihi-ilo) 

4586 vmax = max(0, ntracks-(ihi-ilo)) 

4587 self.scrollbar.setRange(0, vmax) 

4588 self.scrollbar.setValue(ilo) 

4589 self.scrollbar.setHidden(vmax == 0) 

4590 self.scrollbar.blockSignals(False) 

4591 

4592 def scrollbar_changed(self, value): 

4593 self.block_scrollbar_changes = True 

4594 ilo = value 

4595 ihi = ilo + self.scrollbar.pageStep() 

4596 self.viewer.set_tracks_range((ilo, ihi)) 

4597 self.block_scrollbar_changes = False 

4598 self.update_contents() 

4599 

4600 def controls(self): 

4601 frame = qw.QFrame(self) 

4602 layout = qw.QGridLayout() 

4603 frame.setLayout(layout) 

4604 

4605 minfreq = 0.001 

4606 maxfreq = 1000.0 

4607 self.lowpass_control = ValControl(high_is_none=True) 

4608 self.lowpass_control.setup( 

4609 'Lowpass [Hz]:', minfreq, maxfreq, maxfreq, 0) 

4610 self.highpass_control = ValControl(low_is_none=True) 

4611 self.highpass_control.setup( 

4612 'Highpass [Hz]:', minfreq, maxfreq, minfreq, 1) 

4613 self.gain_control = ValControl() 

4614 self.gain_control.setup('Gain:', 0.001, 1000., 1., 2) 

4615 self.rot_control = LinValControl() 

4616 self.rot_control.setup('Rotate [deg]:', -180., 180., 0., 3) 

4617 self.colorbar_control = ColorbarControl(self) 

4618 

4619 self.lowpass_control.valchange.connect( 

4620 self.viewer.lowpass_change) 

4621 self.highpass_control.valchange.connect( 

4622 self.viewer.highpass_change) 

4623 self.gain_control.valchange.connect( 

4624 self.viewer.gain_change) 

4625 self.rot_control.valchange.connect( 

4626 self.viewer.rot_change) 

4627 self.colorbar_control.cmap_changed.connect( 

4628 self.viewer.waterfall_cmap_change 

4629 ) 

4630 self.colorbar_control.clip_changed.connect( 

4631 self.viewer.waterfall_clip_change 

4632 ) 

4633 self.colorbar_control.show_absolute_toggled.connect( 

4634 self.viewer.waterfall_show_absolute_change 

4635 ) 

4636 self.colorbar_control.show_integrate_toggled.connect( 

4637 self.viewer.waterfall_set_integrate 

4638 ) 

4639 

4640 for icontrol, control in enumerate(( 

4641 self.highpass_control, 

4642 self.lowpass_control, 

4643 self.gain_control, 

4644 self.rot_control, 

4645 self.colorbar_control)): 

4646 

4647 for iwidget, widget in enumerate(control.widgets()): 

4648 layout.addWidget(widget, icontrol, iwidget) 

4649 

4650 spacer = qw.QSpacerItem( 

4651 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

4652 layout.addItem(spacer, 4, 0, 1, 3) 

4653 

4654 self.adjust_controls() 

4655 self.viewer.viewmode_change(ViewMode.Wiggle) 

4656 return frame 

4657 

4658 def marker_editor(self): 

4659 editor = pyrocko.gui.snuffler.marker_editor.MarkerEditor( 

4660 self, sortable=self.marker_editor_sortable) 

4661 

4662 editor.set_viewer(self.get_view()) 

4663 editor.get_marker_model().dataChanged.connect( 

4664 self.update_contents) 

4665 return editor 

4666 

4667 def adjust_controls(self): 

4668 dtmin, dtmax = self.viewer.content_deltat_range() 

4669 maxfreq = 0.5/dtmin 

4670 minfreq = (0.5/dtmax)*0.0001 

4671 self.lowpass_control.set_range(minfreq, maxfreq) 

4672 self.highpass_control.set_range(minfreq, maxfreq) 

4673 

4674 def setup_snufflings(self): 

4675 self.viewer.setup_snufflings() 

4676 

4677 def get_view(self): 

4678 return self.viewer 

4679 

4680 def update_contents(self): 

4681 self.viewer.update() 

4682 

4683 def get_pile(self): 

4684 return self.viewer.get_pile() 

4685 

4686 def show_colorbar_ctrl(self, show): 

4687 for w in self.colorbar_control.widgets(): 

4688 w.setVisible(show) 

4689 

4690 def show_gain_ctrl(self, show): 

4691 for w in self.gain_control.widgets(): 

4692 w.setVisible(show)