Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/gui/drum/view.py: 0%

595 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 math 

7import time 

8import numpy as num 

9 

10from pyrocko.gui.qt_compat import qc, qg, qw 

11 

12from pyrocko.gui.talkie import TalkieConnectionOwner 

13from pyrocko.gui.drum.state import State, TextStyle 

14from pyrocko.gui_util import make_QPolygonF, PhaseMarker 

15from pyrocko import trace, util, pile 

16 

17 

18def lim(a, x, b): 

19 return min(max(a, x), b) 

20 

21 

22class Label(object): 

23 def __init__( 

24 self, x, y, label_str, 

25 anchor='BL', 

26 style=None, 

27 keep_inside=None, 

28 head=None): 

29 

30 if style is None: 

31 style = TextStyle() 

32 

33 text = qg.QTextDocument() 

34 font = style.qt_font 

35 if font: 

36 text.setDefaultFont(font) 

37 

38 color = style.color.qt_color 

39 

40 text.setDefaultStyleSheet('span { color: %s; }' % color.name()) 

41 text.setHtml('<span>%s</span>' % label_str) 

42 

43 self.position = x, y 

44 self.anchor = anchor 

45 self.text = text 

46 self.style = style 

47 self.keep_inside = keep_inside 

48 if head: 

49 self.head = head 

50 else: 

51 self.head = (0., 0.) 

52 

53 def draw(self, p): 

54 s = self.text.size() 

55 rect = qc.QRectF(0., 0., s.width(), s.height()) 

56 

57 tx, ty = x, y = self.position 

58 anchor = self.anchor 

59 

60 pxs = self.text.defaultFont().pointSize() 

61 

62 oy = self.head[0] * pxs 

63 ox = self.head[1]/2. * pxs 

64 

65 if 'L' in anchor: 

66 tx -= min(ox*2, rect.width()/2.) 

67 

68 elif 'R' in anchor: 

69 tx -= rect.width() - min(ox*2., rect.width()/2.) 

70 

71 elif 'C' in anchor: 

72 tx -= rect.width()/2. 

73 

74 if 'B' in anchor: 

75 ty -= rect.height() + oy 

76 

77 elif 'T' in anchor: 

78 ty += oy 

79 

80 elif 'M' in anchor: 

81 ty -= rect.height()/2. 

82 

83 rect.translate(tx, ty) 

84 

85 if self.keep_inside: 

86 keep_inside = self.keep_inside 

87 if rect.top() < keep_inside.top(): 

88 rect.moveTop(keep_inside.top()) 

89 

90 if rect.bottom() > keep_inside.bottom(): 

91 rect.moveBottom(keep_inside.bottom()) 

92 

93 if rect.left() < keep_inside.left(): 

94 rect.moveLeft(keep_inside.left()) 

95 

96 if rect.right() > keep_inside.right(): 

97 rect.moveRight(keep_inside.right()) 

98 

99 poly = None 

100 if self.head[0] != 0.: 

101 l, r, t, b = rect.left(), rect.right(), rect.top(), rect.bottom() 

102 

103 if 'T' in anchor: 

104 a, b = t, b 

105 elif 'B' in anchor: 

106 a, b = b, t 

107 elif 'M' in anchor: 

108 assert False, 'label cannot have head with M alignment' 

109 

110 c1, c2 = lim(l, x-ox, r), lim(l, x+ox, r) 

111 

112 px = (l, c1, x, c2, r, r, l) 

113 py = (a, a, y, a, a, b, b) 

114 

115 poly = make_QPolygonF(px, py) 

116 

117 tx = rect.left() 

118 ty = rect.top() 

119 

120 if self.style.outline or self.style.background_color: 

121 oldpen = p.pen() 

122 oldbrush = p.brush() 

123 if not self.style.outline: 

124 p.setPen(qg.QPen(qc.Qt.NoPen)) 

125 

126 p.setBrush(self.style.background_color.qt_color) 

127 if poly: 

128 p.drawPolygon(poly) 

129 else: 

130 p.drawRect(rect) 

131 

132 if self.style.background_color: 

133 p.fillRect(rect, self.style.background_color.qt_color) 

134 

135 p.setPen(oldpen) 

136 p.setBrush(oldbrush) 

137 

138 p.translate(tx, ty) 

139 self.text.drawContents(p) 

140 p.translate(-tx, -ty) 

141 

142 

143class ProjectXU(object): 

144 def __init__(self, xr=(0., 1.), ur=(0., 1.)): 

145 (self.xmin, self.xmax) = xr 

146 (self.umin, self.umax) = ur 

147 self.cxu = (self.umax - self.umin) / (self.xmax - self.xmin) 

148 

149 def u(self, x): 

150 return self.umin + (x-self.xmin) * self.cxu 

151 

152 def x(self, u): 

153 return self.xmin + (u-self.umin) / self.cxu 

154 

155 

156class ProjectYV(object): 

157 def __init__(self, yr=(0., 1.), vr=(0., 1.)): 

158 (self.ymin, self.ymax) = yr 

159 (self.vmin, self.vmax) = vr 

160 self.cyv = (self.vmax - self.vmin) / (self.ymax - self.ymin) 

161 

162 def v(self, y): 

163 return self.vmin + (y-self.ymin) * self.cyv 

164 

165 def y(self, v): 

166 return self.ymin + (v-self.vmin) / self.cyv 

167 

168 

169class ProjectXYUV(object): 

170 def __init__(self, xyr=((0., 1.), (0., 1.)), uvr=((0., 1.), (0., 1.))): 

171 (self.xmin, self.xmax), (self.ymin, self.ymax) = xyr 

172 (self.umin, self.umax), (self.vmin, self.vmax) = uvr 

173 self.cxu = (self.umax - self.umin) / (self.xmax - self.xmin) 

174 self.cyv = (self.vmax - self.vmin) / (self.ymax - self.ymin) 

175 

176 def u(self, x): 

177 return self.umin + (x-self.xmin) * self.cxu 

178 

179 def v(self, y): 

180 return self.vmin + (y-self.ymin) * self.cyv 

181 

182 def x(self, u): 

183 return self.xmin + (u-self.umin) / self.cxu 

184 

185 def y(self, v): 

186 return self.ymin + (v-self.vmin) / self.cyv 

187 

188 

189class PlotEnv(qg.QPainter): 

190 def __init__(self, *args): 

191 qg.QPainter.__init__(self, *args) 

192 self.umin = 0. 

193 self.umax = 1. 

194 self.vmin = 0. 

195 self.vmax = 1. 

196 

197 def draw_vline(self, project, x, y0, y1): 

198 u = project.u(x) 

199 line = qc.QLineF(u, project.v(y0), u, project.v(y1)) 

200 self.drawLine(line) 

201 

202 def projector(self, xyr): 

203 return ProjectXYUV(xyr, self.uvrange) 

204 

205 @property 

206 def uvrange(self): 

207 return (self.umin, self.umax), (self.vmin, self.vmax) 

208 

209 

210def time_fmt_drumline(t): 

211 ti = int(t) 

212 if ti % 60 == 0: 

213 fmt = '%H:%M' 

214 else: 

215 fmt = '%H:%M:%S' 

216 

217 if ti % (3600*24) == 0: 

218 fmt = fmt + ' %Y-%m-%d' 

219 

220 return fmt 

221 

222 

223class Empty(Exception): 

224 pass 

225 

226 

227class DrumLine(qc.QObject, TalkieConnectionOwner): 

228 def __init__(self, iline, tmin, tmax, traces, state): 

229 qc.QObject.__init__(self) 

230 self.traces = traces 

231 self.tmin = tmin 

232 self.tmax = tmax 

233 self.ymin = 0. 

234 self.ymax = 1. 

235 self.ymin_data = 0. 

236 self.ymax_data = 1. 

237 self.iline = iline 

238 self._last_mode = None 

239 self._ydata_cache = {} 

240 self._time_per_pixel = None 

241 self.access_counter = 0 

242 self.talkie_connect(state, 'style.trace_resolution', self._empty_cache) 

243 

244 def data_range(self, mode='min-max'): 

245 if not self.traces: 

246 raise Empty() 

247 

248 modemap = { 

249 'min-max': 'minmax', 

250 'mean-plusminus-1-sigma': 1., 

251 'mean-plusminus-2-sigma': 2., 

252 'mean-plusminus-4-sigma': 4., 

253 } 

254 

255 if self._last_mode != mode: 

256 mi, ma = trace.minmax( 

257 self.traces, key=lambda tr: None, mode=modemap[mode])[None] 

258 self.ymin_data = mi 

259 self.ymax_data = ma 

260 self._last_mode = mode 

261 

262 return self.ymin_data, self.ymax_data 

263 

264 def set_yrange(self, ymin, ymax): 

265 if ymax == ymin: 

266 ymax = 0.5*(ymin + ymax) + 1.0 

267 ymin = 0.5*(ymin + ymax) - 1.0 

268 

269 self.ymin = ymin 

270 self.ymax = ymax 

271 

272 @property 

273 def tyrange(self): 

274 return (self.tmin, self.tmax), (self.ymin, self.ymax) 

275 

276 def draw(self, plotenv, markers): 

277 self._draw_traces(plotenv) 

278 self._draw_time_label(plotenv) 

279 self._draw_markers(plotenv, markers) 

280 

281 def _draw_time_label(self, plotenv): 

282 text = util.time_to_str(self.tmin, format=time_fmt_drumline(self.tmin)) 

283 font = plotenv.style.label_textstyle.qt_font 

284 lab = Label( 

285 font.pointSize(), plotenv.vmin, text, 

286 anchor='ML', style=plotenv.style.label_textstyle) 

287 

288 lab.draw(plotenv) 

289 

290 def _draw_markers(self, plotenv, markers): 

291 project = plotenv.projector(((self.tmin, self.tmax), (-1., 1.))) 

292 plotenv.setPen(plotenv.style.marker_color.qt_color) 

293 

294 rect = qc.QRectF( 

295 plotenv.umin, 0., 

296 plotenv.umax-plotenv.umin, plotenv.widget.height()) 

297 

298 for marker in markers: 

299 plotenv.draw_vline(project, marker.tmin, -1., 1.) 

300 s = marker.get_label() 

301 if s: 

302 if isinstance(marker, PhaseMarker): 

303 anchor = 'BC' 

304 v = project.v(-1.) 

305 else: 

306 anchor = 'BR' 

307 v = project.v(-1.) 

308 

309 lab = Label( 

310 project.u(marker.tmin), v, s, 

311 anchor=anchor, 

312 style=plotenv.style.marker_textstyle, 

313 keep_inside=rect, 

314 head=(1.0, 3.0)) 

315 

316 lab.draw(plotenv) 

317 

318 def _draw_traces(self, plotenv): 

319 project = plotenv.projector(self.tyrange) 

320 tpp = (project.xmax - project.xmin) / (project.umax - project.umin) 

321 if self._time_per_pixel != tpp: 

322 self._empty_cache() 

323 self._time_per_pixel = tpp 

324 

325 for tr in self.traces: 

326 udata, vdata = self._projected_trace_data( 

327 tr, 

328 project, 

329 plotenv.style.trace_resolution) 

330 

331 qpoints = make_QPolygonF(udata, vdata) 

332 plotenv.setPen(plotenv.style.trace_color.qt_color) 

333 plotenv.drawPolyline(qpoints) 

334 

335 def _projected_trace_data(self, tr, project, trace_resolution): 

336 n = tr.data_len() 

337 if trace_resolution > 0 \ 

338 and n > 2 \ 

339 and tr.deltat < 0.5 / trace_resolution*self._time_per_pixel: 

340 

341 spp = int(self._time_per_pixel / tr.deltat / trace_resolution) 

342 if tr not in self._ydata_cache: 

343 nok = (tr.data_len() // spp) * spp 

344 ydata_rs = tr.ydata[:nok].reshape((-1, spp)) 

345 ydata = num.empty((nok // spp)*2) 

346 ydata[::2] = num.min(ydata_rs, axis=1) 

347 ydata[1::2] = num.max(ydata_rs, axis=1) 

348 self._ydata_cache[tr] = ydata 

349 else: 

350 ydata = self._ydata_cache[tr] 

351 

352 udata_min = float( 

353 project.u(tr.tmin)) 

354 udata_max = float( 

355 project.u(tr.tmin+0.5*tr.deltat*spp*(ydata.size-1))) 

356 else: 

357 ydata = tr.ydata 

358 udata_min = float(project.u(tr.tmin)) 

359 udata_max = float(project.u(tr.tmin+tr.deltat*(n-1))) 

360 

361 vdata = project.v(ydata) 

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

363 return udata, vdata 

364 

365 def _empty_cache(self): 

366 self._ydata_cache = {} 

367 

368 

369def tlen(x): 

370 return x.tmax-x.tmin 

371 

372 

373def is_relevant(x, tmin, tmax): 

374 return tmax >= x.tmin and x.tmax >= tmin 

375 

376 

377class MarkerStore(object): 

378 def __init__(self): 

379 self.empty() 

380 

381 def empty(self): 

382 self._by_tmin = pile.Sorted([], 'tmin') 

383 self._by_tmax = pile.Sorted([], 'tmax') 

384 self._by_tlen = pile.Sorted([], tlen) 

385 self._adjust_minmax() 

386 self._listeners = [] 

387 

388 def relevant(self, tmin, tmax, selector=None): 

389 if not self._by_tmin or not is_relevant(self, tmin, tmax): 

390 return [] 

391 

392 if selector is None: 

393 return [ 

394 x for x in self._by_tmin.with_key_in(tmin-self.tlenmax, tmax) 

395 if is_relevant(x, tmin, tmax)] 

396 else: 

397 return [ 

398 x for x in self._by_tmin.with_key_in(tmin-self.tlenmax, tmax) 

399 if is_relevant(x, tmin, tmax) and selector(x)] 

400 

401 def insert(self, x): 

402 self._by_tmin.insert(x) 

403 self._by_tmax.insert(x) 

404 self._by_tlen.insert(x) 

405 

406 self._adjust_minmax() 

407 self._notify_listeners('insert', x) 

408 

409 def remove(self, x): 

410 self._by_tmin.remove(x) 

411 self._by_tmax.remove(x) 

412 self._by_tlen.remove(x) 

413 

414 self._adjust_minmax() 

415 self._notify_listeners('remove', x) 

416 

417 def insert_many(self, x): 

418 self._by_tmin.insert_many(x) 

419 self._by_tmax.insert_many(x) 

420 self._by_tlen.insert_many(x) 

421 

422 self._adjust_minmax() 

423 self._notify_listeners('insert_many', x) 

424 

425 def remove_many(self, x): 

426 self._by_tmin.remove_many(x) 

427 self._by_tmax.remove_many(x) 

428 self._by_tlen.remove_many(x) 

429 

430 self._adjust_minmax() 

431 self._notify_listeners('remove_many', x) 

432 

433 def add_listener(self, obj): 

434 self._listeners.append(util.smart_weakref(obj)) 

435 

436 def _adjust_minmax(self): 

437 if self._by_tmin: 

438 self.tmin = self._by_tmin.min().tmin 

439 self.tmax = self._by_tmax.max().tmax 

440 self.tlenmax = tlen(self._by_tlen.max()) 

441 else: 

442 self.tmin = None 

443 self.tmax = None 

444 self.tlenmax = None 

445 

446 def _notify_listeners(self, what, x): 

447 for ref in self._listeners: 

448 obj = ref() 

449 if obj: 

450 obj(what, x) 

451 

452 def __iter__(self): 

453 return iter(self._by_tmin) 

454 

455 

456class DrumViewMain(qw.QWidget, TalkieConnectionOwner): 

457 

458 def __init__(self, pile, *args): 

459 qw.QWidget.__init__(self, *args) 

460 

461 self.setAttribute(qc.Qt.WA_AcceptTouchEvents, True) 

462 

463 st = self.state = State() 

464 self.markers = MarkerStore() 

465 self.markers.add_listener(self._markers_changed) 

466 

467 self.pile = pile 

468 self.pile.add_listener(self._pile_changed) 

469 

470 self._drumlines = {} 

471 self._wheel_pos = 0 

472 self._iline_float = None 

473 self._project_iline_to_screen = ProjectYV( 

474 (st.iline-0.5, st.iline+st.nlines+0.5), (0., self.height())) 

475 self._access_counter = 0 

476 self._waiting_for_first_data = True 

477 

478 self._init_touch() 

479 self._init_following() 

480 

481 self.talkie_connect( 

482 self.state, 

483 '', 

484 self._state_changed, 

485 drop_args=True) 

486 

487 self.talkie_connect( 

488 self.state, 

489 ['filters', 'nslc', 'tline'], 

490 self._drop_cached_drumlines, 

491 drop_args=True) 

492 

493 self.talkie_connect( 

494 self.state, 

495 'style.background_color', 

496 self._adjust_background_color, 

497 drop_args=True) 

498 

499 self.talkie_connect( 

500 self.state, 

501 'follow', 

502 self._adjust_follow, 

503 drop_args=True) 

504 

505 self._adjust_background_color() 

506 self._adjust_follow() 

507 

508 def goto_data_begin(self): 

509 if self.pile.tmin: 

510 self.state.tmin = self.pile.tmin 

511 

512 def goto_data_end(self): 

513 if self.pile.tmax: 

514 self.state.tmax = self.pile.tmax 

515 

516 def next_nslc(self): 

517 nslc_ids = sorted(self.pile.nslc_ids.keys()) 

518 if nslc_ids: 

519 try: 

520 i = nslc_ids.index(self.state.nslc) 

521 except ValueError: 

522 i = -1 

523 

524 self.state.nslc = nslc_ids[(i+1) % len(nslc_ids)] 

525 

526 def title(self): 

527 return ' '.join(x for x in self.state.nslc if x) 

528 

529 def _state_changed(self): 

530 self.update() 

531 

532 def _markers_changed(self, *_): 

533 self.update() 

534 

535 def _pile_changed(self, what, content): 

536 self._adjust_first_data() 

537 

538 delete = [] 

539 tline = self.state.tline 

540 for iline in self._drumlines.keys(): 

541 for c in content: 

542 if c.overlaps(iline*tline, (iline+1)*tline): 

543 delete.append(iline) 

544 

545 for iline in delete: 

546 del self._drumlines[iline] 

547 

548 self.update() 

549 

550 def _init_following(self): 

551 self._follow_timer = None 

552 self._following_interrupted = False 

553 self._following_interrupted_tstart = None 

554 

555 def interrupt_following(self): 

556 self._following_interrupted = True 

557 self._following_interrupted_tstart = time.time() 

558 

559 def _follow_update(self): 

560 if self._following_interrupted: 

561 now = time.time() 

562 if self._following_interrupted_tstart < now - 20.: 

563 self._following_interrupted = False 

564 else: 

565 return 

566 

567 now = time.time() 

568 iline = int(math.ceil(now / self.state.tline))-self.state.nlines 

569 if iline != self.state.iline: 

570 self.state.iline = iline 

571 

572 def _adjust_follow(self): 

573 follow = self.state.follow 

574 if follow and not self._follow_timer: 

575 self._follow_timer = qc.QTimer(self) 

576 self._follow_timer.timeout.connect(self._follow_update) 

577 self._follow_timer.setInterval(1000) 

578 self._follow_update() 

579 self._follow_timer.start() 

580 

581 elif not follow and self._follow_timer: 

582 self._follow_timer.stop() 

583 

584 def _draw(self, plotenv): 

585 self._adjust_first_data() 

586 plotenv.umin = 0. 

587 plotenv.umax = self.width() 

588 self._draw_title(plotenv) 

589 self._draw_time_axis(plotenv) 

590 self._draw_lines(plotenv) 

591 

592 def _draw_title(self, plotenv): 

593 font = plotenv.style.title_textstyle.qt_font 

594 lab = Label( 

595 0.5*(plotenv.umin + plotenv.umax), 

596 font.pointSize(), 

597 self.title(), 

598 anchor='TC', 

599 style=plotenv.style.title_textstyle) 

600 

601 lab.draw(plotenv) 

602 

603 def _draw_time_axis(self, plotenv): 

604 pass 

605 

606 def _draw_lines(self, plotenv): 

607 st = self.state 

608 drumlines_seen = [] 

609 for iline in range(st.iline, st.iline+st.nlines): 

610 self._update_line(iline) 

611 drumline = self._drumlines.get(iline, None) 

612 if drumline: 

613 drumlines_seen.append(drumline) 

614 

615 self._autoscale(drumlines_seen) 

616 

617 top_margin = 50. 

618 bottom_margin = 50. 

619 

620 self._project_iline_to_screen = ProjectYV( 

621 (st.iline-0.5, st.iline+st.nlines-0.5), 

622 (top_margin, self.height()-bottom_margin)) 

623 

624 for drumline in drumlines_seen: 

625 plotenv.vmin = self._project_iline_to_screen.v(drumline.iline-0.5) 

626 plotenv.vmax = self._project_iline_to_screen.v(drumline.iline+0.5) 

627 markers = self._relevant_markers(drumline.tmin, drumline.tmax) 

628 drumline.draw(plotenv, markers) 

629 drumline.access_counter = self._access_counter 

630 self._access_counter += 1 

631 

632 drumlines_by_access = sorted( 

633 self._drumlines.values(), key=lambda dl: dl.access_counter) 

634 

635 for drumline in drumlines_by_access[:-st.npages_cache*st.nlines]: 

636 del self._drumlines[drumline.iline] 

637 

638 def _relevant_markers(self, tmin, tmax): 

639 return self.markers.relevant( 

640 tmin, tmax, 

641 lambda m: not m.nslc_ids or m.match_nslc(self.state.nslc)) 

642 

643 def _autoscale(self, drumlines): 

644 if not drumlines: 

645 return 

646 

647 st = self.state 

648 

649 data = [] 

650 for drumline in drumlines: 

651 try: 

652 data.append(drumline.data_range(st.scaling.base)) 

653 except Empty: 

654 pass 

655 

656 if not data: 

657 data = [[0, 0]] 

658 

659 mi, ma = num.array(data, dtype=float).T 

660 gain = st.scaling.gain 

661 if st.scaling.mode == 'same': 

662 ymin, ymax = mi.min(), ma.max() 

663 for drumline in drumlines: 

664 drumline.set_yrange(ymin/gain, ymax/gain) 

665 elif st.scaling.mode == 'individual': 

666 for drumline, ymin, ymax in zip(drumlines, mi, ma): 

667 drumline.set_yrange(ymin/gain, ymax/gain) 

668 elif st.scaling.mode == 'fixed': 

669 for drumline in drumlines: 

670 drumline.set_yrange(st.scaling.min/gain, st.scaling.max/gain) 

671 

672 def _update_line(self, iline): 

673 

674 if iline not in self._drumlines: 

675 st = self.state 

676 tmin = iline*st.tline 

677 tmax = (iline+1)*st.tline 

678 if st.filters: 

679 tpad = max(x.tpad() for x in st.filters) 

680 else: 

681 tpad = 0.0 

682 

683 traces = self.pile.all( 

684 tmin=iline*st.tline, 

685 tmax=(iline+1)*st.tline, 

686 tpad=tpad, 

687 trace_selector=lambda tr: tr.nslc_id == st.nslc, 

688 keep_current_files_open=True, 

689 accessor_id=id(self)) 

690 

691 for tr in traces: 

692 for filter in st.filters: 

693 filter.apply(tr) 

694 

695 self._drumlines[iline] = DrumLine( 

696 iline, tmin, tmax, traces, self.state) 

697 

698 def _drop_cached_drumlines(self): 

699 self._drumlines = {} 

700 

701 def _adjust_background_color(self): 

702 color = self.state.style.background_color.qt_color 

703 

704 p = qg.QPalette() 

705 p.setColor(qg.QPalette.Background, color) 

706 self.setAutoFillBackground(True) 

707 self.setPalette(p) 

708 

709 def _adjust_first_data(self): 

710 if self._waiting_for_first_data: 

711 if self.pile.tmin: 

712 self.next_nslc() 

713 self.goto_data_end() 

714 self._waiting_for_first_data = False 

715 

716 # qt event handlers 

717 

718 def paintEvent(self, event): 

719 plotenv = PlotEnv(self) 

720 plotenv.style = self.state.style 

721 plotenv.widget = self 

722 

723 if plotenv.style.antialiasing: 

724 plotenv.setRenderHint(qg.QPainter.Antialiasing) 

725 

726 self._draw(plotenv) 

727 

728 def event(self, event): 

729 

730 if event.type() in ( 

731 qc.QEvent.TouchBegin, 

732 qc.QEvent.TouchUpdate, 

733 qc.QEvent.TouchEnd, 

734 qc.QEvent.TouchCancel): 

735 

736 return self._touch_event(event) 

737 

738 return qw.QWidget.event(self, event) 

739 

740 def _init_touch(self): 

741 self._gesture = None 

742 

743 def _touch_event(self, event): 

744 if event.type() == qc.QEvent.TouchBegin: 

745 self._gesture = DrumGesture(self) 

746 self._gesture.update(event) 

747 

748 elif event.type() == qc.QEvent.TouchUpdate: 

749 self._gesture.update(event) 

750 

751 elif event.type() == qc.QEvent.TouchEnd: 

752 self._gesture.update(event) 

753 self._gesture.end() 

754 self._gesture = None 

755 

756 elif event.type() == qc.QEvent.TouchCancel: 

757 self._gesture.update(event) 

758 self._gesture.cancel() 

759 self._gesture = None 

760 

761 return True 

762 

763 def wheelEvent(self, event): 

764 

765 self.interrupt_following() 

766 

767 self._wheel_pos += event.angleDelta().y() 

768 

769 n = self._wheel_pos // 120 

770 self._wheel_pos = self._wheel_pos % 120 

771 if n == 0: 

772 return 

773 

774 amount = max(1., self.state.nlines/24.) 

775 wdelta = amount * n 

776 

777 if event.modifiers() & qc.Qt.ControlModifier: 

778 proj = self._project_iline_to_screen 

779 

780 anchor = ( 

781 proj.y(event.y()) - proj.ymin) / (proj.ymax - proj.ymin) 

782 

783 nlines = max(1, self.state.nlines + int(round(wdelta))) 

784 

785 if self._iline_float is None: 

786 iline_float = float(self.state.iline) 

787 else: 

788 iline_float = self._iline_float 

789 

790 self._iline_float = iline_float-anchor*wdelta 

791 

792 self.state.iline = int(round(iline_float)) 

793 self.state.nlines = nlines 

794 

795 else: 

796 self.state.iline -= int(wdelta) 

797 self._iline_float = None 

798 

799 def keyPressEvent(self, event): 

800 

801 keytext = str(event.text()) 

802 

803 if event.key() == qc.Qt.Key_PageDown: 

804 self.interrupt_following() 

805 self.state.iline += self.state.nlines 

806 

807 elif event.key() == qc.Qt.Key_PageUp: 

808 self.interrupt_following() 

809 self.state.iline -= self.state.nlines 

810 

811 elif keytext == '+': 

812 self.state.scaling.gain *= 1.5 

813 

814 elif keytext == '-': 

815 self.state.scaling.gain *= 1.0 / 1.5 

816 

817 elif keytext == ' ': 

818 self.next_nslc() 

819 

820 elif keytext == 'p': 

821 print(self.state) 

822 

823 

824tline_choices = [10., 30., 60., 120., 300., 600., 1200., 3600.] 

825 

826 

827class DrumGesture(object): 

828 

829 def __init__(self, view): 

830 self._view = view 

831 self._active = False 

832 self.begin() 

833 

834 def begin(self): 

835 self._active = True 

836 self._iline = self._view.state.iline 

837 self._nlines = self._view.state.nlines 

838 self._tline = self._view.state.tline 

839 self._proj = self._view._project_iline_to_screen 

840 self._gain = self._view.state.scaling.gain 

841 self._max_len_tps = 0 

842 

843 def end(self): 

844 self._active = False 

845 

846 def cancel(self): 

847 self.end() 

848 

849 def update(self, event): 

850 if not self._active: 

851 return 

852 

853 tps = event.touchPoints() 

854 proj = self._proj 

855 

856 self._max_len_tps = max(len(tps), self._max_len_tps) 

857 

858 if len(tps) == 1 and self._max_len_tps < 2: 

859 tp = tps[0] 

860 iline = proj.y(tp.pos().y()) 

861 iline_start = proj.y(tp.startPos().y()) 

862 idelta = int(round(iline - iline_start)) 

863 iline = self._iline - idelta 

864 if iline != self._view.state.iline: 

865 self._view.interrupt_following() 

866 self._view.state.iline = iline 

867 

868 if len(tps) == 2: 

869 # self._view.state.iline = self._iline 

870 

871 u = [ 

872 (tp.pos().x(), tp.startPos().x()) 

873 for tp in tps] 

874 

875 y = [ 

876 (proj.y(tp.pos().y()), proj.y(tp.startPos().y())) 

877 for tp in tps] 

878 

879 v = [ 

880 (tp.pos().y(), tp.startPos().y()) 

881 for tp in tps] 

882 

883 if abs(u[0][1] - u[1][1]) < abs(v[0][1] - v[1][1]): 

884 

885 vclip = self._view.size().height() * 0.05 

886 d0 = max(vclip, abs(v[0][1] - v[1][1])) 

887 d1 = max(vclip, abs(v[0][0] - v[1][0])) 

888 

889 nlines = min(max(1, int(round(self._nlines * (d0 / d1)))), 50) 

890 

891 idelta = int(round( 

892 0.5 * ((y[0][0]+y[1][0]) - (y[0][1]+y[1][1])) 

893 * (nlines / self._nlines))) 

894 

895 iline = self._iline - idelta 

896 

897 self._view.interrupt_following 

898 if self._view.state.nlines != nlines \ 

899 or self._view.state.iline != iline: 

900 self._view.interrupt_following() 

901 self._view.state.iline = iline 

902 self._view.state.nlines = nlines 

903 

904 else: 

905 if abs(u[0][0] - u[0][1]) + abs(u[1][0] - u[1][1]) \ 

906 > abs(v[0][0] - v[0][1]) + abs(v[1][0] - v[1][1]): 

907 

908 ustretch = abs(u[0][1] - u[1][1]) \ 

909 / (abs(u[0][0] - u[1][0]) 

910 + self._view.size().width() * 0.05) 

911 

912 print('ustretch', ustretch) 

913 log_tline_choices = num.log(tline_choices) 

914 log_tline = num.log(ustretch * self._tline) 

915 

916 ichoice = num.argmin( 

917 num.abs(log_tline_choices - log_tline)) 

918 tline = tline_choices[ichoice] 

919 # yanchor = 0.5 * (y[0][1] + y[1][1]) 

920 # r = (yanchor - proj.ymin) / (proj.ymax - proj.ymin) 

921 

922 if self._view.state.tline != tline: 

923 self._view.state.iline = int( 

924 round(self._iline / (tline / self._tline))) 

925 self._view.state.tline = tline 

926 

927 else: 

928 vstretch = 10**( 

929 -((v[0][0] - v[0][1]) + (v[1][0] - v[1][1])) 

930 / self._view.size().height()) 

931 

932 self._view.state.scaling.gain = self._gain * vstretch