Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/util.py: 54%

1240 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-11 11:01 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import sys 

7import math 

8import time 

9import numpy as num 

10import logging 

11import enum 

12import calendar 

13import signal 

14 

15from matplotlib.colors import Normalize 

16 

17from .qt_compat import qc, qg, qw 

18 

19from .snuffler.marker import Marker, PhaseMarker, EventMarker # noqa 

20from .snuffler.marker import MarkerParseError, MarkerOneNSLCRequired # noqa 

21from .snuffler.marker import load_markers, save_markers # noqa 

22from pyrocko import plot, util 

23 

24 

25logger = logging.getLogger('pyrocko.gui.util') 

26 

27 

28class _Getch: 

29 ''' 

30 Gets a single character from standard input. 

31 

32 Does not echo to the screen. 

33 

34 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user 

35 ''' 

36 def __init__(self): 

37 try: 

38 self.impl = _GetchWindows() 

39 except ImportError: 

40 self.impl = _GetchUnix() 

41 

42 def __call__(self): return self.impl() 

43 

44 

45class _GetchUnix: 

46 def __init__(self): 

47 import tty, sys # noqa 

48 

49 def __call__(self): 

50 import sys 

51 import tty 

52 import termios 

53 

54 fd = sys.stdin.fileno() 

55 old_settings = termios.tcgetattr(fd) 

56 try: 

57 tty.setraw(fd) 

58 ch = sys.stdin.read(1) 

59 finally: 

60 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 

61 

62 return ch 

63 

64 

65class _GetchWindows: 

66 def __init__(self): 

67 import msvcrt # noqa 

68 

69 def __call__(self): 

70 import msvcrt 

71 return msvcrt.getch() 

72 

73 

74getch = _Getch() 

75 

76 

77class PyrockoQApplication(qw.QApplication): 

78 

79 def __init__(self): 

80 qw.QApplication.__init__(self, []) 

81 self._main_window = None 

82 

83 def install_sigint_handler(self): 

84 self._old_signal_handler = signal.signal( 

85 signal.SIGINT, 

86 self.request_close_all_windows) 

87 

88 def uninstall_sigint_handler(self): 

89 signal.signal(signal.SIGINT, self._old_signal_handler) 

90 

91 def set_main_window(self, win): 

92 self._main_window = win 

93 name = win.windowTitle() if win is not None else '' 

94 self.setApplicationName(name) 

95 self.setApplicationDisplayName(name) 

96 self.setDesktopFileName(name) 

97 

98 def unset_main_window(self): 

99 self.set_main_window(None) 

100 

101 def get_main_window(self): 

102 return self._main_window 

103 

104 def get_main_windows(self): 

105 return [self.get_main_window()] 

106 

107 def status(self, message, duration=None): 

108 win = self.get_main_window() 

109 if not win: 

110 print(' - %s' % message, file=sys.stderr) 

111 else: 

112 win.statusBar().showMessage( 

113 message, int((duration or 0) * 1000)) 

114 

115 def event(self, e): 

116 if isinstance(e, qg.QFileOpenEvent): 

117 path = str(e.file()) 

118 if path != sys.argv[0]: 

119 wins = self.get_main_windows() 

120 if wins: 

121 wins[0].get_view().load_soon([path]) 

122 

123 return True 

124 else: 

125 return qw.QApplication.event(self, e) 

126 

127 def request_close_all_windows(self, *args): 

128 

129 def confirm(): 

130 try: 

131 print( 

132 '\nQuit %s? [y/n]' % self.applicationName(), 

133 file=sys.stderr) 

134 

135 confirmed = getch() == 'y' 

136 if not confirmed: 

137 print( 

138 'Continuing.', 

139 file=sys.stderr) 

140 else: 

141 print( 

142 'Quitting %s.' % self.applicationName(), 

143 file=sys.stderr) 

144 

145 return confirmed 

146 

147 except Exception: 

148 return False 

149 

150 if confirm(): 

151 for win in self.get_main_windows(): 

152 win.instant_close = True 

153 

154 self.closeAllWindows() 

155 

156 

157app = None 

158 

159 

160def get_app(): 

161 from .qt_compat import qg 

162 try: 

163 global app 

164 if app is None: 

165 qg.QSurfaceFormat.setDefaultFormat(qg.QSurfaceFormat()) 

166 app = PyrockoQApplication() 

167 return app 

168 except NameError: # can happen during shutdown 

169 return None 

170 

171 

172def rint(x): 

173 return int(round(x)) 

174 

175 

176def make_QPolygonF(xdata, ydata): 

177 assert len(xdata) == len(ydata) 

178 qpoints = qg.QPolygonF(len(ydata)) 

179 vptr = qpoints.data() 

180 vptr.setsize(len(ydata)*8*2) 

181 aa = num.ndarray( 

182 shape=(len(ydata), 2), 

183 dtype=num.float64, 

184 buffer=memoryview(vptr)) 

185 aa.setflags(write=True) 

186 aa[:, 0] = xdata 

187 aa[:, 1] = ydata 

188 return qpoints 

189 

190 

191def get_colormap_qimage(cmap_name, vmin=None, vmax=None): 

192 NCOLORS = 512 

193 norm = Normalize() 

194 norm.vmin = vmin 

195 norm.vmax = vmax 

196 

197 return qg.QImage( 

198 plot.mpl_get_cmap(cmap_name)( 

199 norm(num.linspace(0., 1., NCOLORS)), 

200 alpha=None, bytes=True), 

201 NCOLORS, 1, qg.QImage.Format_RGBX8888) 

202 

203 

204class Label(object): 

205 def __init__( 

206 self, p, x, y, label_str, 

207 label_bg=None, 

208 anchor='BL', 

209 outline=False, 

210 font=None, 

211 color=None): 

212 

213 text = qg.QTextDocument() 

214 if font: 

215 text.setDefaultFont(font) 

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

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

218 s = text.size() 

219 rect = qc.QRect(0, 0, int(s.width()), int(s.height())) 

220 tx, ty = x, y 

221 

222 if 'B' in anchor: 

223 ty -= rect.height() 

224 if 'R' in anchor: 

225 tx -= rect.width() 

226 if 'M' in anchor: 

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

228 if 'C' in anchor: 

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

230 

231 rect.translate(int(tx), int(ty)) 

232 self.rect = rect 

233 self.text = text 

234 self.outline = outline 

235 self.label_bg = label_bg 

236 self.color = color 

237 self.p = p 

238 

239 def draw(self): 

240 p = self.p 

241 rect = self.rect 

242 tx = rect.left() 

243 ty = rect.top() 

244 

245 if self.outline: 

246 oldpen = p.pen() 

247 oldbrush = p.brush() 

248 p.setBrush(self.label_bg) 

249 rect.adjust(-2, 0, 2, 0) 

250 p.drawRect(rect) 

251 p.setPen(oldpen) 

252 p.setBrush(oldbrush) 

253 

254 else: 

255 if self.label_bg: 

256 p.fillRect(rect, self.label_bg) 

257 

258 p.translate(int(tx), int(ty)) 

259 self.text.drawContents(p) 

260 p.translate(-int(tx), -int(ty)) 

261 

262 

263def draw_label(p, x, y, label_str, label_bg, anchor='BL', outline=False): 

264 fm = p.fontMetrics() 

265 

266 label = label_str 

267 rect = fm.boundingRect(label) 

268 

269 tx, ty = x, y 

270 if 'T' in anchor: 

271 ty += rect.height() 

272 if 'R' in anchor: 

273 tx -= rect.width() 

274 if 'M' in anchor: 

275 ty += rect.height() // 2 

276 if 'C' in anchor: 

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

278 

279 rect.translate(int(tx), int(ty)) 

280 if outline: 

281 oldpen = p.pen() 

282 oldbrush = p.brush() 

283 p.setBrush(label_bg) 

284 rect.adjust(-2, 0, 2, 0) 

285 p.drawRect(rect) 

286 p.setPen(oldpen) 

287 p.setBrush(oldbrush) 

288 

289 else: 

290 p.fillRect(rect, label_bg) 

291 

292 p.drawText(int(tx), int(ty), label) 

293 

294 

295def get_err_palette(): 

296 err_palette = qg.QPalette() 

297 err_palette.setColor(qg.QPalette.Base, qg.QColor(255, 200, 200)) 

298 return err_palette 

299 

300 

301class QSliderNoWheel(qw.QSlider): 

302 

303 def wheelEvent(self, ev): 

304 '' 

305 ev.ignore() 

306 

307 def keyPressEvent(self, ev): 

308 '' 

309 ev.ignore() 

310 

311 

312class QSliderFloat(qw.QSlider): 

313 

314 sliderMovedFloat = qc.pyqtSignal(float) 

315 valueChangedFloat = qc.pyqtSignal(float) 

316 rangeChangedFloat = qc.pyqtSignal(float, float) 

317 

318 def __init__(self, *args, **kwargs): 

319 qw.QSlider.__init__(self, *args, **kwargs) 

320 self.setMinimum(0) 

321 self.setMaximum(1000) 

322 self.setSingleStep(10) 

323 self.setPageStep(100) 

324 self._fmin = 0. 

325 self._fmax = 1. 

326 self.valueChanged.connect(self._handleValueChanged) 

327 self.sliderMoved.connect(self._handleSliderMoved) 

328 

329 def _f_to_i(self, fval): 

330 fval = float(fval) 

331 imin = self.minimum() 

332 imax = self.maximum() 

333 return max( 

334 imin, 

335 imin + min( 

336 int(round( 

337 (fval-self._fmin) * (imax - imin) 

338 / (self._fmax-self._fmin))), 

339 imax)) 

340 

341 def _i_to_f(self, ival): 

342 imin = self.minimum() 

343 imax = self.maximum() 

344 return self._fmin + (ival - imin) * (self._fmax - self._fmin) \ 

345 / (imax - imin) 

346 

347 def minimumFloat(self): 

348 return self._fmin 

349 

350 def setMinimumFloat(self, fval): 

351 self._fmin = float(fval) 

352 self.rangeChangedFloat.emit(self._fmin, self._fmax) 

353 

354 def maximumFloat(self): 

355 return self._fmax 

356 

357 def setMaximumFloat(self, fval): 

358 self._fmax = float(fval) 

359 self.rangeChangedFloat.emit(self._fmin, self._fmax) 

360 

361 def setRangeFloat(self, fmin, fmax): 

362 self._fmin = float(fmin) 

363 self._fmax = float(fmax) 

364 self.rangeChangedFloat.emit(self._fmin, self._fmax) 

365 

366 def valueFloat(self): 

367 return self._i_to_f(self.value()) 

368 

369 def setValueFloat(self, fval): 

370 qw.QSlider.setValue(self, self._f_to_i(fval)) 

371 

372 def _handleValueChanged(self, ival): 

373 self.valueChangedFloat.emit(self._i_to_f(ival)) 

374 

375 def _handleSliderMoved(self, ival): 

376 self.sliderMovedFloat.emit(self._i_to_f(ival)) 

377 

378 

379class MyValueEdit(qw.QLineEdit): 

380 

381 edited = qc.pyqtSignal(float) 

382 

383 def __init__( 

384 self, 

385 low_is_none=False, 

386 high_is_none=False, 

387 low_is_zero=False, 

388 *args, **kwargs): 

389 

390 qw.QLineEdit.__init__(self, *args, **kwargs) 

391 self.value = 0. 

392 self.mi = 0. 

393 self.ma = 1. 

394 self.low_is_none = low_is_none 

395 self.high_is_none = high_is_none 

396 self.low_is_zero = low_is_zero 

397 self.editingFinished.connect( 

398 self.myEditingFinished) 

399 self.lock = False 

400 

401 def setRange(self, mi, ma): 

402 self.mi = mi 

403 self.ma = ma 

404 

405 def setValue(self, value): 

406 if not self.lock: 

407 self.value = value 

408 self.setPalette(qw.QApplication.palette()) 

409 self.adjust_text() 

410 

411 def myEditingFinished(self): 

412 try: 

413 t = str(self.text()).strip() 

414 if self.low_is_none and t in ('off', 'below'): 

415 value = self.mi 

416 elif self.high_is_none and t in ('off', 'above'): 

417 value = self.ma 

418 elif self.low_is_zero and float(t) == 0.0: 

419 value = self.mi 

420 else: 

421 value = float(t) 

422 

423 if not (self.mi <= value <= self.ma): 

424 raise Exception('out of range') 

425 

426 if value != self.value: 

427 self.value = value 

428 self.lock = True 

429 self.edited.emit(value) 

430 self.setPalette(qw.QApplication.palette()) 

431 except Exception: 

432 self.setPalette(get_err_palette()) 

433 

434 self.lock = False 

435 

436 def adjust_text(self): 

437 t = ('%8.5g' % self.value).strip() 

438 

439 if self.low_is_zero and self.value == self.mi: 

440 t = '0' 

441 

442 if self.low_is_none and self.value == self.mi: 

443 if self.high_is_none: 

444 t = 'below' 

445 else: 

446 t = 'off' 

447 

448 if self.high_is_none and self.value == self.ma: 

449 if self.low_is_none: 

450 t = 'above' 

451 else: 

452 t = 'off' 

453 

454 if t in ('off', 'below', 'above'): 

455 self.setStyleSheet('font-style: italic;') 

456 else: 

457 self.setStyleSheet(None) 

458 

459 self.setText(t) 

460 

461 

462class ValControl(qw.QWidget): 

463 

464 valchange = qc.pyqtSignal(object, int) 

465 

466 def __init__( 

467 self, 

468 low_is_none=False, 

469 high_is_none=False, 

470 low_is_zero=False, 

471 type=float, 

472 *args): 

473 

474 qc.QObject.__init__(self, *args) 

475 

476 self.lname = qw.QLabel('name') 

477 self.lname.setSizePolicy( 

478 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

479 self.lvalue = MyValueEdit( 

480 low_is_none=low_is_none, 

481 high_is_none=high_is_none, 

482 low_is_zero=low_is_zero) 

483 self.lvalue.setFixedWidth(100) 

484 self.slider = QSliderNoWheel(qc.Qt.Horizontal) 

485 self.slider.setSizePolicy( 

486 qw.QSizePolicy(qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

487 self.slider.setMaximum(10000) 

488 self.slider.setSingleStep(100) 

489 self.slider.setPageStep(1000) 

490 self.slider.setTickPosition(qw.QSlider.NoTicks) 

491 self.slider.setFocusPolicy(qc.Qt.ClickFocus) 

492 

493 self.low_is_none = low_is_none 

494 self.high_is_none = high_is_none 

495 self.low_is_zero = low_is_zero 

496 

497 self.slider.valueChanged.connect( 

498 self.slided) 

499 self.lvalue.edited.connect( 

500 self.edited) 

501 

502 self.type = type 

503 self.mute = False 

504 

505 def widgets(self): 

506 return self.lname, self.lvalue, self.slider 

507 

508 def s2v(self, svalue): 

509 if self.ma == 0 or self.mi == 0: 

510 return 0 

511 

512 a = math.log(self.ma/self.mi) / 10000. 

513 value = self.mi*math.exp(a*svalue) 

514 value = self.type(value) 

515 return value 

516 

517 def v2s(self, value): 

518 value = self.type(value) 

519 

520 if value == 0 or self.mi == 0: 

521 return 0 

522 

523 a = math.log(self.ma/self.mi) / 10000. 

524 return int(round(math.log(value/self.mi) / a)) 

525 

526 def setup(self, name, mi, ma, cur, ind): 

527 self.lname.setText(name) 

528 self.mi = mi 

529 self.ma = ma 

530 self.ind = ind 

531 self.lvalue.setRange(self.s2v(0), self.s2v(10000)) 

532 self.set_value(cur) 

533 

534 def set_range(self, mi, ma): 

535 if self.mi == mi and self.ma == ma: 

536 return 

537 

538 vput = None 

539 if self.cursl == 0: 

540 vput = mi 

541 if self.cursl == 10000: 

542 vput = ma 

543 

544 self.mi = mi 

545 self.ma = ma 

546 self.lvalue.setRange(self.s2v(0), self.s2v(10000)) 

547 

548 if vput is not None: 

549 self.set_value(vput) 

550 else: 

551 if self.cur < mi: 

552 self.set_value(mi) 

553 if self.cur > ma: 

554 self.set_value(ma) 

555 

556 def set_value(self, cur): 

557 if cur is None: 

558 if self.low_is_none: 

559 cur = self.mi 

560 elif self.high_is_none: 

561 cur = self.ma 

562 

563 if cur == 0.0: 

564 if self.low_is_zero: 

565 cur = self.mi 

566 

567 self.mute = True 

568 self.cur = cur 

569 self.cursl = self.v2s(cur) 

570 self.slider.blockSignals(True) 

571 self.slider.setValue(self.cursl) 

572 self.slider.blockSignals(False) 

573 self.lvalue.blockSignals(True) 

574 if self.cursl in (0, 10000): 

575 self.lvalue.setValue(self.s2v(self.cursl)) 

576 else: 

577 self.lvalue.setValue(self.cur) 

578 self.lvalue.blockSignals(False) 

579 self.mute = False 

580 

581 def set_tracking(self, tracking): 

582 self.slider.setTracking(tracking) 

583 

584 def get_value(self): 

585 return self.cur 

586 

587 def slided(self, val): 

588 if self.cursl != val: 

589 self.cursl = val 

590 cur = self.s2v(self.cursl) 

591 

592 if cur != self.cur: 

593 self.cur = cur 

594 self.lvalue.blockSignals(True) 

595 self.lvalue.setValue(self.cur) 

596 self.lvalue.blockSignals(False) 

597 self.fire_valchange() 

598 

599 def edited(self, val): 

600 if self.cur != val: 

601 self.cur = val 

602 cursl = self.v2s(val) 

603 if (cursl != self.cursl): 

604 self.slider.blockSignals(True) 

605 self.slider.setValue(cursl) 

606 self.slider.blockSignals(False) 

607 self.cursl = cursl 

608 

609 self.fire_valchange() 

610 

611 def fire_valchange(self): 

612 

613 if self.mute: 

614 return 

615 

616 cur = self.cur 

617 

618 if self.cursl == 0: 

619 if self.low_is_none: 

620 cur = None 

621 

622 elif self.low_is_zero: 

623 cur = 0.0 

624 

625 if self.cursl == 10000 and self.high_is_none: 

626 cur = None 

627 

628 self.valchange.emit(cur, int(self.ind)) 

629 

630 

631class LinValControl(ValControl): 

632 

633 def s2v(self, svalue): 

634 value = svalue/10000. * (self.ma-self.mi) + self.mi 

635 value = self.type(value) 

636 return value 

637 

638 def v2s(self, value): 

639 value = self.type(value) 

640 if self.ma == self.mi: 

641 return 0 

642 return int(round((value-self.mi)/(self.ma-self.mi) * 10000.)) 

643 

644 

645class ColorbarControl(qw.QWidget): 

646 

647 AVAILABLE_CMAPS = ( 

648 'viridis', 

649 'plasma', 

650 'magma', 

651 'binary', 

652 'Reds', 

653 'copper', 

654 'seismic', 

655 'RdBu', 

656 'YlGn', 

657 ) 

658 

659 DEFAULT_CMAP = 'viridis' 

660 

661 cmap_changed = qc.pyqtSignal(str) 

662 show_absolute_toggled = qc.pyqtSignal(bool) 

663 show_integrate_toggled = qc.pyqtSignal(bool) 

664 

665 def __init__(self, *args, **kwargs): 

666 super().__init__(*args, **kwargs) 

667 

668 self.lname = qw.QLabel('Colormap') 

669 self.lname.setSizePolicy( 

670 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

671 

672 self.cmap_options = qw.QComboBox() 

673 self.cmap_options.setIconSize(qc.QSize(64, 12)) 

674 for ic, cmap in enumerate(self.AVAILABLE_CMAPS): 

675 pixmap = qg.QPixmap.fromImage( 

676 get_colormap_qimage(cmap)) 

677 icon = qg.QIcon(pixmap.scaled(64, 12)) 

678 

679 self.cmap_options.addItem(icon, '', cmap) 

680 self.cmap_options.setItemData(ic, cmap, qc.Qt.ToolTipRole) 

681 

682 # self.cmap_options.setCurrentIndex(self.cmap_name) 

683 self.cmap_options.currentIndexChanged.connect(self.set_cmap) 

684 self.cmap_options.setSizePolicy( 

685 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

686 

687 self.colorslider = ColorbarSlider(self) 

688 self.colorslider.setSizePolicy( 

689 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag, 

690 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag 

691 ) 

692 self.clip_changed = self.colorslider.clip_changed 

693 

694 btn_size = qw.QSizePolicy( 

695 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag, 

696 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag) 

697 

698 self.symetry_toggle = qw.QPushButton() 

699 self.symetry_toggle.setIcon( 

700 qg.QIcon.fromTheme('object-flip-horizontal')) 

701 self.symetry_toggle.setToolTip('Symetric clip values') 

702 self.symetry_toggle.setSizePolicy(btn_size) 

703 self.symetry_toggle.setCheckable(True) 

704 self.symetry_toggle.toggled.connect(self.toggle_symetry) 

705 self.symetry_toggle.setChecked(True) 

706 

707 self.reverse_toggle = qw.QPushButton() 

708 self.reverse_toggle.setIcon( 

709 qg.QIcon.fromTheme('object-rotate-right')) 

710 self.reverse_toggle.setToolTip('Reverse the colormap') 

711 self.reverse_toggle.setSizePolicy(btn_size) 

712 self.reverse_toggle.setCheckable(True) 

713 self.reverse_toggle.toggled.connect(self.toggle_reverse_cmap) 

714 

715 self.abs_toggle = qw.QPushButton() 

716 self.abs_toggle.setIcon( 

717 qg.QIcon.fromTheme('go-bottom')) 

718 self.abs_toggle.setToolTip('Show absolute values') 

719 self.abs_toggle.setSizePolicy(btn_size) 

720 self.abs_toggle.setCheckable(True) 

721 self.abs_toggle.toggled.connect(self.toggle_absolute) 

722 

723 self.int_toggle = qw.QPushButton() 

724 self.int_toggle.setText('∫') 

725 self.int_toggle.setToolTip( 

726 u'Integrate traces (e.g. strain rate → strain)') 

727 self.int_toggle.setSizePolicy(btn_size) 

728 self.int_toggle.setCheckable(True) 

729 self.int_toggle.setMaximumSize( 

730 24, 

731 self.int_toggle.maximumSize().height()) 

732 self.int_toggle.toggled.connect(self.show_integrate_toggled.emit) 

733 

734 v_splitter = qw.QFrame() 

735 v_splitter.setFrameShape(qw.QFrame.VLine) 

736 v_splitter.setFrameShadow(qw.QFrame.Sunken) 

737 

738 self.controls = qw.QWidget() 

739 layout = qw.QHBoxLayout() 

740 layout.addWidget(self.colorslider) 

741 layout.addWidget(self.symetry_toggle) 

742 layout.addWidget(self.reverse_toggle) 

743 layout.addWidget(v_splitter) 

744 layout.addWidget(self.abs_toggle) 

745 layout.addWidget(self.int_toggle) 

746 self.controls.setLayout(layout) 

747 

748 self.set_cmap_name(self.DEFAULT_CMAP) 

749 

750 def set_cmap(self, idx): 

751 self.set_cmap_name(self.cmap_options.itemData(idx)) 

752 

753 def set_cmap_name(self, cmap_name): 

754 self.cmap_name = cmap_name 

755 self.colorslider.set_cmap_name(cmap_name) 

756 self.cmap_changed.emit(cmap_name) 

757 

758 def get_cmap(self): 

759 return self.cmap_name 

760 

761 def toggle_symetry(self, toggled): 

762 self.colorslider.set_symetry(toggled) 

763 

764 def toggle_reverse_cmap(self): 

765 cmap = self.get_cmap() 

766 if cmap.endswith('_r'): 

767 r_cmap = cmap.rstrip('_r') 

768 else: 

769 r_cmap = cmap + '_r' 

770 self.set_cmap_name(r_cmap) 

771 

772 def toggle_absolute(self, toggled): 

773 self.symetry_toggle.setChecked(not toggled) 

774 self.show_absolute_toggled.emit(toggled) 

775 

776 def widgets(self): 

777 return (self.lname, self.cmap_options, self.controls) 

778 

779 

780class ColorbarSlider(qw.QWidget): 

781 DEFAULT_CMAP = 'viridis' 

782 CORNER_THRESHOLD = 10 

783 MIN_WIDTH = .05 

784 

785 clip_changed = qc.pyqtSignal(float, float) 

786 

787 class COMPONENTS(enum.Enum): 

788 LeftLine = 1 

789 RightLine = 2 

790 Center = 3 

791 

792 def __init__(self, *args, cmap_name=None): 

793 super().__init__() 

794 self.cmap_name = cmap_name or self.DEFAULT_CMAP 

795 self.clip_min = 0. 

796 self.clip_max = 1. 

797 

798 self._sym_locked = True 

799 self._mouse_inside = False 

800 self._window = None 

801 self._old_pos = None 

802 self._component_grabbed = None 

803 

804 self.setMouseTracking(True) 

805 

806 def set_cmap_name(self, cmap_name): 

807 self.cmap_name = cmap_name 

808 self.repaint() 

809 

810 def get_cmap_name(self): 

811 return self.cmap_name 

812 

813 def set_symetry(self, symetry): 

814 self._sym_locked = symetry 

815 if self._sym_locked: 

816 clip_max = 1. - min(self.clip_min, 1.-self.clip_max) 

817 clip_min = 1. - clip_max 

818 self.set_clip(clip_min, clip_max) 

819 

820 def _set_window(self, window): 

821 self._window = window 

822 

823 def _get_left_line(self): 

824 rect = self._get_active_rect() 

825 if not rect: 

826 return 

827 return qc.QLineF(rect.left(), 0, rect.left(), rect.height()) 

828 

829 def _get_right_line(self): 

830 rect = self._get_active_rect() 

831 if not rect: 

832 return 

833 return qc.QLineF(rect.right(), 0, rect.right(), rect.height()) 

834 

835 def _get_active_rect(self): 

836 if not self._window: 

837 return 

838 rect = qc.QRect(self._window) 

839 width = rect.width() 

840 rect.setLeft(width * self.clip_min) 

841 rect.setRight(width * self.clip_max) 

842 return rect 

843 

844 def set_clip(self, clip_min, clip_max): 

845 if clip_min < 0. or clip_max > 1.: 

846 return 

847 if clip_max - clip_min < self.MIN_WIDTH: 

848 return 

849 

850 self.clip_min = clip_min 

851 self.clip_max = clip_max 

852 self.repaint() 

853 self.clip_changed.emit(self.clip_min, self.clip_max) 

854 

855 def mousePressEvent(self, event): 

856 '' 

857 act_rect = self._get_active_rect() 

858 if event.buttons() != qc.Qt.MouseButton.LeftButton: 

859 self._component_grabbed = None 

860 return 

861 

862 dist_left = abs(event.pos().x() - act_rect.left()) 

863 dist_right = abs(event.pos().x() - act_rect.right()) 

864 

865 if 0 < dist_left < self.CORNER_THRESHOLD: 

866 self._component_grabbed = self.COMPONENTS.LeftLine 

867 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor)) 

868 elif 0 < dist_right < self.CORNER_THRESHOLD: 

869 self._component_grabbed = self.COMPONENTS.RightLine 

870 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor)) 

871 else: 

872 self.setCursor(qg.QCursor()) 

873 

874 def mouseReleaseEvent(self, event): 

875 '' 

876 self._component_grabbed = None 

877 self.repaint() 

878 

879 def mouseDoubleClickEvent(self, event): 

880 '' 

881 self.set_clip(0., 1.) 

882 

883 def wheelEvent(self, event): 

884 '' 

885 event.accept() 

886 if not self._sym_locked: 

887 return 

888 

889 delta = event.angleDelta().y() 

890 delta = -delta / 5e3 

891 clip_min_new = max(self.clip_min + delta, 0.) 

892 clip_max_new = min(self.clip_max - delta, 1.) 

893 self._mouse_inside = True 

894 self.set_clip(clip_min_new, clip_max_new) 

895 

896 def mouseMoveEvent(self, event): 

897 '' 

898 act_rect = self._get_active_rect() 

899 

900 if not self._component_grabbed: 

901 dist_left = abs(event.pos().x() - act_rect.left()) 

902 dist_right = abs(event.pos().x() - act_rect.right()) 

903 

904 if 0 <= dist_left < self.CORNER_THRESHOLD or \ 

905 0 <= dist_right < self.CORNER_THRESHOLD: 

906 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor)) 

907 else: 

908 self.setCursor(qg.QCursor()) 

909 

910 if self._old_pos and self._component_grabbed: 

911 shift = (event.pos() - self._old_pos).x() / self._window.width() 

912 

913 if self._component_grabbed is self.COMPONENTS.LeftLine: 

914 clip_min_new = max(self.clip_min + shift, 0.) 

915 clip_max_new = \ 

916 min(self.clip_max - shift, 1.) \ 

917 if self._sym_locked else self.clip_max 

918 

919 elif self._component_grabbed is self.COMPONENTS.RightLine: 

920 clip_max_new = min(self.clip_max + shift, 1.) 

921 clip_min_new = \ 

922 max(self.clip_min - shift, 0.) \ 

923 if self._sym_locked else self.clip_min 

924 

925 self.set_clip(clip_min_new, clip_max_new) 

926 

927 self._old_pos = event.pos() 

928 

929 def enterEvent(self, e): 

930 '' 

931 self._mouse_inside = True 

932 self.repaint() 

933 

934 def leaveEvent(self, e): 

935 '' 

936 self._mouse_inside = False 

937 self.repaint() 

938 

939 def paintEvent(self, e): 

940 '' 

941 p = qg.QPainter(self) 

942 self._set_window(p.window()) 

943 

944 p.drawImage( 

945 p.window(), 

946 get_colormap_qimage(self.cmap_name, self.clip_min, self.clip_max)) 

947 

948 left_line = self._get_left_line() 

949 right_line = self._get_right_line() 

950 

951 pen = qg.QPen() 

952 pen.setWidth(2) 

953 pen.setStyle(qc.Qt.DotLine) 

954 pen.setBrush(qc.Qt.white) 

955 p.setPen(pen) 

956 p.setCompositionMode( 

957 qg.QPainter.CompositionMode.CompositionMode_Difference) 

958 

959 p.drawLine(left_line) 

960 p.drawLine(right_line) 

961 

962 label_rect = self._get_active_rect() 

963 label_rect.setLeft(label_rect.left() + 5) 

964 label_rect.setRight(label_rect.right() - 5) 

965 label_left_rect = qc.QRectF(label_rect) 

966 label_right_rect = qc.QRectF(label_rect) 

967 label_left_align = qc.Qt.AlignLeft 

968 label_right_align = qc.Qt.AlignRight 

969 

970 if label_rect.left() > 50: 

971 label_left_rect.setRight(label_rect.left() - 10) 

972 label_left_rect.setLeft(0) 

973 label_left_align = qc.Qt.AlignRight 

974 

975 if self._window.right() - label_rect.right() > 50: 

976 label_right_rect.setLeft(label_rect.right() + 10) 

977 label_right_rect.setRight(self._window.right()) 

978 label_right_align = qc.Qt.AlignLeft 

979 

980 if self._mouse_inside or self._component_grabbed: 

981 p.drawText( 

982 label_left_rect, 

983 label_left_align | qc.Qt.AlignVCenter, 

984 '%d%%' % round(self.clip_min * 100)) 

985 p.drawText( 

986 label_right_rect, 

987 label_right_align | qc.Qt.AlignVCenter, 

988 '%d%%' % round(self.clip_max * 100)) 

989 

990 

991class Progressbar(object): 

992 def __init__(self, parent, name, can_abort=True): 

993 self.parent = parent 

994 self.name = name 

995 self.label = qw.QLabel(name, parent) 

996 self.pbar = qw.QProgressBar(parent) 

997 self.aborted = False 

998 self.time_last_update = 0. 

999 if can_abort: 

1000 self.abort_button = qw.QPushButton('Abort', parent) 

1001 self.abort_button.clicked.connect( 

1002 self.abort) 

1003 else: 

1004 self.abort_button = None 

1005 

1006 def widgets(self): 

1007 widgets = [self.label, self.bar()] 

1008 if self.abort_button: 

1009 widgets.append(self.abort_button) 

1010 return widgets 

1011 

1012 def bar(self): 

1013 return self.pbar 

1014 

1015 def abort(self): 

1016 self.aborted = True 

1017 

1018 

1019class Progressbars(qw.QFrame): 

1020 def __init__(self, parent): 

1021 qw.QFrame.__init__(self, parent) 

1022 self.layout = qw.QGridLayout() 

1023 self.setLayout(self.layout) 

1024 self.bars = {} 

1025 self.start_times = {} 

1026 self.hide() 

1027 

1028 def set_status(self, name, value, can_abort=True, force=False): 

1029 value = int(round(value)) 

1030 now = time.time() 

1031 if name not in self.start_times: 

1032 self.start_times[name] = now 

1033 if not force: 

1034 return False 

1035 else: 

1036 if now < self.start_times[name] + 1.0: 

1037 if value == 100: 

1038 del self.start_times[name] 

1039 if not force: 

1040 return False 

1041 

1042 self.start_times.get(name, 0.0) 

1043 if name not in self.bars: 

1044 if value == 100: 

1045 return False 

1046 self.bars[name] = Progressbar(self, name, can_abort=can_abort) 

1047 self.make_layout() 

1048 

1049 bar = self.bars[name] 

1050 if bar.time_last_update < now - 0.1 or value == 100: 

1051 bar.bar().setValue(value) 

1052 bar.time_last_update = now 

1053 

1054 if value == 100: 

1055 del self.bars[name] 

1056 if name in self.start_times: 

1057 del self.start_times[name] 

1058 self.make_layout() 

1059 for w in bar.widgets(): 

1060 w.setParent(None) 

1061 

1062 return bar.aborted 

1063 

1064 def make_layout(self): 

1065 while True: 

1066 c = self.layout.takeAt(0) 

1067 if c is None: 

1068 break 

1069 

1070 for ibar, bar in enumerate(self.bars.values()): 

1071 for iw, w in enumerate(bar.widgets()): 

1072 self.layout.addWidget(w, ibar, iw) 

1073 

1074 if not self.bars: 

1075 self.hide() 

1076 else: 

1077 self.show() 

1078 

1079 

1080def tohex(c): 

1081 return '%02x%02x%02x' % c 

1082 

1083 

1084def to01(c): 

1085 return c[0]/255., c[1]/255., c[2]/255. 

1086 

1087 

1088def beautify_axes(axes): 

1089 try: 

1090 from cycler import cycler 

1091 axes.set_prop_cycle( 

1092 cycler('color', [to01(x) for x in plot.graph_colors])) 

1093 

1094 except (ImportError, KeyError): 

1095 axes.set_color_cycle(list(map(to01, plot.graph_colors))) 

1096 

1097 xa = axes.get_xaxis() 

1098 ya = axes.get_yaxis() 

1099 for attr in ('labelpad', 'LABELPAD'): 

1100 if hasattr(xa, attr): 

1101 setattr(xa, attr, xa.get_label().get_fontsize()) 

1102 setattr(ya, attr, ya.get_label().get_fontsize()) 

1103 break 

1104 

1105 

1106class FigureFrame(qw.QFrame): 

1107 ''' 

1108 A widget to present a :py:mod:`matplotlib` figure. 

1109 ''' 

1110 

1111 def __init__(self, parent=None, figure_cls=None): 

1112 qw.QFrame.__init__(self, parent) 

1113 fgcolor = plot.tango_colors['aluminium5'] 

1114 dpi = 0.5*(self.logicalDpiX() + self.logicalDpiY()) 

1115 

1116 font = qg.QFont() 

1117 font.setBold(True) 

1118 fontsize = font.pointSize() 

1119 

1120 import matplotlib 

1121 matplotlib.rcdefaults() 

1122 matplotlib.rcParams['backend'] = 'Qt5Agg' 

1123 

1124 matplotlib.rc('xtick', direction='out', labelsize=fontsize) 

1125 matplotlib.rc('ytick', direction='out', labelsize=fontsize) 

1126 matplotlib.rc('xtick.major', size=8, width=1) 

1127 matplotlib.rc('xtick.minor', size=4, width=1) 

1128 matplotlib.rc('ytick.major', size=8, width=1) 

1129 matplotlib.rc('ytick.minor', size=4, width=1) 

1130 matplotlib.rc('figure', facecolor='white', edgecolor=tohex(fgcolor)) 

1131 

1132 matplotlib.rc( 

1133 'font', 

1134 family='sans-serif', 

1135 weight='bold', 

1136 size=fontsize, 

1137 **{'sans-serif': [ 

1138 font.family(), 

1139 'DejaVu Sans', 'Bitstream Vera Sans', 'Lucida Grande', 

1140 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica']}) 

1141 

1142 matplotlib.rc('legend', fontsize=fontsize) 

1143 

1144 matplotlib.rc('text', color=tohex(fgcolor)) 

1145 matplotlib.rc('xtick', color=tohex(fgcolor)) 

1146 matplotlib.rc('ytick', color=tohex(fgcolor)) 

1147 matplotlib.rc('figure.subplot', bottom=0.15) 

1148 

1149 matplotlib.rc('axes', linewidth=1.0, unicode_minus=False) 

1150 matplotlib.rc( 

1151 'axes', 

1152 facecolor='white', 

1153 edgecolor=tohex(fgcolor), 

1154 labelcolor=tohex(fgcolor)) 

1155 

1156 try: 

1157 from cycler import cycler 

1158 matplotlib.rc( 

1159 'axes', prop_cycle=cycler( 

1160 'color', [to01(x) for x in plot.graph_colors])) 

1161 

1162 except (ImportError, KeyError): 

1163 try: 

1164 matplotlib.rc('axes', color_cycle=[ 

1165 to01(x) for x in plot.graph_colors]) 

1166 

1167 except KeyError: 

1168 pass 

1169 

1170 try: 

1171 matplotlib.rc('axes', labelsize=fontsize) 

1172 except KeyError: 

1173 pass 

1174 

1175 try: 

1176 matplotlib.rc('axes', labelweight='bold') 

1177 except KeyError: 

1178 pass 

1179 

1180 if figure_cls is None: 

1181 from matplotlib.figure import Figure 

1182 figure_cls = Figure 

1183 

1184 from matplotlib.backends.backend_qt5agg import \ 

1185 NavigationToolbar2QT as NavigationToolbar 

1186 

1187 from matplotlib.backends.backend_qt5agg \ 

1188 import FigureCanvasQTAgg as FigureCanvas 

1189 

1190 layout = qw.QGridLayout() 

1191 layout.setContentsMargins(0, 0, 0, 0) 

1192 layout.setSpacing(0) 

1193 

1194 self.setLayout(layout) 

1195 self.figure = figure_cls(dpi=dpi) 

1196 self.canvas = FigureCanvas(self.figure) 

1197 self.canvas.setParent(self) 

1198 self.canvas.setSizePolicy( 

1199 qw.QSizePolicy( 

1200 qw.QSizePolicy.Expanding, 

1201 qw.QSizePolicy.Expanding)) 

1202 toolbar_frame = qw.QFrame() 

1203 toolbar_frame.setFrameShape(qw.QFrame.StyledPanel) 

1204 toolbar_frame_layout = qw.QHBoxLayout() 

1205 toolbar_frame_layout.setContentsMargins(0, 0, 0, 0) 

1206 toolbar_frame.setLayout(toolbar_frame_layout) 

1207 self.toolbar = NavigationToolbar(self.canvas, self) 

1208 layout.addWidget(self.canvas, 0, 0) 

1209 toolbar_frame_layout.addWidget(self.toolbar) 

1210 layout.addWidget(toolbar_frame, 1, 0) 

1211 self.closed = False 

1212 

1213 def gca(self): 

1214 axes = self.figure.gca() 

1215 beautify_axes(axes) 

1216 return axes 

1217 

1218 def gcf(self): 

1219 return self.figure 

1220 

1221 def draw(self): 

1222 ''' 

1223 Draw with AGG, then queue for Qt update. 

1224 ''' 

1225 self.canvas.draw() 

1226 

1227 def closeEvent(self, ev): 

1228 self.closed = True 

1229 

1230 

1231class SmartplotFrame(FigureFrame): 

1232 ''' 

1233 A widget to present a :py:mod:`pyrocko.plot.smartplot` figure. 

1234 ''' 

1235 

1236 def __init__( 

1237 self, parent=None, plot_args=[], plot_kwargs={}, plot_cls=None): 

1238 

1239 from pyrocko.plot import smartplot 

1240 

1241 FigureFrame.__init__( 

1242 self, 

1243 parent=parent, 

1244 figure_cls=smartplot.SmartplotFigure) 

1245 

1246 if plot_cls is None: 

1247 plot_cls = smartplot.Plot 

1248 

1249 self.plot = plot_cls( 

1250 *plot_args, 

1251 fig=self.figure, 

1252 call_mpl_init=False, 

1253 **plot_kwargs) 

1254 

1255 

1256class WebKitFrame(qw.QFrame): 

1257 ''' 

1258 A widget to present a html page using WebKit. 

1259 ''' 

1260 

1261 def __init__(self, url=None, parent=None): 

1262 try: 

1263 from PyQt5.QtWebEngineWidgets import QWebEngineView as WebView 

1264 except ImportError: 

1265 from PyQt5.QtWebKitWidgets import QWebView as WebView 

1266 qw.QFrame.__init__(self, parent) 

1267 layout = qw.QGridLayout() 

1268 layout.setContentsMargins(0, 0, 0, 0) 

1269 layout.setSpacing(0) 

1270 self.setLayout(layout) 

1271 self.web_widget = WebView() 

1272 layout.addWidget(self.web_widget, 0, 0) 

1273 if url: 

1274 self.web_widget.load(qc.QUrl(url)) 

1275 

1276 

1277class VTKFrame(qw.QFrame): 

1278 ''' 

1279 A widget to present a VTK visualization. 

1280 ''' 

1281 

1282 def __init__(self, actors=None, parent=None): 

1283 import vtk 

1284 from vtk.qt.QVTKRenderWindowInteractor import \ 

1285 QVTKRenderWindowInteractor 

1286 

1287 qw.QFrame.__init__(self, parent) 

1288 layout = qw.QGridLayout() 

1289 layout.setContentsMargins(0, 0, 0, 0) 

1290 layout.setSpacing(0) 

1291 

1292 self.setLayout(layout) 

1293 

1294 self.vtk_widget = QVTKRenderWindowInteractor(self) 

1295 layout.addWidget(self.vtk_widget, 0, 0) 

1296 

1297 self.renderer = vtk.vtkRenderer() 

1298 self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer) 

1299 self.iren = self.vtk_widget.GetRenderWindow().GetInteractor() 

1300 

1301 if actors: 

1302 for a in actors: 

1303 self.renderer.AddActor(a) 

1304 

1305 def init(self): 

1306 self.iren.Initialize() 

1307 

1308 def add_actor(self, actor): 

1309 self.renderer.AddActor(actor) 

1310 

1311 

1312class PixmapFrame(qw.QLabel): 

1313 ''' 

1314 A widget to preset a pixmap image. 

1315 ''' 

1316 

1317 def __init__(self, filename=None, parent=None): 

1318 

1319 qw.QLabel.__init__(self, parent) 

1320 self.setAlignment(qc.Qt.AlignCenter) 

1321 self.setContentsMargins(0, 0, 0, 0) 

1322 self.menu = qw.QMenu(self) 

1323 action = qw.QAction('Save as', self.menu) 

1324 action.triggered.connect(self.save_pixmap) 

1325 self.menu.addAction(action) 

1326 

1327 if filename: 

1328 self.load_pixmap(filename) 

1329 

1330 def contextMenuEvent(self, event): 

1331 self.menu.popup(qg.QCursor.pos()) 

1332 

1333 def load_pixmap(self, filename): 

1334 self.pixmap = qg.QPixmap(filename) 

1335 self.setPixmap(self.pixmap) 

1336 

1337 def save_pixmap(self, filename=None): 

1338 if not filename: 

1339 filename, _ = qw.QFileDialog.getSaveFileName( 

1340 self.parent(), caption='save as') 

1341 self.pixmap.save(filename) 

1342 

1343 

1344class Projection(object): 

1345 def __init__(self): 

1346 self.xr = 0., 1. 

1347 self.ur = 0., 1. 

1348 

1349 def set_in_range(self, xmin, xmax): 

1350 if xmax == xmin: 

1351 xmax = xmin + 1. 

1352 

1353 self.xr = xmin, xmax 

1354 

1355 def get_in_range(self): 

1356 return self.xr 

1357 

1358 def set_out_range(self, umin, umax): 

1359 if umax == umin: 

1360 umax = umin + 1. 

1361 

1362 self.ur = umin, umax 

1363 

1364 def get_out_range(self): 

1365 return self.ur 

1366 

1367 def __call__(self, x): 

1368 umin, umax = self.ur 

1369 xmin, xmax = self.xr 

1370 return umin + (x-xmin)*((umax-umin)/(xmax-xmin)) 

1371 

1372 def clipped(self, x): 

1373 umin, umax = self.ur 

1374 xmin, xmax = self.xr 

1375 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin)))) 

1376 

1377 def rev(self, u): 

1378 umin, umax = self.ur 

1379 xmin, xmax = self.xr 

1380 return xmin + (u-umin)*((xmax-xmin)/(umax-umin)) 

1381 

1382 

1383class NoData(Exception): 

1384 pass 

1385 

1386 

1387g_working_system_time_range = util.get_working_system_time_range() 

1388 

1389g_initial_time_range = [] 

1390 

1391try: 

1392 g_initial_time_range.append( 

1393 calendar.timegm((1950, 1, 1, 0, 0, 0))) 

1394except Exception: 

1395 g_initial_time_range.append(g_working_system_time_range[0]) 

1396 

1397try: 

1398 g_initial_time_range.append( 

1399 calendar.timegm((time.gmtime().tm_year + 1, 1, 1, 0, 0, 0))) 

1400except Exception: 

1401 g_initial_time_range.append(g_working_system_time_range[1]) 

1402 

1403 

1404def four_way_arrow(position, size): 

1405 r = 5. 

1406 w = 1. 

1407 

1408 points = [ 

1409 (position[0]+size*float(a), position[1]+size*float(b)) 

1410 for (a, b) in [ 

1411 (0, r), 

1412 (1.5*w, r-2*w), 

1413 (0.5*w, r-2*w), 

1414 (0.5*w, 0.5*w), 

1415 (r-2*w, 0.5*w), 

1416 (r-2*w, 1.5*w), 

1417 (r, 0), 

1418 (r-2*w, -1.5*w), 

1419 (r-2*w, -0.5*w), 

1420 (0.5*w, -0.5*w), 

1421 (0.5*w, -(r-2*w)), 

1422 (1.5*w, -(r-2*w)), 

1423 (0, -r), 

1424 (-1.5*w, -(r-2*w)), 

1425 (-0.5*w, -(r-2*w)), 

1426 (-0.5*w, -0.5*w), 

1427 (-(r-2*w), -0.5*w), 

1428 (-(r-2*w), -1.5*w), 

1429 (-r, 0), 

1430 (-(r-2*w), 1.5*w), 

1431 (-(r-2*w), 0.5*w), 

1432 (-0.5*w, 0.5*w), 

1433 (-0.5*w, r-2*w), 

1434 (-1.5*w, r-2*w)]] 

1435 

1436 poly = qg.QPolygon(len(points)) 

1437 for ipoint, point in enumerate(points): 

1438 poly.setPoint(ipoint, *(int(round(v)) for v in point)) 

1439 

1440 return poly 

1441 

1442 

1443def tmin_effective(tmin, tmax, tduration, tposition): 

1444 if None in (tmin, tmax, tduration, tposition): 

1445 return tmin 

1446 else: 

1447 return tmin + (tmax - tmin) * tposition 

1448 

1449 

1450def tmax_effective(tmin, tmax, tduration, tposition): 

1451 if None in (tmin, tmax, tduration, tposition): 

1452 return tmax 

1453 else: 

1454 return tmin + (tmax - tmin) * tposition + tduration 

1455 

1456 

1457class RangeEdit(qw.QFrame): 

1458 

1459 rangeChanged = qc.pyqtSignal() 

1460 focusChanged = qc.pyqtSignal() 

1461 tcursorChanged = qc.pyqtSignal() 

1462 rangeEditPressed = qc.pyqtSignal() 

1463 rangeEditReleased = qc.pyqtSignal() 

1464 

1465 def __init__(self, parent=None): 

1466 qw.QFrame.__init__(self, parent) 

1467 self.setFrameStyle(qw.QFrame.StyledPanel | qw.QFrame.Plain) 

1468 # self.setBackgroundRole(qg.QPalette.Button) 

1469 # self.setAutoFillBackground(True) 

1470 self.setMouseTracking(True) 

1471 poli = qw.QSizePolicy( 

1472 qw.QSizePolicy.Expanding, 

1473 qw.QSizePolicy.Fixed) 

1474 

1475 self.setSizePolicy(poli) 

1476 self.setMinimumSize(100, 3*24) 

1477 

1478 self._size_hint = qw.QPushButton().sizeHint() 

1479 

1480 self._track_start = None 

1481 self._track_range = None 

1482 self._track_focus = None 

1483 self._track_what = None 

1484 

1485 self._tcursor = None 

1486 self._hover_point = None 

1487 

1488 self._provider = None 

1489 self.tmin, self.tmax = None, None 

1490 self.tduration, self.tposition = None, 0. 

1491 

1492 def set_data_provider(self, provider): 

1493 self._provider = provider 

1494 

1495 def set_data_name(self, name): 

1496 self._data_name = name 

1497 

1498 def sizeHint(self): 

1499 '' 

1500 return self._size_hint 

1501 

1502 def get_data_range(self): 

1503 if self._provider: 

1504 vals = [] 

1505 for data in self._provider.iter_data(self._data_name): 

1506 vals.append(data.min()) 

1507 vals.append(data.max()) 

1508 

1509 if vals: 

1510 return min(vals), max(vals) 

1511 

1512 return None, None 

1513 

1514 def get_histogram(self, projection, h): 

1515 h = int(h) 

1516 umin_w, umax_w = projection.get_out_range() 

1517 tmin_w, tmax_w = projection.get_in_range() 

1518 nbins = int(umax_w - umin_w) 

1519 counts = num.zeros(nbins, dtype=int) 

1520 if self._provider: 

1521 for data in self._provider.iter_data(self._data_name): 

1522 ibins = ((data - tmin_w) * (nbins / (tmax_w - tmin_w))) \ 

1523 .astype(int) 

1524 num.clip(ibins, 0, nbins-1, ibins) 

1525 counts += num.bincount(ibins, minlength=nbins) 

1526 

1527 histogram = counts * h // (num.max(counts[1:-1]) or 1) 

1528 bitmap = num.zeros((h, nbins), dtype=bool) 

1529 for i in range(h): 

1530 bitmap[h-1-i, :] = histogram > i 

1531 

1532 try: 

1533 bitmap = num.packbits(bitmap, axis=1, bitorder='little') 

1534 except TypeError: 

1535 # numpy < 1.17.0 has no bitorder and default behaviour is 'big' 

1536 bitmap = num.packbits( 

1537 num.flip(bitmap.reshape((h*nbins//8, 8)), axis=1), 

1538 axis=1) 

1539 

1540 return qg.QBitmap.fromData( 

1541 qc.QSize(nbins, h), 

1542 bitmap.tobytes(), 

1543 qg.QImage.Format_MonoLSB) 

1544 

1545 def draw_time_ticks(self, painter, projection, rect): 

1546 

1547 palette = self.palette() 

1548 alpha_brush = palette.highlight() 

1549 color = alpha_brush.color() 

1550 # color.setAlpha(60) 

1551 painter.setPen(qg.QPen(color)) 

1552 

1553 tmin, tmax = projection.get_in_range() 

1554 tinc, tinc_unit = plot.nice_time_tick_inc((tmax - tmin) / 7.) 

1555 tick_times, _ = plot.time_tick_labels(tmin, tmax, tinc, tinc_unit) 

1556 

1557 for tick_time in tick_times: 

1558 x = int(round(projection(tick_time))) 

1559 painter.drawLine( 

1560 x, rect.top(), x, rect.top() + rect.height() // 5) 

1561 

1562 def drawit(self, painter): 

1563 

1564 palette = self.palette() 

1565 

1566 upper_projection = self.upper_projection() 

1567 lower_projection = self.lower_projection() 

1568 

1569 upper_rect = self.upper_rect() 

1570 lower_rect = self.lower_rect() 

1571 focus_rect = self.focus_rect(upper_projection) 

1572 

1573 fill_brush = palette.brush(qg.QPalette.Button) 

1574 painter.fillRect(upper_rect, fill_brush) 

1575 

1576 if focus_rect: 

1577 painter.setBrush(palette.light()) 

1578 poly = qg.QPolygon(8) 

1579 poly.setPoint( 

1580 0, lower_rect.x(), lower_rect.y()) 

1581 poly.setPoint( 

1582 1, lower_rect.x(), lower_rect.y()+lower_rect.height()) 

1583 poly.setPoint( 

1584 2, lower_rect.x() + lower_rect.width(), 

1585 lower_rect.y() + lower_rect.height()) 

1586 poly.setPoint( 

1587 3, lower_rect.x() + lower_rect.width(), lower_rect.y()) 

1588 poly.setPoint( 

1589 4, focus_rect.x() + focus_rect.width(), 

1590 upper_rect.y() + upper_rect.height()) 

1591 poly.setPoint( 

1592 5, focus_rect.x() + focus_rect.width(), upper_rect.y()) 

1593 poly.setPoint( 

1594 6, focus_rect.x(), upper_rect.y()) 

1595 poly.setPoint( 

1596 7, focus_rect.x(), upper_rect.y() + upper_rect.height()) 

1597 painter.drawPolygon(poly) 

1598 else: 

1599 fill_brush = palette.light() 

1600 painter.fillRect(upper_rect, fill_brush) 

1601 

1602 # painter.setBrush(palette.text()) 

1603 # poly = four_way_arrow((self.width() / 2.0, self.height() / 2.0), 2.) 

1604 # painter.drawPolygon(poly) 

1605 

1606 self.draw_time_ticks(painter, upper_projection, upper_rect) 

1607 if focus_rect and self.tduration: 

1608 self.draw_time_ticks(painter, lower_projection, lower_rect) 

1609 

1610 xpen = qg.QPen(palette.color(qg.QPalette.ButtonText)) 

1611 painter.setPen(xpen) 

1612 painter.drawPixmap( 

1613 0, upper_rect.x(), 

1614 self.get_histogram(upper_projection, upper_rect.height())) 

1615 

1616 if focus_rect and self.tduration: 

1617 painter.drawPixmap( 

1618 0, lower_rect.y(), 

1619 self.get_histogram(lower_projection, lower_rect.height())) 

1620 

1621 # frame_pen = qg.QPen(palette.color(qg.QPalette.ButtonText)) 

1622 # painter.setPen(frame_pen) 

1623 # painter.drawRect(upper_rect) 

1624 # if self.tduration: 

1625 # painter.drawRect(lower_rect) 

1626 

1627 if self._tcursor is not None: 

1628 x = int(round(upper_projection(self._tcursor))) 

1629 painter.drawLine(x, upper_rect.top(), x, upper_rect.bottom()) 

1630 if focus_rect and self.tduration and lower_projection: 

1631 x = int(round(lower_projection(self._tcursor))) 

1632 painter.drawLine(x, lower_rect.top(), x, lower_rect.bottom()) 

1633 

1634 if self._hover_point and lower_rect.contains(self._hover_point) \ 

1635 and not self.tduration and not self._track_start: 

1636 

1637 alpha_brush = palette.highlight() 

1638 color = alpha_brush.color() 

1639 color.setAlpha(30) 

1640 alpha_brush.setColor(color) 

1641 painter.fillRect(lower_rect, alpha_brush) 

1642 

1643 def upper_projection(self): 

1644 p = Projection() 

1645 if None in (self.tmin, self.tmax): 

1646 p.set_in_range(*g_initial_time_range) 

1647 else: 

1648 p.set_in_range(self.tmin, self.tmax) 

1649 

1650 p.set_out_range(0., self.width()) 

1651 return p 

1652 

1653 def lower_projection(self): 

1654 tmin_eff = self.tmin_effective() 

1655 tmax_eff = self.tmax_effective() 

1656 if None in (tmin_eff, tmax_eff): 

1657 return None 

1658 

1659 p = Projection() 

1660 p.set_in_range(tmin_eff, tmax_eff) 

1661 p.set_out_range(0., self.width()) 

1662 return p 

1663 

1664 def tmin_effective(self): 

1665 return tmin_effective( 

1666 self.tmin, self.tmax, self.tduration, self.tposition) 

1667 

1668 def tmax_effective(self): 

1669 return tmax_effective( 

1670 self.tmin, self.tmax, self.tduration, self.tposition) 

1671 

1672 def upper_rect(self): 

1673 vmin = 0 

1674 vmax = self.height() // 3 

1675 umin, umax = 0, self.width() 

1676 return qc.QRect(umin, vmin, umax-umin, vmax-vmin) 

1677 

1678 def lower_rect(self): 

1679 vmin = 2 * self.height() // 3 

1680 vmax = self.height() 

1681 umin, umax = 0, self.width() 

1682 return qc.QRect(umin, vmin, umax-umin, vmax-vmin) 

1683 

1684 def focus_rect(self, projection): 

1685 vmin = 0 

1686 vmax = self.height() // 3 

1687 

1688 tmin_eff = self.tmin_effective() 

1689 tmax_eff = self.tmax_effective() 

1690 if None in (tmin_eff, tmax_eff): 

1691 return None 

1692 

1693 umin = rint(projection(tmin_eff)) 

1694 umax = rint(projection(tmax_eff)) 

1695 

1696 return qc.QRect(umin, vmin, umax-umin+1, vmax-vmin) 

1697 

1698 def set_range(self, tmin, tmax): 

1699 if None in (tmin, tmax): 

1700 tmin = None 

1701 tmax = None 

1702 elif tmin == tmax: 

1703 tmin -= 0.5 

1704 tmax += 0.5 

1705 

1706 self.tmin = tmin 

1707 self.tmax = tmax 

1708 

1709 self.rangeChanged.emit() 

1710 self.update() 

1711 

1712 def get_range(self): 

1713 return self.tmin, self.tmax 

1714 

1715 def set_focus(self, tduration, tposition): 

1716 self.tduration = tduration 

1717 self.tposition = tposition 

1718 self.focusChanged.emit() 

1719 self.update() 

1720 

1721 def get_focus(self): 

1722 return (self.tduration, self.tposition) 

1723 

1724 def get_tcursor(self): 

1725 return self._tcursor 

1726 

1727 def update_data_range(self): 

1728 self.set_range(*self.get_data_range()) 

1729 

1730 def paintEvent(self, paint_ev): 

1731 '' 

1732 painter = qg.QPainter(self) 

1733 painter.setRenderHint(qg.QPainter.Antialiasing) 

1734 self.drawit(painter) 

1735 qw.QFrame.paintEvent(self, paint_ev) 

1736 

1737 def mousePressEvent(self, mouse_ev): 

1738 '' 

1739 if mouse_ev.button() == qc.Qt.LeftButton: 

1740 self.rangeEditPressed.emit() 

1741 

1742 if None in (self.tmin, self.tmax): 

1743 self.set_range(*g_initial_time_range) 

1744 

1745 self._track_start = mouse_ev.x(), mouse_ev.y() 

1746 self._track_range = self.get_range() 

1747 self._track_focus = self.get_focus() 

1748 # upper_projection = self.upper_projection() 

1749 # focus_rect = self.focus_rect(upper_projection) 

1750 upper_rect = self.upper_rect() 

1751 lower_rect = self.lower_rect() 

1752 if upper_rect.contains(mouse_ev.pos()): 

1753 self._track_what = 'global' 

1754 elif lower_rect.contains(mouse_ev.pos()): 

1755 self._track_what = 'focus' 

1756 if self.tduration is None: 

1757 frac = 0.02 

1758 tduration = (self.tmax - self.tmin) * (1.0 - frac) 

1759 tposition = 0.5*frac 

1760 self.set_focus(tduration, tposition) 

1761 

1762 else: 

1763 if self.tduration is not None: 

1764 self._track_what = 'focus_slide' 

1765 else: 

1766 self._track_what = 'global_slide' 

1767 

1768 self.update() 

1769 

1770 def enterEvent(self, ev): 

1771 '' 

1772 self._tcursor = None # is set later by mouseMoveEvent 

1773 self._hover_point = None 

1774 self.tcursorChanged.emit() 

1775 

1776 def leaveEvent(self, ev): 

1777 '' 

1778 self._tcursor = None 

1779 self._hover_point = None 

1780 self.tcursorChanged.emit() 

1781 self.update() 

1782 

1783 def mouseReleaseEvent(self, mouse_ev): 

1784 '' 

1785 if self._track_start: 

1786 self.rangeEditReleased.emit() 

1787 self.update() 

1788 

1789 self._track_start = None 

1790 self._track_range = None 

1791 self._track_focus = None 

1792 self._track_what = None 

1793 if self.tduration is not None: 

1794 if self.tduration >= self.tmax - self.tmin: 

1795 self.set_focus(None, 0.0) 

1796 elif self.tposition < 0.: 

1797 self.set_focus(self.tduration, 0.0) 

1798 elif self.tposition > 1.0 - self.tduration \ 

1799 / (self.tmax - self.tmin): 

1800 self.set_focus( 

1801 self.tduration, 1.0 - self.tduration 

1802 / (self.tmax - self.tmin)) 

1803 

1804 def mouseDoubleClickEvent(self, mouse_ev): 

1805 '' 

1806 if mouse_ev.button() == qc.Qt.LeftButton: 

1807 lower_rect = self.lower_rect() 

1808 if lower_rect.contains(mouse_ev.pos()) \ 

1809 and self.tduration is not None: 

1810 

1811 etmin = self.tmin_effective() 

1812 etmax = self.tmax_effective() 

1813 self.set_range(etmin, etmax) 

1814 self.set_focus(None, 0.0) 

1815 

1816 upper_rect = self.upper_rect() 

1817 if upper_rect.contains(mouse_ev.pos()) \ 

1818 and self.tduration is not None: 

1819 

1820 self.set_focus(None, 0.0) 

1821 

1822 def mouseMoveEvent(self, mouse_ev): 

1823 '' 

1824 point = self.mapFromGlobal(mouse_ev.globalPos()) 

1825 self._hover_point = point 

1826 

1827 if self._track_start is not None: 

1828 x0, y0 = self._track_start 

1829 dx = (point.x() - x0)/float(self.width()) 

1830 dy = (point.y() - y0)/float(self.height()) 

1831 xfrac = x0/float(self.width()) 

1832 tmin0, tmax0 = self._track_range 

1833 tduration0, tposition0 = self._track_focus 

1834 

1835 if self._track_what in ('global', 'global_slide'): 

1836 if self._track_what == 'global': 

1837 scale = math.exp(-dy) 

1838 else: 

1839 scale = 1.0 

1840 

1841 dtr = (tmax0-tmin0) * (scale - 1.0) 

1842 dt = dx*(tmax0-tmin0)*scale 

1843 

1844 tmin = tmin0 - dt - dtr*xfrac 

1845 tmax = tmax0 - dt + dtr*(1.-xfrac) 

1846 

1847 self.set_range(tmin, tmax) 

1848 

1849 tduration, tposition = self._track_focus 

1850 if tduration is not None: 

1851 etmin0 = tmin_effective( 

1852 tmin0, tmax0, tduration0, tposition0) 

1853 

1854 tposition = (etmin0 - tmin) / (tmax - tmin) 

1855 self.set_focus(tduration0, tposition) 

1856 

1857 elif self._track_what == 'focus': 

1858 if tduration0 is not None: 

1859 scale = math.exp(-dy) 

1860 

1861 dtr = tduration0 * (scale - 1.0) 

1862 dt = dx * tduration0 * scale 

1863 

1864 etmin0 = tmin_effective( 

1865 tmin0, tmax0, tduration0, tposition0) 

1866 etmax0 = tmax_effective( 

1867 tmin0, tmax0, tduration0, tposition0) 

1868 

1869 tmin = etmin0 - dt - dtr*xfrac 

1870 tmax = etmax0 - dt + dtr*(1.-xfrac) 

1871 

1872 tduration = tmax - tmin 

1873 

1874 tposition = (tmin - tmin0) / (tmax0 - tmin0) 

1875 tposition = min( 

1876 max(0., tposition), 

1877 1.0 - tduration / (tmax0 - tmin0)) 

1878 

1879 if tduration < (tmax0 - tmin0): 

1880 self.set_focus(tduration, tposition) 

1881 else: 

1882 self.set_focus(None, tposition) 

1883 

1884 else: 

1885 tduration, tposition = tmax0 - tmin0, 0.0 

1886 self.set_focus(tduration, tposition) 

1887 self._track_focus = (tduration, tposition) 

1888 

1889 elif self._track_what == 'focus_slide': 

1890 if tduration0 is not None: 

1891 self.set_focus( 

1892 tduration0, 

1893 min( 

1894 max(0., tposition0 + dx), 

1895 1.0 - tduration0 / (tmax0 - tmin0))) 

1896 

1897 else: 

1898 

1899 upper_rect = self.upper_rect() 

1900 lower_rect = self.lower_rect() 

1901 upper_projection = self.upper_projection() 

1902 lower_projection = self.lower_projection() 

1903 

1904 app = get_app() 

1905 have_focus = lower_projection and self.tduration is not None 

1906 

1907 if upper_rect.contains(point): 

1908 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor)) 

1909 self._tcursor = upper_projection.rev(point.x()) 

1910 app.status( 

1911 'Click and drag to change global time interval. ' 

1912 'Move up/down to zoom.' + ( 

1913 ' Double-click to clear focus time interval.' 

1914 if have_focus else '')) 

1915 

1916 elif lower_rect.contains(point): 

1917 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor)) 

1918 if have_focus: 

1919 self._tcursor = lower_projection.rev(point.x()) 

1920 app.status( 

1921 'Click and drag to change local time interval. ' 

1922 'Double-click to set global time interval from focus.') 

1923 else: 

1924 app.status( 

1925 'Click to activate focus time window.') 

1926 else: 

1927 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor)) 

1928 self._tcursor = None 

1929 if have_focus: 

1930 app.status('Move focus time interval with fixed length.') 

1931 else: 

1932 app.status('Move global time interval with fixed length.') 

1933 

1934 self.update() 

1935 self.tcursorChanged.emit()