Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/drum/view.py: 0%
596 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6from __future__ import absolute_import, print_function, division
8import math
9import time
10import numpy as num
12from pyrocko.gui.qt_compat import qc, qg, qw
14from pyrocko.gui.talkie import TalkieConnectionOwner
15from pyrocko.gui.drum.state import State, TextStyle
16from pyrocko.gui_util import make_QPolygonF, PhaseMarker
17from pyrocko import trace, util, pile
20def lim(a, x, b):
21 return min(max(a, x), b)
24class Label(object):
25 def __init__(
26 self, x, y, label_str,
27 anchor='BL',
28 style=None,
29 keep_inside=None,
30 head=None):
32 if style is None:
33 style = TextStyle()
35 text = qg.QTextDocument()
36 font = style.qt_font
37 if font:
38 text.setDefaultFont(font)
40 color = style.color.qt_color
42 text.setDefaultStyleSheet('span { color: %s; }' % color.name())
43 text.setHtml('<span>%s</span>' % label_str)
45 self.position = x, y
46 self.anchor = anchor
47 self.text = text
48 self.style = style
49 self.keep_inside = keep_inside
50 if head:
51 self.head = head
52 else:
53 self.head = (0., 0.)
55 def draw(self, p):
56 s = self.text.size()
57 rect = qc.QRectF(0., 0., s.width(), s.height())
59 tx, ty = x, y = self.position
60 anchor = self.anchor
62 pxs = self.text.defaultFont().pointSize()
64 oy = self.head[0] * pxs
65 ox = self.head[1]/2. * pxs
67 if 'L' in anchor:
68 tx -= min(ox*2, rect.width()/2.)
70 elif 'R' in anchor:
71 tx -= rect.width() - min(ox*2., rect.width()/2.)
73 elif 'C' in anchor:
74 tx -= rect.width()/2.
76 if 'B' in anchor:
77 ty -= rect.height() + oy
79 elif 'T' in anchor:
80 ty += oy
82 elif 'M' in anchor:
83 ty -= rect.height()/2.
85 rect.translate(tx, ty)
87 if self.keep_inside:
88 keep_inside = self.keep_inside
89 if rect.top() < keep_inside.top():
90 rect.moveTop(keep_inside.top())
92 if rect.bottom() > keep_inside.bottom():
93 rect.moveBottom(keep_inside.bottom())
95 if rect.left() < keep_inside.left():
96 rect.moveLeft(keep_inside.left())
98 if rect.right() > keep_inside.right():
99 rect.moveRight(keep_inside.right())
101 poly = None
102 if self.head[0] != 0.:
103 l, r, t, b = rect.left(), rect.right(), rect.top(), rect.bottom()
105 if 'T' in anchor:
106 a, b = t, b
107 elif 'B' in anchor:
108 a, b = b, t
109 elif 'M' in anchor:
110 assert False, 'label cannot have head with M alignment'
112 c1, c2 = lim(l, x-ox, r), lim(l, x+ox, r)
114 px = (l, c1, x, c2, r, r, l)
115 py = (a, a, y, a, a, b, b)
117 poly = make_QPolygonF(px, py)
119 tx = rect.left()
120 ty = rect.top()
122 if self.style.outline or self.style.background_color:
123 oldpen = p.pen()
124 oldbrush = p.brush()
125 if not self.style.outline:
126 p.setPen(qg.QPen(qc.Qt.NoPen))
128 p.setBrush(self.style.background_color.qt_color)
129 if poly:
130 p.drawPolygon(poly)
131 else:
132 p.drawRect(rect)
134 if self.style.background_color:
135 p.fillRect(rect, self.style.background_color.qt_color)
137 p.setPen(oldpen)
138 p.setBrush(oldbrush)
140 p.translate(tx, ty)
141 self.text.drawContents(p)
142 p.translate(-tx, -ty)
145class ProjectXU(object):
146 def __init__(self, xr=(0., 1.), ur=(0., 1.)):
147 (self.xmin, self.xmax) = xr
148 (self.umin, self.umax) = ur
149 self.cxu = (self.umax - self.umin) / (self.xmax - self.xmin)
151 def u(self, x):
152 return self.umin + (x-self.xmin) * self.cxu
154 def x(self, u):
155 return self.xmin + (u-self.umin) / self.cxu
158class ProjectYV(object):
159 def __init__(self, yr=(0., 1.), vr=(0., 1.)):
160 (self.ymin, self.ymax) = yr
161 (self.vmin, self.vmax) = vr
162 self.cyv = (self.vmax - self.vmin) / (self.ymax - self.ymin)
164 def v(self, y):
165 return self.vmin + (y-self.ymin) * self.cyv
167 def y(self, v):
168 return self.ymin + (v-self.vmin) / self.cyv
171class ProjectXYUV(object):
172 def __init__(self, xyr=((0., 1.), (0., 1.)), uvr=((0., 1.), (0., 1.))):
173 (self.xmin, self.xmax), (self.ymin, self.ymax) = xyr
174 (self.umin, self.umax), (self.vmin, self.vmax) = uvr
175 self.cxu = (self.umax - self.umin) / (self.xmax - self.xmin)
176 self.cyv = (self.vmax - self.vmin) / (self.ymax - self.ymin)
178 def u(self, x):
179 return self.umin + (x-self.xmin) * self.cxu
181 def v(self, y):
182 return self.vmin + (y-self.ymin) * self.cyv
184 def x(self, u):
185 return self.xmin + (u-self.umin) / self.cxu
187 def y(self, v):
188 return self.ymin + (v-self.vmin) / self.cyv
191class PlotEnv(qg.QPainter):
192 def __init__(self, *args):
193 qg.QPainter.__init__(self, *args)
194 self.umin = 0.
195 self.umax = 1.
196 self.vmin = 0.
197 self.vmax = 1.
199 def draw_vline(self, project, x, y0, y1):
200 u = project.u(x)
201 line = qc.QLineF(u, project.v(y0), u, project.v(y1))
202 self.drawLine(line)
204 def projector(self, xyr):
205 return ProjectXYUV(xyr, self.uvrange)
207 @property
208 def uvrange(self):
209 return (self.umin, self.umax), (self.vmin, self.vmax)
212def time_fmt_drumline(t):
213 ti = int(t)
214 if ti % 60 == 0:
215 fmt = '%H:%M'
216 else:
217 fmt = '%H:%M:%S'
219 if ti % (3600*24) == 0:
220 fmt = fmt + ' %Y-%m-%d'
222 return fmt
225class Empty(Exception):
226 pass
229class DrumLine(qc.QObject, TalkieConnectionOwner):
230 def __init__(self, iline, tmin, tmax, traces, state):
231 qc.QObject.__init__(self)
232 self.traces = traces
233 self.tmin = tmin
234 self.tmax = tmax
235 self.ymin = 0.
236 self.ymax = 1.
237 self.ymin_data = 0.
238 self.ymax_data = 1.
239 self.iline = iline
240 self._last_mode = None
241 self._ydata_cache = {}
242 self._time_per_pixel = None
243 self.access_counter = 0
244 self.talkie_connect(state, 'style.trace_resolution', self._empty_cache)
246 def data_range(self, mode='min-max'):
247 if not self.traces:
248 raise Empty()
250 modemap = {
251 'min-max': 'minmax',
252 'mean-plusminus-1-sigma': 1.,
253 'mean-plusminus-2-sigma': 2.,
254 'mean-plusminus-4-sigma': 4.,
255 }
257 if self._last_mode != mode:
258 mi, ma = trace.minmax(
259 self.traces, key=lambda tr: None, mode=modemap[mode])[None]
260 self.ymin_data = mi
261 self.ymax_data = ma
262 self._last_mode = mode
264 return self.ymin_data, self.ymax_data
266 def set_yrange(self, ymin, ymax):
267 if ymax == ymin:
268 ymax = 0.5*(ymin + ymax) + 1.0
269 ymin = 0.5*(ymin + ymax) - 1.0
271 self.ymin = ymin
272 self.ymax = ymax
274 @property
275 def tyrange(self):
276 return (self.tmin, self.tmax), (self.ymin, self.ymax)
278 def draw(self, plotenv, markers):
279 self._draw_traces(plotenv)
280 self._draw_time_label(plotenv)
281 self._draw_markers(plotenv, markers)
283 def _draw_time_label(self, plotenv):
284 text = util.time_to_str(self.tmin, format=time_fmt_drumline(self.tmin))
285 font = plotenv.style.label_textstyle.qt_font
286 lab = Label(
287 font.pointSize(), plotenv.vmin, text,
288 anchor='ML', style=plotenv.style.label_textstyle)
290 lab.draw(plotenv)
292 def _draw_markers(self, plotenv, markers):
293 project = plotenv.projector(((self.tmin, self.tmax), (-1., 1.)))
294 plotenv.setPen(plotenv.style.marker_color.qt_color)
296 rect = qc.QRectF(
297 plotenv.umin, 0.,
298 plotenv.umax-plotenv.umin, plotenv.widget.height())
300 for marker in markers:
301 plotenv.draw_vline(project, marker.tmin, -1., 1.)
302 s = marker.get_label()
303 if s:
304 if isinstance(marker, PhaseMarker):
305 anchor = 'BC'
306 v = project.v(-1.)
307 else:
308 anchor = 'BR'
309 v = project.v(-1.)
311 lab = Label(
312 project.u(marker.tmin), v, s,
313 anchor=anchor,
314 style=plotenv.style.marker_textstyle,
315 keep_inside=rect,
316 head=(1.0, 3.0))
318 lab.draw(plotenv)
320 def _draw_traces(self, plotenv):
321 project = plotenv.projector(self.tyrange)
322 tpp = (project.xmax - project.xmin) / (project.umax - project.umin)
323 if self._time_per_pixel != tpp:
324 self._empty_cache()
325 self._time_per_pixel = tpp
327 for tr in self.traces:
328 udata, vdata = self._projected_trace_data(
329 tr,
330 project,
331 plotenv.style.trace_resolution)
333 qpoints = make_QPolygonF(udata, vdata)
334 plotenv.setPen(plotenv.style.trace_color.qt_color)
335 plotenv.drawPolyline(qpoints)
337 def _projected_trace_data(self, tr, project, trace_resolution):
338 n = tr.data_len()
339 if trace_resolution > 0 \
340 and n > 2 \
341 and tr.deltat < 0.5 / trace_resolution*self._time_per_pixel:
343 spp = int(self._time_per_pixel / tr.deltat / trace_resolution)
344 if tr not in self._ydata_cache:
345 nok = (tr.data_len() // spp) * spp
346 ydata_rs = tr.ydata[:nok].reshape((-1, spp))
347 ydata = num.empty((nok // spp)*2)
348 ydata[::2] = num.min(ydata_rs, axis=1)
349 ydata[1::2] = num.max(ydata_rs, axis=1)
350 self._ydata_cache[tr] = ydata
351 else:
352 ydata = self._ydata_cache[tr]
354 udata_min = float(
355 project.u(tr.tmin))
356 udata_max = float(
357 project.u(tr.tmin+0.5*tr.deltat*spp*(ydata.size-1)))
358 else:
359 ydata = tr.ydata
360 udata_min = float(project.u(tr.tmin))
361 udata_max = float(project.u(tr.tmin+tr.deltat*(n-1)))
363 vdata = project.v(ydata)
364 udata = num.linspace(udata_min, udata_max, vdata.size)
365 return udata, vdata
367 def _empty_cache(self):
368 self._ydata_cache = {}
371def tlen(x):
372 return x.tmax-x.tmin
375def is_relevant(x, tmin, tmax):
376 return tmax >= x.tmin and x.tmax >= tmin
379class MarkerStore(object):
380 def __init__(self):
381 self.empty()
383 def empty(self):
384 self._by_tmin = pile.Sorted([], 'tmin')
385 self._by_tmax = pile.Sorted([], 'tmax')
386 self._by_tlen = pile.Sorted([], tlen)
387 self._adjust_minmax()
388 self._listeners = []
390 def relevant(self, tmin, tmax, selector=None):
391 if not self._by_tmin or not is_relevant(self, tmin, tmax):
392 return []
394 if selector is None:
395 return [
396 x for x in self._by_tmin.with_key_in(tmin-self.tlenmax, tmax)
397 if is_relevant(x, tmin, tmax)]
398 else:
399 return [
400 x for x in self._by_tmin.with_key_in(tmin-self.tlenmax, tmax)
401 if is_relevant(x, tmin, tmax) and selector(x)]
403 def insert(self, x):
404 self._by_tmin.insert(x)
405 self._by_tmax.insert(x)
406 self._by_tlen.insert(x)
408 self._adjust_minmax()
409 self._notify_listeners('insert', x)
411 def remove(self, x):
412 self._by_tmin.remove(x)
413 self._by_tmax.remove(x)
414 self._by_tlen.remove(x)
416 self._adjust_minmax()
417 self._notify_listeners('remove', x)
419 def insert_many(self, x):
420 self._by_tmin.insert_many(x)
421 self._by_tmax.insert_many(x)
422 self._by_tlen.insert_many(x)
424 self._adjust_minmax()
425 self._notify_listeners('insert_many', x)
427 def remove_many(self, x):
428 self._by_tmin.remove_many(x)
429 self._by_tmax.remove_many(x)
430 self._by_tlen.remove_many(x)
432 self._adjust_minmax()
433 self._notify_listeners('remove_many', x)
435 def add_listener(self, obj):
436 self._listeners.append(util.smart_weakref(obj))
438 def _adjust_minmax(self):
439 if self._by_tmin:
440 self.tmin = self._by_tmin.min().tmin
441 self.tmax = self._by_tmax.max().tmax
442 self.tlenmax = tlen(self._by_tlen.max())
443 else:
444 self.tmin = None
445 self.tmax = None
446 self.tlenmax = None
448 def _notify_listeners(self, what, x):
449 for ref in self._listeners:
450 obj = ref()
451 if obj:
452 obj(what, x)
454 def __iter__(self):
455 return iter(self._by_tmin)
458class DrumViewMain(qw.QWidget, TalkieConnectionOwner):
460 def __init__(self, pile, *args):
461 qw.QWidget.__init__(self, *args)
463 self.setAttribute(qc.Qt.WA_AcceptTouchEvents, True)
465 st = self.state = State()
466 self.markers = MarkerStore()
467 self.markers.add_listener(self._markers_changed)
469 self.pile = pile
470 self.pile.add_listener(self._pile_changed)
472 self._drumlines = {}
473 self._wheel_pos = 0
474 self._iline_float = None
475 self._project_iline_to_screen = ProjectYV(
476 (st.iline-0.5, st.iline+st.nlines+0.5), (0., self.height()))
477 self._access_counter = 0
478 self._waiting_for_first_data = True
480 self._init_touch()
481 self._init_following()
483 self.talkie_connect(
484 self.state,
485 '',
486 self._state_changed,
487 drop_args=True)
489 self.talkie_connect(
490 self.state,
491 ['filters', 'nslc', 'tline'],
492 self._drop_cached_drumlines,
493 drop_args=True)
495 self.talkie_connect(
496 self.state,
497 'style.background_color',
498 self._adjust_background_color,
499 drop_args=True)
501 self.talkie_connect(
502 self.state,
503 'follow',
504 self._adjust_follow,
505 drop_args=True)
507 self._adjust_background_color()
508 self._adjust_follow()
510 def goto_data_begin(self):
511 if self.pile.tmin:
512 self.state.tmin = self.pile.tmin
514 def goto_data_end(self):
515 if self.pile.tmax:
516 self.state.tmax = self.pile.tmax
518 def next_nslc(self):
519 nslc_ids = sorted(self.pile.nslc_ids.keys())
520 if nslc_ids:
521 try:
522 i = nslc_ids.index(self.state.nslc)
523 except ValueError:
524 i = -1
526 self.state.nslc = nslc_ids[(i+1) % len(nslc_ids)]
528 def title(self):
529 return ' '.join(x for x in self.state.nslc if x)
531 def _state_changed(self):
532 self.update()
534 def _markers_changed(self, *_):
535 self.update()
537 def _pile_changed(self, what, content):
538 self._adjust_first_data()
540 delete = []
541 tline = self.state.tline
542 for iline in self._drumlines.keys():
543 for c in content:
544 if c.overlaps(iline*tline, (iline+1)*tline):
545 delete.append(iline)
547 for iline in delete:
548 del self._drumlines[iline]
550 self.update()
552 def _init_following(self):
553 self._follow_timer = None
554 self._following_interrupted = False
555 self._following_interrupted_tstart = None
557 def interrupt_following(self):
558 self._following_interrupted = True
559 self._following_interrupted_tstart = time.time()
561 def _follow_update(self):
562 if self._following_interrupted:
563 now = time.time()
564 if self._following_interrupted_tstart < now - 20.:
565 self._following_interrupted = False
566 else:
567 return
569 now = time.time()
570 iline = int(math.ceil(now / self.state.tline))-self.state.nlines
571 if iline != self.state.iline:
572 self.state.iline = iline
574 def _adjust_follow(self):
575 follow = self.state.follow
576 if follow and not self._follow_timer:
577 self._follow_timer = qc.QTimer(self)
578 self._follow_timer.timeout.connect(self._follow_update)
579 self._follow_timer.setInterval(1000)
580 self._follow_update()
581 self._follow_timer.start()
583 elif not follow and self._follow_timer:
584 self._follow_timer.stop()
586 def _draw(self, plotenv):
587 self._adjust_first_data()
588 plotenv.umin = 0.
589 plotenv.umax = self.width()
590 self._draw_title(plotenv)
591 self._draw_time_axis(plotenv)
592 self._draw_lines(plotenv)
594 def _draw_title(self, plotenv):
595 font = plotenv.style.title_textstyle.qt_font
596 lab = Label(
597 0.5*(plotenv.umin + plotenv.umax),
598 font.pointSize(),
599 self.title(),
600 anchor='TC',
601 style=plotenv.style.title_textstyle)
603 lab.draw(plotenv)
605 def _draw_time_axis(self, plotenv):
606 pass
608 def _draw_lines(self, plotenv):
609 st = self.state
610 drumlines_seen = []
611 for iline in range(st.iline, st.iline+st.nlines):
612 self._update_line(iline)
613 drumline = self._drumlines.get(iline, None)
614 if drumline:
615 drumlines_seen.append(drumline)
617 self._autoscale(drumlines_seen)
619 top_margin = 50.
620 bottom_margin = 50.
622 self._project_iline_to_screen = ProjectYV(
623 (st.iline-0.5, st.iline+st.nlines-0.5),
624 (top_margin, self.height()-bottom_margin))
626 for drumline in drumlines_seen:
627 plotenv.vmin = self._project_iline_to_screen.v(drumline.iline-0.5)
628 plotenv.vmax = self._project_iline_to_screen.v(drumline.iline+0.5)
629 markers = self._relevant_markers(drumline.tmin, drumline.tmax)
630 drumline.draw(plotenv, markers)
631 drumline.access_counter = self._access_counter
632 self._access_counter += 1
634 drumlines_by_access = sorted(
635 self._drumlines.values(), key=lambda dl: dl.access_counter)
637 for drumline in drumlines_by_access[:-st.npages_cache*st.nlines]:
638 del self._drumlines[drumline.iline]
640 def _relevant_markers(self, tmin, tmax):
641 return self.markers.relevant(
642 tmin, tmax,
643 lambda m: not m.nslc_ids or m.match_nslc(self.state.nslc))
645 def _autoscale(self, drumlines):
646 if not drumlines:
647 return
649 st = self.state
651 data = []
652 for drumline in drumlines:
653 try:
654 data.append(drumline.data_range(st.scaling.base))
655 except Empty:
656 pass
658 if not data:
659 data = [[0, 0]]
661 mi, ma = num.array(data, dtype=float).T
662 gain = st.scaling.gain
663 if st.scaling.mode == 'same':
664 ymin, ymax = mi.min(), ma.max()
665 for drumline in drumlines:
666 drumline.set_yrange(ymin/gain, ymax/gain)
667 elif st.scaling.mode == 'individual':
668 for drumline, ymin, ymax in zip(drumlines, mi, ma):
669 drumline.set_yrange(ymin/gain, ymax/gain)
670 elif st.scaling.mode == 'fixed':
671 for drumline in drumlines:
672 drumline.set_yrange(st.scaling.min/gain, st.scaling.max/gain)
674 def _update_line(self, iline):
676 if iline not in self._drumlines:
677 st = self.state
678 tmin = iline*st.tline
679 tmax = (iline+1)*st.tline
680 if st.filters:
681 tpad = max(x.tpad() for x in st.filters)
682 else:
683 tpad = 0.0
685 traces = self.pile.all(
686 tmin=iline*st.tline,
687 tmax=(iline+1)*st.tline,
688 tpad=tpad,
689 trace_selector=lambda tr: tr.nslc_id == st.nslc,
690 keep_current_files_open=True,
691 accessor_id=id(self))
693 for tr in traces:
694 for filter in st.filters:
695 filter.apply(tr)
697 self._drumlines[iline] = DrumLine(
698 iline, tmin, tmax, traces, self.state)
700 def _drop_cached_drumlines(self):
701 self._drumlines = {}
703 def _adjust_background_color(self):
704 color = self.state.style.background_color.qt_color
706 p = qg.QPalette()
707 p.setColor(qg.QPalette.Background, color)
708 self.setAutoFillBackground(True)
709 self.setPalette(p)
711 def _adjust_first_data(self):
712 if self._waiting_for_first_data:
713 if self.pile.tmin:
714 self.next_nslc()
715 self.goto_data_end()
716 self._waiting_for_first_data = False
718 # qt event handlers
720 def paintEvent(self, event):
721 plotenv = PlotEnv(self)
722 plotenv.style = self.state.style
723 plotenv.widget = self
725 if plotenv.style.antialiasing:
726 plotenv.setRenderHint(qg.QPainter.Antialiasing)
728 self._draw(plotenv)
730 def event(self, event):
732 if event.type() in (
733 qc.QEvent.TouchBegin,
734 qc.QEvent.TouchUpdate,
735 qc.QEvent.TouchEnd,
736 qc.QEvent.TouchCancel):
738 return self._touch_event(event)
740 return qw.QWidget.event(self, event)
742 def _init_touch(self):
743 self._gesture = None
745 def _touch_event(self, event):
746 if event.type() == qc.QEvent.TouchBegin:
747 self._gesture = DrumGesture(self)
748 self._gesture.update(event)
750 elif event.type() == qc.QEvent.TouchUpdate:
751 self._gesture.update(event)
753 elif event.type() == qc.QEvent.TouchEnd:
754 self._gesture.update(event)
755 self._gesture.end()
756 self._gesture = None
758 elif event.type() == qc.QEvent.TouchCancel:
759 self._gesture.update(event)
760 self._gesture.cancel()
761 self._gesture = None
763 return True
765 def wheelEvent(self, event):
767 self.interrupt_following()
769 self._wheel_pos += event.angleDelta().y()
771 n = self._wheel_pos // 120
772 self._wheel_pos = self._wheel_pos % 120
773 if n == 0:
774 return
776 amount = max(1., self.state.nlines/24.)
777 wdelta = amount * n
779 if event.modifiers() & qc.Qt.ControlModifier:
780 proj = self._project_iline_to_screen
782 anchor = (
783 proj.y(event.y()) - proj.ymin) / (proj.ymax - proj.ymin)
785 nlines = max(1, self.state.nlines + int(round(wdelta)))
787 if self._iline_float is None:
788 iline_float = float(self.state.iline)
789 else:
790 iline_float = self._iline_float
792 self._iline_float = iline_float-anchor*wdelta
794 self.state.iline = int(round(iline_float))
795 self.state.nlines = nlines
797 else:
798 self.state.iline -= int(wdelta)
799 self._iline_float = None
801 def keyPressEvent(self, event):
803 keytext = str(event.text())
805 if event.key() == qc.Qt.Key_PageDown:
806 self.interrupt_following()
807 self.state.iline += self.state.nlines
809 elif event.key() == qc.Qt.Key_PageUp:
810 self.interrupt_following()
811 self.state.iline -= self.state.nlines
813 elif keytext == '+':
814 self.state.scaling.gain *= 1.5
816 elif keytext == '-':
817 self.state.scaling.gain *= 1.0 / 1.5
819 elif keytext == ' ':
820 self.next_nslc()
822 elif keytext == 'p':
823 print(self.state)
826tline_choices = [10., 30., 60., 120., 300., 600., 1200., 3600.]
829class DrumGesture(object):
831 def __init__(self, view):
832 self._view = view
833 self._active = False
834 self.begin()
836 def begin(self):
837 self._active = True
838 self._iline = self._view.state.iline
839 self._nlines = self._view.state.nlines
840 self._tline = self._view.state.tline
841 self._proj = self._view._project_iline_to_screen
842 self._gain = self._view.state.scaling.gain
843 self._max_len_tps = 0
845 def end(self):
846 self._active = False
848 def cancel(self):
849 self.end()
851 def update(self, event):
852 if not self._active:
853 return
855 tps = event.touchPoints()
856 proj = self._proj
858 self._max_len_tps = max(len(tps), self._max_len_tps)
860 if len(tps) == 1 and self._max_len_tps < 2:
861 tp = tps[0]
862 iline = proj.y(tp.pos().y())
863 iline_start = proj.y(tp.startPos().y())
864 idelta = int(round(iline - iline_start))
865 iline = self._iline - idelta
866 if iline != self._view.state.iline:
867 self._view.interrupt_following()
868 self._view.state.iline = iline
870 if len(tps) == 2:
871 # self._view.state.iline = self._iline
873 u = [
874 (tp.pos().x(), tp.startPos().x())
875 for tp in tps]
877 y = [
878 (proj.y(tp.pos().y()), proj.y(tp.startPos().y()))
879 for tp in tps]
881 v = [
882 (tp.pos().y(), tp.startPos().y())
883 for tp in tps]
885 if abs(u[0][1] - u[1][1]) < abs(v[0][1] - v[1][1]):
887 vclip = self._view.size().height() * 0.05
888 d0 = max(vclip, abs(v[0][1] - v[1][1]))
889 d1 = max(vclip, abs(v[0][0] - v[1][0]))
891 nlines = min(max(1, int(round(self._nlines * (d0 / d1)))), 50)
893 idelta = int(round(
894 0.5 * ((y[0][0]+y[1][0]) - (y[0][1]+y[1][1]))
895 * (nlines / self._nlines)))
897 iline = self._iline - idelta
899 self._view.interrupt_following
900 if self._view.state.nlines != nlines \
901 or self._view.state.iline != iline:
902 self._view.interrupt_following()
903 self._view.state.iline = iline
904 self._view.state.nlines = nlines
906 else:
907 if abs(u[0][0] - u[0][1]) + abs(u[1][0] - u[1][1]) \
908 > abs(v[0][0] - v[0][1]) + abs(v[1][0] - v[1][1]):
910 ustretch = abs(u[0][1] - u[1][1]) \
911 / (abs(u[0][0] - u[1][0])
912 + self._view.size().width() * 0.05)
914 print('ustretch', ustretch)
915 log_tline_choices = num.log(tline_choices)
916 log_tline = num.log(ustretch * self._tline)
918 ichoice = num.argmin(
919 num.abs(log_tline_choices - log_tline))
920 tline = tline_choices[ichoice]
921 # yanchor = 0.5 * (y[0][1] + y[1][1])
922 # r = (yanchor - proj.ymin) / (proj.ymax - proj.ymin)
924 if self._view.state.tline != tline:
925 self._view.state.iline = int(
926 round(self._iline / (tline / self._tline)))
927 self._view.state.tline = tline
929 else:
930 vstretch = 10**(
931 -((v[0][0] - v[0][1]) + (v[1][0] - v[1][1]))
932 / self._view.size().height())
934 self._view.state.scaling.gain = self._gain * vstretch