1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6from __future__ import absolute_import, print_function, division 

7 

8import math 

9import time 

10import weakref 

11import numpy as num 

12 

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

14 

15from pyrocko.gui.talkie import Listener 

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

17from pyrocko.gui_util import make_QPolygonF, PhaseMarker 

18from pyrocko import trace, util, pile 

19 

20 

21def lim(a, x, b): 

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

23 

24 

25class Label(object): 

26 def __init__( 

27 self, x, y, label_str, 

28 anchor='BL', 

29 style=None, 

30 keep_inside=None, 

31 head=None): 

32 

33 if style is None: 

34 style = TextStyle() 

35 

36 text = qg.QTextDocument() 

37 font = style.qt_font 

38 if font: 

39 text.setDefaultFont(font) 

40 

41 color = style.color.qt_color 

42 

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

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

45 

46 self.position = x, y 

47 self.anchor = anchor 

48 self.text = text 

49 self.style = style 

50 self.keep_inside = keep_inside 

51 if head: 

52 self.head = head 

53 else: 

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

55 

56 def draw(self, p): 

57 s = self.text.size() 

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

59 

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

61 anchor = self.anchor 

62 

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

64 

65 oy = self.head[0] * pxs 

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

67 

68 if 'L' in anchor: 

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

70 

71 elif 'R' in anchor: 

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

73 

74 elif 'C' in anchor: 

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

76 

77 if 'B' in anchor: 

78 ty -= rect.height() + oy 

79 

80 elif 'T' in anchor: 

81 ty += oy 

82 

83 elif 'M' in anchor: 

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

85 

86 rect.translate(tx, ty) 

87 

88 if self.keep_inside: 

89 keep_inside = self.keep_inside 

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

91 rect.moveTop(keep_inside.top()) 

92 

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

94 rect.moveBottom(keep_inside.bottom()) 

95 

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

97 rect.moveLeft(keep_inside.left()) 

98 

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

100 rect.moveRight(keep_inside.right()) 

101 

102 poly = None 

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

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

105 

106 if 'T' in anchor: 

107 a, b = t, b 

108 elif 'B' in anchor: 

109 a, b = b, t 

110 elif 'M' in anchor: 

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

112 

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

114 

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

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

117 

118 poly = make_QPolygonF(px, py) 

119 

120 tx = rect.left() 

121 ty = rect.top() 

122 

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

124 oldpen = p.pen() 

125 oldbrush = p.brush() 

126 if not self.style.outline: 

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

128 

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

130 if poly: 

131 p.drawPolygon(poly) 

132 else: 

133 p.drawRect(rect) 

134 

135 if self.style.background_color: 

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

137 

138 p.setPen(oldpen) 

139 p.setBrush(oldbrush) 

140 

141 p.translate(tx, ty) 

142 self.text.drawContents(p) 

143 p.translate(-tx, -ty) 

144 

145 

146class ProjectXU(object): 

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

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

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

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

151 

152 def u(self, x): 

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

154 

155 def x(self, u): 

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

157 

158 

159class ProjectYV(object): 

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

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

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

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

164 

165 def v(self, y): 

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

167 

168 def y(self, v): 

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

170 

171 

172class ProjectXYUV(object): 

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

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

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

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

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

178 

179 def u(self, x): 

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

181 

182 def v(self, y): 

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

184 

185 def x(self, u): 

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

187 

188 def y(self, v): 

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

190 

191 

192class PlotEnv(qg.QPainter): 

193 def __init__(self, *args): 

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

195 self.umin = 0. 

196 self.umax = 1. 

197 self.vmin = 0. 

198 self.vmax = 1. 

199 

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

201 u = project.u(x) 

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

203 self.drawLine(line) 

204 

205 def projector(self, xyr): 

206 return ProjectXYUV(xyr, self.uvrange) 

207 

208 @property 

209 def uvrange(self): 

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

211 

212 

213def time_fmt_drumline(t): 

214 ti = int(t) 

215 if ti % 60 == 0: 

216 fmt = '%H:%M' 

217 else: 

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

219 

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

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

222 

223 return fmt 

224 

225 

226class Empty(Exception): 

227 pass 

228 

229 

230class DrumLine(qc.QObject, Listener): 

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

232 qc.QObject.__init__(self) 

233 self.traces = traces 

234 self.tmin = tmin 

235 self.tmax = tmax 

236 self.ymin = 0. 

237 self.ymax = 1. 

238 self.ymin_data = 0. 

239 self.ymax_data = 1. 

240 self.iline = iline 

241 self._last_mode = None 

242 self._ydata_cache = {} 

243 self._time_per_pixel = None 

244 self.access_counter = 0 

245 

246 state.add_listener( 

247 self.listener_no_args(self._empty_cache), 

248 path='style.trace_resolution') 

249 

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

251 if not self.traces: 

252 raise Empty() 

253 

254 modemap = { 

255 'min-max': 'minmax', 

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

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

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

259 } 

260 

261 if self._last_mode != mode: 

262 mi, ma = trace.minmax( 

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

264 self.ymin_data = mi 

265 self.ymax_data = ma 

266 self._last_mode = mode 

267 

268 return self.ymin_data, self.ymax_data 

269 

270 def set_yrange(self, ymin, ymax): 

271 if ymax == ymin: 

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

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

274 

275 self.ymin = ymin 

276 self.ymax = ymax 

277 

278 @property 

279 def tyrange(self): 

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

281 

282 def draw(self, plotenv, markers): 

283 self._draw_traces(plotenv) 

284 self._draw_time_label(plotenv) 

285 self._draw_markers(plotenv, markers) 

286 

287 def _draw_time_label(self, plotenv): 

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

289 font = plotenv.style.label_textstyle.qt_font 

290 lab = Label( 

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

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

293 

294 lab.draw(plotenv) 

295 

296 def _draw_markers(self, plotenv, markers): 

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

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

299 

300 rect = qc.QRectF( 

301 plotenv.umin, 0., 

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

303 

304 for marker in markers: 

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

306 s = marker.get_label() 

307 if s: 

308 if isinstance(marker, PhaseMarker): 

309 anchor = 'BC' 

310 v = project.v(-1.) 

311 else: 

312 anchor = 'BR' 

313 v = project.v(-1.) 

314 

315 lab = Label( 

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

317 anchor=anchor, 

318 style=plotenv.style.marker_textstyle, 

319 keep_inside=rect, 

320 head=(1.0, 3.0)) 

321 

322 lab.draw(plotenv) 

323 

324 def _draw_traces(self, plotenv): 

325 project = plotenv.projector(self.tyrange) 

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

327 if self._time_per_pixel != tpp: 

328 self._empty_cache() 

329 self._time_per_pixel = tpp 

330 

331 for tr in self.traces: 

332 udata, vdata = self._projected_trace_data( 

333 tr, 

334 project, 

335 plotenv.style.trace_resolution) 

336 

337 qpoints = make_QPolygonF(udata, vdata) 

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

339 plotenv.drawPolyline(qpoints) 

340 

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

342 n = tr.data_len() 

343 if trace_resolution > 0 \ 

344 and n > 2 \ 

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

346 

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

348 if tr not in self._ydata_cache: 

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

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

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

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

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

354 self._ydata_cache[tr] = ydata 

355 else: 

356 ydata = self._ydata_cache[tr] 

357 

358 udata_min = float( 

359 project.u(tr.tmin)) 

360 udata_max = float( 

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

362 else: 

363 ydata = tr.ydata 

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

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

366 

367 vdata = project.v(ydata) 

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

369 return udata, vdata 

370 

371 def _empty_cache(self): 

372 self._ydata_cache = {} 

373 

374 

375def tlen(x): 

376 return x.tmax-x.tmin 

377 

378 

379def is_relevant(x, tmin, tmax): 

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

381 

382 

383class MarkerStore(object): 

384 def __init__(self): 

385 self.empty() 

386 

387 def empty(self): 

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

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

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

391 self._adjust_minmax() 

392 self._listeners = [] 

393 

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

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

396 return [] 

397 

398 if selector is None: 

399 return [ 

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

401 if is_relevant(x, tmin, tmax)] 

402 else: 

403 return [ 

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

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

406 

407 def insert(self, x): 

408 self._by_tmin.insert(x) 

409 self._by_tmax.insert(x) 

410 self._by_tlen.insert(x) 

411 

412 self._adjust_minmax() 

413 self._notify_listeners('insert', x) 

414 

415 def remove(self, x): 

416 self._by_tmin.remove(x) 

417 self._by_tmax.remove(x) 

418 self._by_tlen.remove(x) 

419 

420 self._adjust_minmax() 

421 self._notify_listeners('remove', x) 

422 

423 def insert_many(self, x): 

424 self._by_tmin.insert_many(x) 

425 self._by_tmax.insert_many(x) 

426 self._by_tlen.insert_many(x) 

427 

428 self._adjust_minmax() 

429 self._notify_listeners('insert_many', x) 

430 

431 def remove_many(self, x): 

432 self._by_tmin.remove_many(x) 

433 self._by_tmax.remove_many(x) 

434 self._by_tlen.remove_many(x) 

435 

436 self._adjust_minmax() 

437 self._notify_listeners('remove_many', x) 

438 

439 def add_listener(self, obj): 

440 self._listeners.append(weakref.ref(obj)) 

441 

442 def _adjust_minmax(self): 

443 if self._by_tmin: 

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

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

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

447 else: 

448 self.tmin = None 

449 self.tmax = None 

450 self.tlenmax = None 

451 

452 def _notify_listeners(self, what, x): 

453 for ref in self._listeners: 

454 obj = ref() 

455 if obj: 

456 obj(what, x) 

457 

458 def __iter__(self): 

459 return iter(self._by_tmin) 

460 

461 

462class DrumViewMain(qw.QWidget, Listener): 

463 

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

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

466 

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

468 

469 st = self.state = State() 

470 self.markers = MarkerStore() 

471 self.markers.add_listener(self.listener_no_args(self._markers_changed)) 

472 

473 self.pile = pile 

474 self.pile.add_listener(self.listener(self._pile_changed)) 

475 

476 self._drumlines = {} 

477 self._wheel_pos = 0 

478 self._iline_float = None 

479 self._project_iline_to_screen = ProjectYV( 

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

481 self._access_counter = 0 

482 self._waiting_for_first_data = True 

483 

484 self._init_touch() 

485 self._init_following() 

486 

487 sal = self.state.add_listener 

488 

489 sal(self.listener_no_args(self._state_changed)) 

490 sal(self.listener_no_args(self._drop_cached_drumlines), 'filters') 

491 sal(self.listener_no_args(self._drop_cached_drumlines), 'nslc') 

492 sal(self.listener_no_args(self._drop_cached_drumlines), 'tline') 

493 sal(self.listener_no_args( 

494 self._adjust_background_color), 'style.background_color') 

495 sal(self.listener_no_args(self._adjust_follow), 'follow') 

496 

497 self._adjust_background_color() 

498 self._adjust_follow() 

499 

500 def goto_data_begin(self): 

501 if self.pile.tmin: 

502 self.state.tmin = self.pile.tmin 

503 

504 def goto_data_end(self): 

505 if self.pile.tmax: 

506 self.state.tmax = self.pile.tmax 

507 

508 def next_nslc(self): 

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

510 if nslc_ids: 

511 try: 

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

513 except ValueError: 

514 i = -1 

515 

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

517 

518 def title(self): 

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

520 

521 def _state_changed(self): 

522 self.update() 

523 

524 def _markers_changed(self): 

525 self.update() 

526 

527 def _pile_changed(self, what, content): 

528 self._adjust_first_data() 

529 

530 delete = [] 

531 tline = self.state.tline 

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

533 for c in content: 

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

535 delete.append(iline) 

536 

537 for iline in delete: 

538 del self._drumlines[iline] 

539 

540 self.update() 

541 

542 def _init_following(self): 

543 self._follow_timer = None 

544 self._following_interrupted = False 

545 self._following_interrupted_tstart = None 

546 

547 def interrupt_following(self): 

548 self._following_interrupted = True 

549 self._following_interrupted_tstart = time.time() 

550 

551 def _follow_update(self): 

552 if self._following_interrupted: 

553 now = time.time() 

554 if self._following_interrupted_tstart < now - 20.: 

555 self._following_interrupted = False 

556 else: 

557 return 

558 

559 now = time.time() 

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

561 if iline != self.state.iline: 

562 self.state.iline = iline 

563 

564 def _adjust_follow(self): 

565 follow = self.state.follow 

566 if follow and not self._follow_timer: 

567 self._follow_timer = qc.QTimer(self) 

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

569 self._follow_timer.setInterval(1000) 

570 self._follow_update() 

571 self._follow_timer.start() 

572 

573 elif not follow and self._follow_timer: 

574 self._follow_timer.stop() 

575 

576 def _draw(self, plotenv): 

577 self._adjust_first_data() 

578 plotenv.umin = 0. 

579 plotenv.umax = self.width() 

580 self._draw_title(plotenv) 

581 self._draw_time_axis(plotenv) 

582 self._draw_lines(plotenv) 

583 

584 def _draw_title(self, plotenv): 

585 font = plotenv.style.title_textstyle.qt_font 

586 lab = Label( 

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

588 font.pointSize(), 

589 self.title(), 

590 anchor='TC', 

591 style=plotenv.style.title_textstyle) 

592 

593 lab.draw(plotenv) 

594 

595 def _draw_time_axis(self, plotenv): 

596 pass 

597 

598 def _draw_lines(self, plotenv): 

599 st = self.state 

600 drumlines_seen = [] 

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

602 self._update_line(iline) 

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

604 if drumline: 

605 drumlines_seen.append(drumline) 

606 

607 self._autoscale(drumlines_seen) 

608 

609 top_margin = 50. 

610 bottom_margin = 50. 

611 

612 self._project_iline_to_screen = ProjectYV( 

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

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

615 

616 for drumline in drumlines_seen: 

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

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

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

620 drumline.draw(plotenv, markers) 

621 drumline.access_counter = self._access_counter 

622 self._access_counter += 1 

623 

624 drumlines_by_access = sorted( 

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

626 

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

628 del self._drumlines[drumline.iline] 

629 

630 def _relevant_markers(self, tmin, tmax): 

631 return self.markers.relevant( 

632 tmin, tmax, 

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

634 

635 def _autoscale(self, drumlines): 

636 if not drumlines: 

637 return 

638 

639 st = self.state 

640 

641 data = [] 

642 for drumline in drumlines: 

643 try: 

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

645 except Empty: 

646 pass 

647 

648 if not data: 

649 data = [[0, 0]] 

650 

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

652 gain = st.scaling.gain 

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

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

655 for drumline in drumlines: 

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

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

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

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

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

661 for drumline in drumlines: 

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

663 

664 def _update_line(self, iline): 

665 

666 if iline not in self._drumlines: 

667 st = self.state 

668 tmin = iline*st.tline 

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

670 if st.filters: 

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

672 else: 

673 tpad = 0.0 

674 

675 traces = self.pile.all( 

676 tmin=iline*st.tline, 

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

678 tpad=tpad, 

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

680 keep_current_files_open=True, 

681 accessor_id=id(self)) 

682 

683 for tr in traces: 

684 for filter in st.filters: 

685 filter.apply(tr) 

686 

687 self._drumlines[iline] = DrumLine( 

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

689 

690 def _drop_cached_drumlines(self): 

691 self._drumlines = {} 

692 

693 def _adjust_background_color(self): 

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

695 

696 p = qg.QPalette() 

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

698 self.setAutoFillBackground(True) 

699 self.setPalette(p) 

700 

701 def _adjust_first_data(self): 

702 if self._waiting_for_first_data: 

703 if self.pile.tmin: 

704 self.next_nslc() 

705 self.goto_data_end() 

706 self._waiting_for_first_data = False 

707 

708 # qt event handlers 

709 

710 def paintEvent(self, event): 

711 plotenv = PlotEnv(self) 

712 plotenv.style = self.state.style 

713 plotenv.widget = self 

714 

715 if plotenv.style.antialiasing: 

716 plotenv.setRenderHint(qg.QPainter.Antialiasing) 

717 

718 self._draw(plotenv) 

719 

720 def event(self, event): 

721 

722 if event.type() in ( 

723 qc.QEvent.TouchBegin, 

724 qc.QEvent.TouchUpdate, 

725 qc.QEvent.TouchEnd, 

726 qc.QEvent.TouchCancel): 

727 

728 return self._touch_event(event) 

729 

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

731 

732 def _init_touch(self): 

733 self._gesture = None 

734 

735 def _touch_event(self, event): 

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

737 self._gesture = DrumGesture(self) 

738 self._gesture.update(event) 

739 

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

741 self._gesture.update(event) 

742 

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

744 self._gesture.update(event) 

745 self._gesture.end() 

746 self._gesture = None 

747 

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

749 self._gesture.update(event) 

750 self._gesture.cancel() 

751 self._gesture = None 

752 

753 return True 

754 

755 def wheelEvent(self, event): 

756 

757 self.interrupt_following() 

758 

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

760 

761 n = self._wheel_pos // 120 

762 self._wheel_pos = self._wheel_pos % 120 

763 if n == 0: 

764 return 

765 

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

767 wdelta = amount * n 

768 

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

770 proj = self._project_iline_to_screen 

771 

772 anchor = ( 

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

774 

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

776 

777 if self._iline_float is None: 

778 iline_float = float(self.state.iline) 

779 else: 

780 iline_float = self._iline_float 

781 

782 self._iline_float = iline_float-anchor*wdelta 

783 

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

785 self.state.nlines = nlines 

786 

787 else: 

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

789 self._iline_float = None 

790 

791 def keyPressEvent(self, event): 

792 

793 keytext = str(event.text()) 

794 

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

796 self.interrupt_following() 

797 self.state.iline += self.state.nlines 

798 

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

800 self.interrupt_following() 

801 self.state.iline -= self.state.nlines 

802 

803 elif keytext == '+': 

804 self.state.scaling.gain *= 1.5 

805 

806 elif keytext == '-': 

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

808 

809 elif keytext == ' ': 

810 self.next_nslc() 

811 

812 elif keytext == 'p': 

813 print(self.state) 

814 

815 

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

817 

818 

819class DrumGesture(object): 

820 

821 def __init__(self, view): 

822 self._view = view 

823 self._active = False 

824 self.begin() 

825 

826 def begin(self): 

827 self._active = True 

828 self._iline = self._view.state.iline 

829 self._nlines = self._view.state.nlines 

830 self._tline = self._view.state.tline 

831 self._proj = self._view._project_iline_to_screen 

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

833 self._max_len_tps = 0 

834 

835 def end(self): 

836 self._active = False 

837 

838 def cancel(self): 

839 self.end() 

840 

841 def update(self, event): 

842 if not self._active: 

843 return 

844 

845 tps = event.touchPoints() 

846 proj = self._proj 

847 

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

849 

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

851 tp = tps[0] 

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

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

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

855 iline = self._iline - idelta 

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

857 self._view.interrupt_following() 

858 self._view.state.iline = iline 

859 

860 if len(tps) == 2: 

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

862 

863 u = [ 

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

865 for tp in tps] 

866 

867 y = [ 

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

869 for tp in tps] 

870 

871 v = [ 

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

873 for tp in tps] 

874 

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

876 

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

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

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

880 

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

882 

883 idelta = int(round( 

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

885 * (nlines / self._nlines))) 

886 

887 iline = self._iline - idelta 

888 

889 self._view.interrupt_following 

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

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

892 self._view.interrupt_following() 

893 self._view.state.iline = iline 

894 self._view.state.nlines = nlines 

895 

896 else: 

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

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

899 

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

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

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

903 

904 print('ustretch', ustretch) 

905 log_tline_choices = num.log(tline_choices) 

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

907 

908 ichoice = num.argmin( 

909 num.abs(log_tline_choices - log_tline)) 

910 tline = tline_choices[ichoice] 

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

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

913 

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

915 self._view.state.iline = int( 

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

917 self._view.state.tline = tline 

918 

919 else: 

920 vstretch = 10**( 

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

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

923 

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