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 weakref
11import numpy as num
13from pyrocko.gui.qt_compat import qc, qg, qw
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
21def lim(a, x, b):
22 return min(max(a, x), b)
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):
33 if style is None:
34 style = TextStyle()
36 text = qg.QTextDocument()
37 font = style.qt_font
38 if font:
39 text.setDefaultFont(font)
41 color = style.color.qt_color
43 text.setDefaultStyleSheet('span { color: %s; }' % color.name())
44 text.setHtml('<span>%s</span>' % label_str)
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.)
56 def draw(self, p):
57 s = self.text.size()
58 rect = qc.QRectF(0., 0., s.width(), s.height())
60 tx, ty = x, y = self.position
61 anchor = self.anchor
63 pxs = self.text.defaultFont().pointSize()
65 oy = self.head[0] * pxs
66 ox = self.head[1]/2. * pxs
68 if 'L' in anchor:
69 tx -= min(ox*2, rect.width()/2.)
71 elif 'R' in anchor:
72 tx -= rect.width() - min(ox*2., rect.width()/2.)
74 elif 'C' in anchor:
75 tx -= rect.width()/2.
77 if 'B' in anchor:
78 ty -= rect.height() + oy
80 elif 'T' in anchor:
81 ty += oy
83 elif 'M' in anchor:
84 ty -= rect.height()/2.
86 rect.translate(tx, ty)
88 if self.keep_inside:
89 keep_inside = self.keep_inside
90 if rect.top() < keep_inside.top():
91 rect.moveTop(keep_inside.top())
93 if rect.bottom() > keep_inside.bottom():
94 rect.moveBottom(keep_inside.bottom())
96 if rect.left() < keep_inside.left():
97 rect.moveLeft(keep_inside.left())
99 if rect.right() > keep_inside.right():
100 rect.moveRight(keep_inside.right())
102 poly = None
103 if self.head[0] != 0.:
104 l, r, t, b = rect.left(), rect.right(), rect.top(), rect.bottom()
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'
113 c1, c2 = lim(l, x-ox, r), lim(l, x+ox, r)
115 px = (l, c1, x, c2, r, r, l)
116 py = (a, a, y, a, a, b, b)
118 poly = make_QPolygonF(px, py)
120 tx = rect.left()
121 ty = rect.top()
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))
129 p.setBrush(self.style.background_color.qt_color)
130 if poly:
131 p.drawPolygon(poly)
132 else:
133 p.drawRect(rect)
135 if self.style.background_color:
136 p.fillRect(rect, self.style.background_color.qt_color)
138 p.setPen(oldpen)
139 p.setBrush(oldbrush)
141 p.translate(tx, ty)
142 self.text.drawContents(p)
143 p.translate(-tx, -ty)
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)
152 def u(self, x):
153 return self.umin + (x-self.xmin) * self.cxu
155 def x(self, u):
156 return self.xmin + (u-self.umin) / self.cxu
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)
165 def v(self, y):
166 return self.vmin + (y-self.ymin) * self.cyv
168 def y(self, v):
169 return self.ymin + (v-self.vmin) / self.cyv
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)
179 def u(self, x):
180 return self.umin + (x-self.xmin) * self.cxu
182 def v(self, y):
183 return self.vmin + (y-self.ymin) * self.cyv
185 def x(self, u):
186 return self.xmin + (u-self.umin) / self.cxu
188 def y(self, v):
189 return self.ymin + (v-self.vmin) / self.cyv
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.
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)
205 def projector(self, xyr):
206 return ProjectXYUV(xyr, self.uvrange)
208 @property
209 def uvrange(self):
210 return (self.umin, self.umax), (self.vmin, self.vmax)
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'
220 if ti % (3600*24) == 0:
221 fmt = fmt + ' %Y-%m-%d'
223 return fmt
226class Empty(Exception):
227 pass
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
246 state.add_listener(
247 self.listener_no_args(self._empty_cache),
248 path='style.trace_resolution')
250 def data_range(self, mode='min-max'):
251 if not self.traces:
252 raise Empty()
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 }
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
268 return self.ymin_data, self.ymax_data
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
275 self.ymin = ymin
276 self.ymax = ymax
278 @property
279 def tyrange(self):
280 return (self.tmin, self.tmax), (self.ymin, self.ymax)
282 def draw(self, plotenv, markers):
283 self._draw_traces(plotenv)
284 self._draw_time_label(plotenv)
285 self._draw_markers(plotenv, markers)
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)
294 lab.draw(plotenv)
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)
300 rect = qc.QRectF(
301 plotenv.umin, 0.,
302 plotenv.umax-plotenv.umin, plotenv.widget.height())
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.)
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))
322 lab.draw(plotenv)
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
331 for tr in self.traces:
332 udata, vdata = self._projected_trace_data(
333 tr,
334 project,
335 plotenv.style.trace_resolution)
337 qpoints = make_QPolygonF(udata, vdata)
338 plotenv.setPen(plotenv.style.trace_color.qt_color)
339 plotenv.drawPolyline(qpoints)
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:
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]
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)))
367 vdata = project.v(ydata)
368 udata = num.linspace(udata_min, udata_max, vdata.size)
369 return udata, vdata
371 def _empty_cache(self):
372 self._ydata_cache = {}
375def tlen(x):
376 return x.tmax-x.tmin
379def is_relevant(x, tmin, tmax):
380 return tmax >= x.tmin and x.tmax >= tmin
383class MarkerStore(object):
384 def __init__(self):
385 self.empty()
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 = []
394 def relevant(self, tmin, tmax, selector=None):
395 if not self._by_tmin or not is_relevant(self, tmin, tmax):
396 return []
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)]
407 def insert(self, x):
408 self._by_tmin.insert(x)
409 self._by_tmax.insert(x)
410 self._by_tlen.insert(x)
412 self._adjust_minmax()
413 self._notify_listeners('insert', x)
415 def remove(self, x):
416 self._by_tmin.remove(x)
417 self._by_tmax.remove(x)
418 self._by_tlen.remove(x)
420 self._adjust_minmax()
421 self._notify_listeners('remove', x)
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)
428 self._adjust_minmax()
429 self._notify_listeners('insert_many', x)
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)
436 self._adjust_minmax()
437 self._notify_listeners('remove_many', x)
439 def add_listener(self, obj):
440 self._listeners.append(weakref.ref(obj))
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
452 def _notify_listeners(self, what, x):
453 for ref in self._listeners:
454 obj = ref()
455 if obj:
456 obj(what, x)
458 def __iter__(self):
459 return iter(self._by_tmin)
462class DrumViewMain(qw.QWidget, Listener):
464 def __init__(self, pile, *args):
465 qw.QWidget.__init__(self, *args)
467 self.setAttribute(qc.Qt.WA_AcceptTouchEvents, True)
469 st = self.state = State()
470 self.markers = MarkerStore()
471 self.markers.add_listener(self.listener_no_args(self._markers_changed))
473 self.pile = pile
474 self.pile.add_listener(self.listener(self._pile_changed))
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
484 self._init_touch()
485 self._init_following()
487 sal = self.state.add_listener
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')
497 self._adjust_background_color()
498 self._adjust_follow()
500 def goto_data_begin(self):
501 if self.pile.tmin:
502 self.state.tmin = self.pile.tmin
504 def goto_data_end(self):
505 if self.pile.tmax:
506 self.state.tmax = self.pile.tmax
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
516 self.state.nslc = nslc_ids[(i+1) % len(nslc_ids)]
518 def title(self):
519 return ' '.join(x for x in self.state.nslc if x)
521 def _state_changed(self):
522 self.update()
524 def _markers_changed(self):
525 self.update()
527 def _pile_changed(self, what, content):
528 self._adjust_first_data()
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)
537 for iline in delete:
538 del self._drumlines[iline]
540 self.update()
542 def _init_following(self):
543 self._follow_timer = None
544 self._following_interrupted = False
545 self._following_interrupted_tstart = None
547 def interrupt_following(self):
548 self._following_interrupted = True
549 self._following_interrupted_tstart = time.time()
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
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
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()
573 elif not follow and self._follow_timer:
574 self._follow_timer.stop()
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)
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)
593 lab.draw(plotenv)
595 def _draw_time_axis(self, plotenv):
596 pass
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)
607 self._autoscale(drumlines_seen)
609 top_margin = 50.
610 bottom_margin = 50.
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))
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
624 drumlines_by_access = sorted(
625 self._drumlines.values(), key=lambda dl: dl.access_counter)
627 for drumline in drumlines_by_access[:-st.npages_cache*st.nlines]:
628 del self._drumlines[drumline.iline]
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))
635 def _autoscale(self, drumlines):
636 if not drumlines:
637 return
639 st = self.state
641 data = []
642 for drumline in drumlines:
643 try:
644 data.append(drumline.data_range(st.scaling.base))
645 except Empty:
646 pass
648 if not data:
649 data = [[0, 0]]
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)
664 def _update_line(self, iline):
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
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))
683 for tr in traces:
684 for filter in st.filters:
685 filter.apply(tr)
687 self._drumlines[iline] = DrumLine(
688 iline, tmin, tmax, traces, self.state)
690 def _drop_cached_drumlines(self):
691 self._drumlines = {}
693 def _adjust_background_color(self):
694 color = self.state.style.background_color.qt_color
696 p = qg.QPalette()
697 p.setColor(qg.QPalette.Background, color)
698 self.setAutoFillBackground(True)
699 self.setPalette(p)
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
708 # qt event handlers
710 def paintEvent(self, event):
711 plotenv = PlotEnv(self)
712 plotenv.style = self.state.style
713 plotenv.widget = self
715 if plotenv.style.antialiasing:
716 plotenv.setRenderHint(qg.QPainter.Antialiasing)
718 self._draw(plotenv)
720 def event(self, event):
722 if event.type() in (
723 qc.QEvent.TouchBegin,
724 qc.QEvent.TouchUpdate,
725 qc.QEvent.TouchEnd,
726 qc.QEvent.TouchCancel):
728 return self._touch_event(event)
730 return qw.QWidget.event(self, event)
732 def _init_touch(self):
733 self._gesture = None
735 def _touch_event(self, event):
736 if event.type() == qc.QEvent.TouchBegin:
737 self._gesture = DrumGesture(self)
738 self._gesture.update(event)
740 elif event.type() == qc.QEvent.TouchUpdate:
741 self._gesture.update(event)
743 elif event.type() == qc.QEvent.TouchEnd:
744 self._gesture.update(event)
745 self._gesture.end()
746 self._gesture = None
748 elif event.type() == qc.QEvent.TouchCancel:
749 self._gesture.update(event)
750 self._gesture.cancel()
751 self._gesture = None
753 return True
755 def wheelEvent(self, event):
757 self.interrupt_following()
759 self._wheel_pos += event.angleDelta().y()
761 n = self._wheel_pos // 120
762 self._wheel_pos = self._wheel_pos % 120
763 if n == 0:
764 return
766 amount = max(1., self.state.nlines/24.)
767 wdelta = amount * n
769 if event.modifiers() & qc.Qt.ControlModifier:
770 proj = self._project_iline_to_screen
772 anchor = (
773 proj.y(event.y()) - proj.ymin) / (proj.ymax - proj.ymin)
775 nlines = max(1, self.state.nlines + int(round(wdelta)))
777 if self._iline_float is None:
778 iline_float = float(self.state.iline)
779 else:
780 iline_float = self._iline_float
782 self._iline_float = iline_float-anchor*wdelta
784 self.state.iline = int(round(iline_float))
785 self.state.nlines = nlines
787 else:
788 self.state.iline -= int(wdelta)
789 self._iline_float = None
791 def keyPressEvent(self, event):
793 keytext = str(event.text())
795 if event.key() == qc.Qt.Key_PageDown:
796 self.interrupt_following()
797 self.state.iline += self.state.nlines
799 elif event.key() == qc.Qt.Key_PageUp:
800 self.interrupt_following()
801 self.state.iline -= self.state.nlines
803 elif keytext == '+':
804 self.state.scaling.gain *= 1.5
806 elif keytext == '-':
807 self.state.scaling.gain *= 1.0 / 1.5
809 elif keytext == ' ':
810 self.next_nslc()
812 elif keytext == 'p':
813 print(self.state)
816tline_choices = [10., 30., 60., 120., 300., 600., 1200., 3600.]
819class DrumGesture(object):
821 def __init__(self, view):
822 self._view = view
823 self._active = False
824 self.begin()
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
835 def end(self):
836 self._active = False
838 def cancel(self):
839 self.end()
841 def update(self, event):
842 if not self._active:
843 return
845 tps = event.touchPoints()
846 proj = self._proj
848 self._max_len_tps = max(len(tps), self._max_len_tps)
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
860 if len(tps) == 2:
861 # self._view.state.iline = self._iline
863 u = [
864 (tp.pos().x(), tp.startPos().x())
865 for tp in tps]
867 y = [
868 (proj.y(tp.pos().y()), proj.y(tp.startPos().y()))
869 for tp in tps]
871 v = [
872 (tp.pos().y(), tp.startPos().y())
873 for tp in tps]
875 if abs(u[0][1] - u[1][1]) < abs(v[0][1] - v[1][1]):
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]))
881 nlines = min(max(1, int(round(self._nlines * (d0 / d1)))), 50)
883 idelta = int(round(
884 0.5 * ((y[0][0]+y[1][0]) - (y[0][1]+y[1][1]))
885 * (nlines / self._nlines)))
887 iline = self._iline - idelta
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
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]):
900 ustretch = abs(u[0][1] - u[1][1]) \
901 / (abs(u[0][0] - u[1][0])
902 + self._view.size().width() * 0.05)
904 print('ustretch', ustretch)
905 log_tline_choices = num.log(tline_choices)
906 log_tline = num.log(ustretch * self._tline)
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)
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
919 else:
920 vstretch = 10**(
921 -((v[0][0] - v[0][1]) + (v[1][0] - v[1][1]))
922 / self._view.size().height())
924 self._view.state.scaling.gain = self._gain * vstretch