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
« 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----------
6import math
7import time
8import numpy as num
10from pyrocko.gui.qt_compat import qc, qg, qw
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
18def lim(a, x, b):
19 return min(max(a, x), b)
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):
30 if style is None:
31 style = TextStyle()
33 text = qg.QTextDocument()
34 font = style.qt_font
35 if font:
36 text.setDefaultFont(font)
38 color = style.color.qt_color
40 text.setDefaultStyleSheet('span { color: %s; }' % color.name())
41 text.setHtml('<span>%s</span>' % label_str)
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.)
53 def draw(self, p):
54 s = self.text.size()
55 rect = qc.QRectF(0., 0., s.width(), s.height())
57 tx, ty = x, y = self.position
58 anchor = self.anchor
60 pxs = self.text.defaultFont().pointSize()
62 oy = self.head[0] * pxs
63 ox = self.head[1]/2. * pxs
65 if 'L' in anchor:
66 tx -= min(ox*2, rect.width()/2.)
68 elif 'R' in anchor:
69 tx -= rect.width() - min(ox*2., rect.width()/2.)
71 elif 'C' in anchor:
72 tx -= rect.width()/2.
74 if 'B' in anchor:
75 ty -= rect.height() + oy
77 elif 'T' in anchor:
78 ty += oy
80 elif 'M' in anchor:
81 ty -= rect.height()/2.
83 rect.translate(tx, ty)
85 if self.keep_inside:
86 keep_inside = self.keep_inside
87 if rect.top() < keep_inside.top():
88 rect.moveTop(keep_inside.top())
90 if rect.bottom() > keep_inside.bottom():
91 rect.moveBottom(keep_inside.bottom())
93 if rect.left() < keep_inside.left():
94 rect.moveLeft(keep_inside.left())
96 if rect.right() > keep_inside.right():
97 rect.moveRight(keep_inside.right())
99 poly = None
100 if self.head[0] != 0.:
101 l, r, t, b = rect.left(), rect.right(), rect.top(), rect.bottom()
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'
110 c1, c2 = lim(l, x-ox, r), lim(l, x+ox, r)
112 px = (l, c1, x, c2, r, r, l)
113 py = (a, a, y, a, a, b, b)
115 poly = make_QPolygonF(px, py)
117 tx = rect.left()
118 ty = rect.top()
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))
126 p.setBrush(self.style.background_color.qt_color)
127 if poly:
128 p.drawPolygon(poly)
129 else:
130 p.drawRect(rect)
132 if self.style.background_color:
133 p.fillRect(rect, self.style.background_color.qt_color)
135 p.setPen(oldpen)
136 p.setBrush(oldbrush)
138 p.translate(tx, ty)
139 self.text.drawContents(p)
140 p.translate(-tx, -ty)
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)
149 def u(self, x):
150 return self.umin + (x-self.xmin) * self.cxu
152 def x(self, u):
153 return self.xmin + (u-self.umin) / self.cxu
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)
162 def v(self, y):
163 return self.vmin + (y-self.ymin) * self.cyv
165 def y(self, v):
166 return self.ymin + (v-self.vmin) / self.cyv
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)
176 def u(self, x):
177 return self.umin + (x-self.xmin) * self.cxu
179 def v(self, y):
180 return self.vmin + (y-self.ymin) * self.cyv
182 def x(self, u):
183 return self.xmin + (u-self.umin) / self.cxu
185 def y(self, v):
186 return self.ymin + (v-self.vmin) / self.cyv
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.
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)
202 def projector(self, xyr):
203 return ProjectXYUV(xyr, self.uvrange)
205 @property
206 def uvrange(self):
207 return (self.umin, self.umax), (self.vmin, self.vmax)
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'
217 if ti % (3600*24) == 0:
218 fmt = fmt + ' %Y-%m-%d'
220 return fmt
223class Empty(Exception):
224 pass
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)
244 def data_range(self, mode='min-max'):
245 if not self.traces:
246 raise Empty()
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 }
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
262 return self.ymin_data, self.ymax_data
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
269 self.ymin = ymin
270 self.ymax = ymax
272 @property
273 def tyrange(self):
274 return (self.tmin, self.tmax), (self.ymin, self.ymax)
276 def draw(self, plotenv, markers):
277 self._draw_traces(plotenv)
278 self._draw_time_label(plotenv)
279 self._draw_markers(plotenv, markers)
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)
288 lab.draw(plotenv)
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)
294 rect = qc.QRectF(
295 plotenv.umin, 0.,
296 plotenv.umax-plotenv.umin, plotenv.widget.height())
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.)
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))
316 lab.draw(plotenv)
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
325 for tr in self.traces:
326 udata, vdata = self._projected_trace_data(
327 tr,
328 project,
329 plotenv.style.trace_resolution)
331 qpoints = make_QPolygonF(udata, vdata)
332 plotenv.setPen(plotenv.style.trace_color.qt_color)
333 plotenv.drawPolyline(qpoints)
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:
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]
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)))
361 vdata = project.v(ydata)
362 udata = num.linspace(udata_min, udata_max, vdata.size)
363 return udata, vdata
365 def _empty_cache(self):
366 self._ydata_cache = {}
369def tlen(x):
370 return x.tmax-x.tmin
373def is_relevant(x, tmin, tmax):
374 return tmax >= x.tmin and x.tmax >= tmin
377class MarkerStore(object):
378 def __init__(self):
379 self.empty()
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 = []
388 def relevant(self, tmin, tmax, selector=None):
389 if not self._by_tmin or not is_relevant(self, tmin, tmax):
390 return []
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)]
401 def insert(self, x):
402 self._by_tmin.insert(x)
403 self._by_tmax.insert(x)
404 self._by_tlen.insert(x)
406 self._adjust_minmax()
407 self._notify_listeners('insert', x)
409 def remove(self, x):
410 self._by_tmin.remove(x)
411 self._by_tmax.remove(x)
412 self._by_tlen.remove(x)
414 self._adjust_minmax()
415 self._notify_listeners('remove', x)
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)
422 self._adjust_minmax()
423 self._notify_listeners('insert_many', x)
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)
430 self._adjust_minmax()
431 self._notify_listeners('remove_many', x)
433 def add_listener(self, obj):
434 self._listeners.append(util.smart_weakref(obj))
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
446 def _notify_listeners(self, what, x):
447 for ref in self._listeners:
448 obj = ref()
449 if obj:
450 obj(what, x)
452 def __iter__(self):
453 return iter(self._by_tmin)
456class DrumViewMain(qw.QWidget, TalkieConnectionOwner):
458 def __init__(self, pile, *args):
459 qw.QWidget.__init__(self, *args)
461 self.setAttribute(qc.Qt.WA_AcceptTouchEvents, True)
463 st = self.state = State()
464 self.markers = MarkerStore()
465 self.markers.add_listener(self._markers_changed)
467 self.pile = pile
468 self.pile.add_listener(self._pile_changed)
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
478 self._init_touch()
479 self._init_following()
481 self.talkie_connect(
482 self.state,
483 '',
484 self._state_changed,
485 drop_args=True)
487 self.talkie_connect(
488 self.state,
489 ['filters', 'nslc', 'tline'],
490 self._drop_cached_drumlines,
491 drop_args=True)
493 self.talkie_connect(
494 self.state,
495 'style.background_color',
496 self._adjust_background_color,
497 drop_args=True)
499 self.talkie_connect(
500 self.state,
501 'follow',
502 self._adjust_follow,
503 drop_args=True)
505 self._adjust_background_color()
506 self._adjust_follow()
508 def goto_data_begin(self):
509 if self.pile.tmin:
510 self.state.tmin = self.pile.tmin
512 def goto_data_end(self):
513 if self.pile.tmax:
514 self.state.tmax = self.pile.tmax
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
524 self.state.nslc = nslc_ids[(i+1) % len(nslc_ids)]
526 def title(self):
527 return ' '.join(x for x in self.state.nslc if x)
529 def _state_changed(self):
530 self.update()
532 def _markers_changed(self, *_):
533 self.update()
535 def _pile_changed(self, what, content):
536 self._adjust_first_data()
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)
545 for iline in delete:
546 del self._drumlines[iline]
548 self.update()
550 def _init_following(self):
551 self._follow_timer = None
552 self._following_interrupted = False
553 self._following_interrupted_tstart = None
555 def interrupt_following(self):
556 self._following_interrupted = True
557 self._following_interrupted_tstart = time.time()
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
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
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()
581 elif not follow and self._follow_timer:
582 self._follow_timer.stop()
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)
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)
601 lab.draw(plotenv)
603 def _draw_time_axis(self, plotenv):
604 pass
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)
615 self._autoscale(drumlines_seen)
617 top_margin = 50.
618 bottom_margin = 50.
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))
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
632 drumlines_by_access = sorted(
633 self._drumlines.values(), key=lambda dl: dl.access_counter)
635 for drumline in drumlines_by_access[:-st.npages_cache*st.nlines]:
636 del self._drumlines[drumline.iline]
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))
643 def _autoscale(self, drumlines):
644 if not drumlines:
645 return
647 st = self.state
649 data = []
650 for drumline in drumlines:
651 try:
652 data.append(drumline.data_range(st.scaling.base))
653 except Empty:
654 pass
656 if not data:
657 data = [[0, 0]]
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)
672 def _update_line(self, iline):
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
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))
691 for tr in traces:
692 for filter in st.filters:
693 filter.apply(tr)
695 self._drumlines[iline] = DrumLine(
696 iline, tmin, tmax, traces, self.state)
698 def _drop_cached_drumlines(self):
699 self._drumlines = {}
701 def _adjust_background_color(self):
702 color = self.state.style.background_color.qt_color
704 p = qg.QPalette()
705 p.setColor(qg.QPalette.Background, color)
706 self.setAutoFillBackground(True)
707 self.setPalette(p)
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
716 # qt event handlers
718 def paintEvent(self, event):
719 plotenv = PlotEnv(self)
720 plotenv.style = self.state.style
721 plotenv.widget = self
723 if plotenv.style.antialiasing:
724 plotenv.setRenderHint(qg.QPainter.Antialiasing)
726 self._draw(plotenv)
728 def event(self, event):
730 if event.type() in (
731 qc.QEvent.TouchBegin,
732 qc.QEvent.TouchUpdate,
733 qc.QEvent.TouchEnd,
734 qc.QEvent.TouchCancel):
736 return self._touch_event(event)
738 return qw.QWidget.event(self, event)
740 def _init_touch(self):
741 self._gesture = None
743 def _touch_event(self, event):
744 if event.type() == qc.QEvent.TouchBegin:
745 self._gesture = DrumGesture(self)
746 self._gesture.update(event)
748 elif event.type() == qc.QEvent.TouchUpdate:
749 self._gesture.update(event)
751 elif event.type() == qc.QEvent.TouchEnd:
752 self._gesture.update(event)
753 self._gesture.end()
754 self._gesture = None
756 elif event.type() == qc.QEvent.TouchCancel:
757 self._gesture.update(event)
758 self._gesture.cancel()
759 self._gesture = None
761 return True
763 def wheelEvent(self, event):
765 self.interrupt_following()
767 self._wheel_pos += event.angleDelta().y()
769 n = self._wheel_pos // 120
770 self._wheel_pos = self._wheel_pos % 120
771 if n == 0:
772 return
774 amount = max(1., self.state.nlines/24.)
775 wdelta = amount * n
777 if event.modifiers() & qc.Qt.ControlModifier:
778 proj = self._project_iline_to_screen
780 anchor = (
781 proj.y(event.y()) - proj.ymin) / (proj.ymax - proj.ymin)
783 nlines = max(1, self.state.nlines + int(round(wdelta)))
785 if self._iline_float is None:
786 iline_float = float(self.state.iline)
787 else:
788 iline_float = self._iline_float
790 self._iline_float = iline_float-anchor*wdelta
792 self.state.iline = int(round(iline_float))
793 self.state.nlines = nlines
795 else:
796 self.state.iline -= int(wdelta)
797 self._iline_float = None
799 def keyPressEvent(self, event):
801 keytext = str(event.text())
803 if event.key() == qc.Qt.Key_PageDown:
804 self.interrupt_following()
805 self.state.iline += self.state.nlines
807 elif event.key() == qc.Qt.Key_PageUp:
808 self.interrupt_following()
809 self.state.iline -= self.state.nlines
811 elif keytext == '+':
812 self.state.scaling.gain *= 1.5
814 elif keytext == '-':
815 self.state.scaling.gain *= 1.0 / 1.5
817 elif keytext == ' ':
818 self.next_nslc()
820 elif keytext == 'p':
821 print(self.state)
824tline_choices = [10., 30., 60., 120., 300., 600., 1200., 3600.]
827class DrumGesture(object):
829 def __init__(self, view):
830 self._view = view
831 self._active = False
832 self.begin()
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
843 def end(self):
844 self._active = False
846 def cancel(self):
847 self.end()
849 def update(self, event):
850 if not self._active:
851 return
853 tps = event.touchPoints()
854 proj = self._proj
856 self._max_len_tps = max(len(tps), self._max_len_tps)
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
868 if len(tps) == 2:
869 # self._view.state.iline = self._iline
871 u = [
872 (tp.pos().x(), tp.startPos().x())
873 for tp in tps]
875 y = [
876 (proj.y(tp.pos().y()), proj.y(tp.startPos().y()))
877 for tp in tps]
879 v = [
880 (tp.pos().y(), tp.startPos().y())
881 for tp in tps]
883 if abs(u[0][1] - u[1][1]) < abs(v[0][1] - v[1][1]):
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]))
889 nlines = min(max(1, int(round(self._nlines * (d0 / d1)))), 50)
891 idelta = int(round(
892 0.5 * ((y[0][0]+y[1][0]) - (y[0][1]+y[1][1]))
893 * (nlines / self._nlines)))
895 iline = self._iline - idelta
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
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]):
908 ustretch = abs(u[0][1] - u[1][1]) \
909 / (abs(u[0][0] - u[1][0])
910 + self._view.size().width() * 0.05)
912 print('ustretch', ustretch)
913 log_tline_choices = num.log(tline_choices)
914 log_tline = num.log(ustretch * self._tline)
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)
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
927 else:
928 vstretch = 10**(
929 -((v[0][0] - v[0][1]) + (v[1][0] - v[1][1]))
930 / self._view.size().height())
932 self._view.state.scaling.gain = self._gain * vstretch