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

1319 statements  

« 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---------- 

5 

6import sys 

7import math 

8import time 

9import numpy as num 

10import logging 

11import enum 

12import calendar 

13import signal 

14import weakref 

15 

16from matplotlib.colors import Normalize 

17 

18from .qt_compat import qc, qg, qw 

19 

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

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

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

23from pyrocko import plot, util 

24 

25 

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

27 

28 

29class _Getch: 

30 ''' 

31 Gets a single character from standard input. 

32 

33 Does not echo to the screen. 

34 

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

36 ''' 

37 def __init__(self): 

38 try: 

39 self.impl = _GetchWindows() 

40 except ImportError: 

41 self.impl = _GetchUnix() 

42 

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

44 

45 

46class _GetchUnix: 

47 def __init__(self): 

48 import tty, sys # noqa 

49 

50 def __call__(self): 

51 import sys 

52 import tty 

53 import termios 

54 

55 fd = sys.stdin.fileno() 

56 old_settings = termios.tcgetattr(fd) 

57 try: 

58 tty.setraw(fd) 

59 ch = sys.stdin.read(1) 

60 finally: 

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

62 

63 return ch 

64 

65 

66class _GetchWindows: 

67 def __init__(self): 

68 import msvcrt # noqa 

69 

70 def __call__(self): 

71 import msvcrt 

72 return msvcrt.getch() 

73 

74 

75getch = _Getch() 

76 

77 

78class PyrockoQApplication(qw.QApplication): 

79 

80 def __init__(self): 

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

82 self._main_window = None 

83 

84 def install_sigint_handler(self): 

85 self._old_signal_handler = signal.signal( 

86 signal.SIGINT, 

87 self.request_close_all_windows) 

88 

89 def uninstall_sigint_handler(self): 

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

91 

92 def set_main_window(self, win): 

93 self._main_window = win 

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

95 self.setApplicationName(name) 

96 self.setApplicationDisplayName(name) 

97 self.setDesktopFileName(name) 

98 

99 def unset_main_window(self): 

100 self.set_main_window(None) 

101 

102 def get_main_window(self): 

103 return self._main_window 

104 

105 def get_main_windows(self): 

106 return [self.get_main_window()] 

107 

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

109 win = self.get_main_window() 

110 if not win: 

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

112 else: 

113 win.statusBar().showMessage( 

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

115 

116 def event(self, e): 

117 if isinstance(e, qg.QFileOpenEvent): 

118 path = str(e.file()) 

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

120 wins = self.get_main_windows() 

121 if wins: 

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

123 

124 return True 

125 else: 

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

127 

128 def request_close_all_windows(self, *args): 

129 

130 def confirm(): 

131 try: 

132 print( 

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

134 file=sys.stderr) 

135 

136 confirmed = getch() == 'y' 

137 if not confirmed: 

138 print( 

139 'Continuing.', 

140 file=sys.stderr) 

141 else: 

142 print( 

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

144 file=sys.stderr) 

145 

146 return confirmed 

147 

148 except Exception: 

149 return False 

150 

151 windows = self.get_main_windows() 

152 instant_close = all(win.instant_close for win in windows) 

153 

154 if instant_close or confirm(): 

155 for win in windows: 

156 win.instant_close = True 

157 

158 self.closeAllWindows() 

159 

160 

161app = None 

162 

163 

164def get_app(): 

165 from .qt_compat import qg 

166 try: 

167 global app 

168 if app is None: 

169 qg.QSurfaceFormat.setDefaultFormat(qg.QSurfaceFormat()) 

170 app = PyrockoQApplication() 

171 return app 

172 except NameError: # can happen during shutdown 

173 return None 

174 

175 

176def rint(x): 

177 return int(round(x)) 

178 

179 

180def make_QPolygonF(xdata, ydata): 

181 assert len(xdata) == len(ydata) 

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

183 vptr = qpoints.data() 

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

185 aa = num.ndarray( 

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

187 dtype=num.float64, 

188 buffer=memoryview(vptr)) 

189 aa.setflags(write=True) 

190 aa[:, 0] = xdata 

191 aa[:, 1] = ydata 

192 return qpoints 

193 

194 

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

196 NCOLORS = 512 

197 norm = Normalize() 

198 norm.vmin = vmin 

199 norm.vmax = vmax 

200 

201 return qg.QImage( 

202 plot.mpl_get_cmap(cmap_name)( 

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

204 alpha=None, bytes=True), 

205 NCOLORS, 1, qg.QImage.Format_RGBX8888) 

206 

207 

208class Label(object): 

209 def __init__( 

210 self, p, x, y, label_str, 

211 label_bg=None, 

212 anchor='BL', 

213 outline=False, 

214 font=None, 

215 color=None): 

216 

217 text = qg.QTextDocument() 

218 if font: 

219 text.setDefaultFont(font) 

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

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

222 s = text.size() 

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

224 tx, ty = x, y 

225 

226 if 'B' in anchor: 

227 ty -= rect.height() 

228 if 'R' in anchor: 

229 tx -= rect.width() 

230 if 'M' in anchor: 

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

232 if 'C' in anchor: 

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

234 

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

236 self.rect = rect 

237 self.text = text 

238 self.outline = outline 

239 self.label_bg = label_bg 

240 self.color = color 

241 self.p = p 

242 

243 def draw(self): 

244 p = self.p 

245 rect = self.rect 

246 tx = rect.left() 

247 ty = rect.top() 

248 

249 if self.outline: 

250 oldpen = p.pen() 

251 oldbrush = p.brush() 

252 p.setBrush(self.label_bg) 

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

254 p.drawRect(rect) 

255 p.setPen(oldpen) 

256 p.setBrush(oldbrush) 

257 

258 else: 

259 if self.label_bg: 

260 p.fillRect(rect, self.label_bg) 

261 

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

263 self.text.drawContents(p) 

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

265 

266 

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

268 fm = p.fontMetrics() 

269 

270 label = label_str 

271 rect = fm.boundingRect(label) 

272 

273 tx, ty = x, y 

274 if 'T' in anchor: 

275 ty += rect.height() 

276 if 'R' in anchor: 

277 tx -= rect.width() 

278 if 'M' in anchor: 

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

280 if 'C' in anchor: 

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

282 

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

284 if outline: 

285 oldpen = p.pen() 

286 oldbrush = p.brush() 

287 p.setBrush(label_bg) 

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

289 p.drawRect(rect) 

290 p.setPen(oldpen) 

291 p.setBrush(oldbrush) 

292 

293 else: 

294 p.fillRect(rect, label_bg) 

295 

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

297 

298 

299def get_err_palette(): 

300 err_palette = qg.QPalette() 

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

302 return err_palette 

303 

304 

305class QSliderNoWheel(qw.QSlider): 

306 

307 def wheelEvent(self, ev): 

308 '' 

309 ev.ignore() 

310 

311 def keyPressEvent(self, ev): 

312 '' 

313 ev.ignore() 

314 

315 

316class QSliderFloat(qw.QSlider): 

317 

318 sliderMovedFloat = qc.pyqtSignal(float) 

319 valueChangedFloat = qc.pyqtSignal(float) 

320 rangeChangedFloat = qc.pyqtSignal(float, float) 

321 

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

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

324 self.setMinimum(0) 

325 self.setMaximum(1000) 

326 self.setSingleStep(10) 

327 self.setPageStep(100) 

328 self._fmin = 0. 

329 self._fmax = 1. 

330 self.valueChanged.connect(self._handleValueChanged) 

331 self.sliderMoved.connect(self._handleSliderMoved) 

332 

333 def _f_to_i(self, fval): 

334 fval = float(fval) 

335 imin = self.minimum() 

336 imax = self.maximum() 

337 return max( 

338 imin, 

339 imin + min( 

340 int(round( 

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

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

343 imax)) 

344 

345 def _i_to_f(self, ival): 

346 imin = self.minimum() 

347 imax = self.maximum() 

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

349 / (imax - imin) 

350 

351 def minimumFloat(self): 

352 return self._fmin 

353 

354 def setMinimumFloat(self, fval): 

355 self._fmin = float(fval) 

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

357 

358 def maximumFloat(self): 

359 return self._fmax 

360 

361 def setMaximumFloat(self, fval): 

362 self._fmax = float(fval) 

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

364 

365 def setRangeFloat(self, fmin, fmax): 

366 self._fmin = float(fmin) 

367 self._fmax = float(fmax) 

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

369 

370 def valueFloat(self): 

371 return self._i_to_f(self.value()) 

372 

373 def setValueFloat(self, fval): 

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

375 

376 def _handleValueChanged(self, ival): 

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

378 

379 def _handleSliderMoved(self, ival): 

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

381 

382 

383class MyValueEdit(qw.QLineEdit): 

384 

385 edited = qc.pyqtSignal(float) 

386 

387 def __init__( 

388 self, 

389 low_is_none=False, 

390 high_is_none=False, 

391 low_is_zero=False, 

392 *args, **kwargs): 

393 

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

395 self.value = 0. 

396 self.mi = 0. 

397 self.ma = 1. 

398 self.low_is_none = low_is_none 

399 self.high_is_none = high_is_none 

400 self.low_is_zero = low_is_zero 

401 self.editingFinished.connect( 

402 self.myEditingFinished) 

403 self.lock = False 

404 

405 def setRange(self, mi, ma): 

406 self.mi = mi 

407 self.ma = ma 

408 

409 def setValue(self, value): 

410 if not self.lock: 

411 self.value = value 

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

413 self.adjust_text() 

414 

415 def myEditingFinished(self): 

416 try: 

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

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

419 value = self.mi 

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

421 value = self.ma 

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

423 value = self.mi 

424 else: 

425 value = float(t) 

426 

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

428 raise Exception('out of range') 

429 

430 if value != self.value: 

431 self.value = value 

432 self.lock = True 

433 self.edited.emit(value) 

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

435 except Exception: 

436 self.setPalette(get_err_palette()) 

437 

438 self.lock = False 

439 

440 def adjust_text(self): 

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

442 

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

444 t = '0' 

445 

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

447 if self.high_is_none: 

448 t = 'below' 

449 else: 

450 t = 'off' 

451 

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

453 if self.low_is_none: 

454 t = 'above' 

455 else: 

456 t = 'off' 

457 

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

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

460 else: 

461 self.setStyleSheet(None) 

462 

463 self.setText(t) 

464 

465 

466class ValControl(qw.QWidget): 

467 

468 valchange = qc.pyqtSignal(object, int) 

469 

470 def __init__( 

471 self, 

472 low_is_none=False, 

473 high_is_none=False, 

474 low_is_zero=False, 

475 type=float, 

476 *args): 

477 

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

479 

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

481 self.lname.setSizePolicy( 

482 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

483 self.lvalue = MyValueEdit( 

484 low_is_none=low_is_none, 

485 high_is_none=high_is_none, 

486 low_is_zero=low_is_zero) 

487 self.lvalue.setFixedWidth(100) 

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

489 self.slider.setSizePolicy( 

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

491 self.slider.setMaximum(10000) 

492 self.slider.setSingleStep(100) 

493 self.slider.setPageStep(1000) 

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

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

496 

497 self.low_is_none = low_is_none 

498 self.high_is_none = high_is_none 

499 self.low_is_zero = low_is_zero 

500 

501 self.slider.valueChanged.connect( 

502 self.slided) 

503 self.lvalue.edited.connect( 

504 self.edited) 

505 

506 self.type = type 

507 self.mute = False 

508 

509 def widgets(self): 

510 return self.lname, self.lvalue, self.slider 

511 

512 def s2v(self, svalue): 

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

514 return 0 

515 

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

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

518 value = self.type(value) 

519 return value 

520 

521 def v2s(self, value): 

522 value = self.type(value) 

523 

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

525 return 0 

526 

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

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

529 

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

531 self.lname.setText(name) 

532 self.mi = mi 

533 self.ma = ma 

534 self.ind = ind 

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

536 self.set_value(cur) 

537 

538 def set_range(self, mi, ma): 

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

540 return 

541 

542 vput = None 

543 if self.cursl == 0: 

544 vput = mi 

545 if self.cursl == 10000: 

546 vput = ma 

547 

548 self.mi = mi 

549 self.ma = ma 

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

551 

552 if vput is not None: 

553 self.set_value(vput) 

554 else: 

555 if self.cur < mi: 

556 self.set_value(mi) 

557 if self.cur > ma: 

558 self.set_value(ma) 

559 

560 def set_value(self, cur): 

561 if cur is None: 

562 if self.low_is_none: 

563 cur = self.mi 

564 elif self.high_is_none: 

565 cur = self.ma 

566 

567 if cur == 0.0: 

568 if self.low_is_zero: 

569 cur = self.mi 

570 

571 self.mute = True 

572 self.cur = cur 

573 self.cursl = self.v2s(cur) 

574 self.slider.blockSignals(True) 

575 self.slider.setValue(self.cursl) 

576 self.slider.blockSignals(False) 

577 self.lvalue.blockSignals(True) 

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

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

580 else: 

581 self.lvalue.setValue(self.cur) 

582 self.lvalue.blockSignals(False) 

583 self.mute = False 

584 

585 def set_tracking(self, tracking): 

586 self.slider.setTracking(tracking) 

587 

588 def get_value(self): 

589 return self.cur 

590 

591 def slided(self, val): 

592 if self.cursl != val: 

593 self.cursl = val 

594 cur = self.s2v(self.cursl) 

595 

596 if cur != self.cur: 

597 self.cur = cur 

598 self.lvalue.blockSignals(True) 

599 self.lvalue.setValue(self.cur) 

600 self.lvalue.blockSignals(False) 

601 self.fire_valchange() 

602 

603 def edited(self, val): 

604 if self.cur != val: 

605 self.cur = val 

606 cursl = self.v2s(val) 

607 if (cursl != self.cursl): 

608 self.slider.blockSignals(True) 

609 self.slider.setValue(cursl) 

610 self.slider.blockSignals(False) 

611 self.cursl = cursl 

612 

613 self.fire_valchange() 

614 

615 def fire_valchange(self): 

616 

617 if self.mute: 

618 return 

619 

620 cur = self.cur 

621 

622 if self.cursl == 0: 

623 if self.low_is_none: 

624 cur = None 

625 

626 elif self.low_is_zero: 

627 cur = 0.0 

628 

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

630 cur = None 

631 

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

633 

634 

635class LinValControl(ValControl): 

636 

637 def s2v(self, svalue): 

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

639 value = self.type(value) 

640 return value 

641 

642 def v2s(self, value): 

643 value = self.type(value) 

644 if self.ma == self.mi: 

645 return 0 

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

647 

648 

649class ColorbarControl(qw.QWidget): 

650 

651 AVAILABLE_CMAPS = ( 

652 'viridis', 

653 'plasma', 

654 'magma', 

655 'binary', 

656 'Reds', 

657 'copper', 

658 'seismic', 

659 'RdBu', 

660 'YlGn', 

661 ) 

662 

663 DEFAULT_CMAP = 'viridis' 

664 

665 cmap_changed = qc.pyqtSignal(str) 

666 show_absolute_toggled = qc.pyqtSignal(bool) 

667 show_integrate_toggled = qc.pyqtSignal(bool) 

668 

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

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

671 

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

673 self.lname.setSizePolicy( 

674 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

675 

676 self.cmap_options = qw.QComboBox() 

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

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

679 pixmap = qg.QPixmap.fromImage( 

680 get_colormap_qimage(cmap)) 

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

682 

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

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

685 

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

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

688 self.cmap_options.setSizePolicy( 

689 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)) 

690 

691 self.colorslider = ColorbarSlider(self) 

692 self.colorslider.setSizePolicy( 

693 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag, 

694 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag 

695 ) 

696 self.clip_changed = self.colorslider.clip_changed 

697 

698 btn_size = qw.QSizePolicy( 

699 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag, 

700 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag) 

701 

702 self.symetry_toggle = qw.QPushButton() 

703 self.symetry_toggle.setIcon( 

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

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

706 self.symetry_toggle.setSizePolicy(btn_size) 

707 self.symetry_toggle.setCheckable(True) 

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

709 self.symetry_toggle.setChecked(True) 

710 

711 self.reverse_toggle = qw.QPushButton() 

712 self.reverse_toggle.setIcon( 

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

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

715 self.reverse_toggle.setSizePolicy(btn_size) 

716 self.reverse_toggle.setCheckable(True) 

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

718 

719 self.abs_toggle = qw.QPushButton() 

720 self.abs_toggle.setIcon( 

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

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

723 self.abs_toggle.setSizePolicy(btn_size) 

724 self.abs_toggle.setCheckable(True) 

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

726 

727 self.int_toggle = qw.QPushButton() 

728 self.int_toggle.setText('∫') 

729 self.int_toggle.setToolTip( 

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

731 self.int_toggle.setSizePolicy(btn_size) 

732 self.int_toggle.setCheckable(True) 

733 self.int_toggle.setMaximumSize( 

734 24, 

735 self.int_toggle.maximumSize().height()) 

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

737 

738 v_splitter = qw.QFrame() 

739 v_splitter.setFrameShape(qw.QFrame.VLine) 

740 v_splitter.setFrameShadow(qw.QFrame.Sunken) 

741 

742 self.controls = qw.QWidget() 

743 layout = qw.QHBoxLayout() 

744 layout.addWidget(self.colorslider) 

745 layout.addWidget(self.symetry_toggle) 

746 layout.addWidget(self.reverse_toggle) 

747 layout.addWidget(v_splitter) 

748 layout.addWidget(self.abs_toggle) 

749 layout.addWidget(self.int_toggle) 

750 self.controls.setLayout(layout) 

751 

752 self.set_cmap_name(self.DEFAULT_CMAP) 

753 

754 def set_cmap(self, idx): 

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

756 

757 def set_cmap_name(self, cmap_name): 

758 self.cmap_name = cmap_name 

759 self.colorslider.set_cmap_name(cmap_name) 

760 self.cmap_changed.emit(cmap_name) 

761 

762 def get_cmap(self): 

763 return self.cmap_name 

764 

765 def toggle_symetry(self, toggled): 

766 self.colorslider.set_symetry(toggled) 

767 

768 def toggle_reverse_cmap(self): 

769 cmap = self.get_cmap() 

770 if cmap.endswith('_r'): 

771 r_cmap = cmap.rstrip('_r') 

772 else: 

773 r_cmap = cmap + '_r' 

774 self.set_cmap_name(r_cmap) 

775 

776 def toggle_absolute(self, toggled): 

777 self.symetry_toggle.setChecked(not toggled) 

778 self.show_absolute_toggled.emit(toggled) 

779 

780 def widgets(self): 

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

782 

783 

784class ColorbarSlider(qw.QWidget): 

785 DEFAULT_CMAP = 'viridis' 

786 CORNER_THRESHOLD = 10 

787 MIN_WIDTH = .05 

788 

789 clip_changed = qc.pyqtSignal(float, float) 

790 

791 class COMPONENTS(enum.Enum): 

792 LeftLine = 1 

793 RightLine = 2 

794 Center = 3 

795 

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

797 super().__init__() 

798 self.cmap_name = cmap_name or self.DEFAULT_CMAP 

799 self.clip_min = 0. 

800 self.clip_max = 1. 

801 

802 self._sym_locked = True 

803 self._mouse_inside = False 

804 self._window = None 

805 self._old_pos = None 

806 self._component_grabbed = None 

807 

808 self.setMouseTracking(True) 

809 

810 def set_cmap_name(self, cmap_name): 

811 self.cmap_name = cmap_name 

812 self.repaint() 

813 

814 def get_cmap_name(self): 

815 return self.cmap_name 

816 

817 def set_symetry(self, symetry): 

818 self._sym_locked = symetry 

819 if self._sym_locked: 

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

821 clip_min = 1. - clip_max 

822 self.set_clip(clip_min, clip_max) 

823 

824 def _set_window(self, window): 

825 self._window = window 

826 

827 def _get_left_line(self): 

828 rect = self._get_active_rect() 

829 if not rect: 

830 return 

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

832 

833 def _get_right_line(self): 

834 rect = self._get_active_rect() 

835 if not rect: 

836 return 

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

838 

839 def _get_active_rect(self): 

840 if not self._window: 

841 return 

842 rect = qc.QRect(self._window) 

843 width = rect.width() 

844 rect.setLeft(int(width * self.clip_min)) 

845 rect.setRight(int(width * self.clip_max)) 

846 return rect 

847 

848 def set_clip(self, clip_min, clip_max): 

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

850 return 

851 if clip_max - clip_min < self.MIN_WIDTH: 

852 return 

853 

854 self.clip_min = clip_min 

855 self.clip_max = clip_max 

856 self.repaint() 

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

858 

859 def mousePressEvent(self, event): 

860 '' 

861 act_rect = self._get_active_rect() 

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

863 self._component_grabbed = None 

864 return 

865 

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

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

868 

869 if 0 < dist_left < self.CORNER_THRESHOLD: 

870 self._component_grabbed = self.COMPONENTS.LeftLine 

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

872 elif 0 < dist_right < self.CORNER_THRESHOLD: 

873 self._component_grabbed = self.COMPONENTS.RightLine 

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

875 else: 

876 self.setCursor(qg.QCursor()) 

877 

878 def mouseReleaseEvent(self, event): 

879 '' 

880 self._component_grabbed = None 

881 self.repaint() 

882 

883 def mouseDoubleClickEvent(self, event): 

884 '' 

885 self.set_clip(0., 1.) 

886 

887 def wheelEvent(self, event): 

888 '' 

889 event.accept() 

890 if not self._sym_locked: 

891 return 

892 

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

894 delta = -delta / 5e3 

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

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

897 self._mouse_inside = True 

898 self.set_clip(clip_min_new, clip_max_new) 

899 

900 def mouseMoveEvent(self, event): 

901 '' 

902 act_rect = self._get_active_rect() 

903 

904 if not self._component_grabbed: 

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

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

907 

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

909 0 <= dist_right < self.CORNER_THRESHOLD: 

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

911 else: 

912 self.setCursor(qg.QCursor()) 

913 

914 if self._old_pos and self._component_grabbed: 

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

916 

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

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

919 clip_max_new = \ 

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

921 if self._sym_locked else self.clip_max 

922 

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

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

925 clip_min_new = \ 

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

927 if self._sym_locked else self.clip_min 

928 

929 self.set_clip(clip_min_new, clip_max_new) 

930 

931 self._old_pos = event.pos() 

932 

933 def enterEvent(self, e): 

934 '' 

935 self._mouse_inside = True 

936 self.repaint() 

937 

938 def leaveEvent(self, e): 

939 '' 

940 self._mouse_inside = False 

941 self.repaint() 

942 

943 def paintEvent(self, e): 

944 '' 

945 p = qg.QPainter(self) 

946 self._set_window(p.window()) 

947 

948 p.drawImage( 

949 p.window(), 

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

951 

952 left_line = self._get_left_line() 

953 right_line = self._get_right_line() 

954 

955 pen = qg.QPen() 

956 pen.setWidth(2) 

957 pen.setStyle(qc.Qt.DotLine) 

958 pen.setBrush(qc.Qt.white) 

959 p.setPen(pen) 

960 p.setCompositionMode( 

961 qg.QPainter.CompositionMode.CompositionMode_Difference) 

962 

963 p.drawLine(left_line) 

964 p.drawLine(right_line) 

965 

966 label_rect = self._get_active_rect() 

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

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

969 label_left_rect = qc.QRectF(label_rect) 

970 label_right_rect = qc.QRectF(label_rect) 

971 label_left_align = qc.Qt.AlignLeft 

972 label_right_align = qc.Qt.AlignRight 

973 

974 if label_rect.left() > 50: 

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

976 label_left_rect.setLeft(0) 

977 label_left_align = qc.Qt.AlignRight 

978 

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

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

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

982 label_right_align = qc.Qt.AlignLeft 

983 

984 if self._mouse_inside or self._component_grabbed: 

985 p.drawText( 

986 label_left_rect, 

987 label_left_align | qc.Qt.AlignVCenter, 

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

989 p.drawText( 

990 label_right_rect, 

991 label_right_align | qc.Qt.AlignVCenter, 

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

993 

994 

995class Progressbar(object): 

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

997 self.parent = parent 

998 self.name = name 

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

1000 self.pbar = qw.QProgressBar(parent) 

1001 self.aborted = False 

1002 self.time_last_update = 0. 

1003 if can_abort: 

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

1005 self.abort_button.clicked.connect( 

1006 self.abort) 

1007 else: 

1008 self.abort_button = None 

1009 

1010 def widgets(self): 

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

1012 if self.abort_button: 

1013 widgets.append(self.abort_button) 

1014 return widgets 

1015 

1016 def bar(self): 

1017 return self.pbar 

1018 

1019 def abort(self): 

1020 self.aborted = True 

1021 

1022 

1023class Progressbars(qw.QFrame): 

1024 def __init__(self, parent): 

1025 qw.QFrame.__init__(self, parent) 

1026 self.layout = qw.QGridLayout() 

1027 self.setLayout(self.layout) 

1028 self.bars = {} 

1029 self.start_times = {} 

1030 self.hide() 

1031 

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

1033 value = int(round(value)) 

1034 now = time.time() 

1035 if name not in self.start_times: 

1036 self.start_times[name] = now 

1037 if not force: 

1038 return False 

1039 else: 

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

1041 if value == 100: 

1042 del self.start_times[name] 

1043 if not force: 

1044 return False 

1045 

1046 self.start_times.get(name, 0.0) 

1047 if name not in self.bars: 

1048 if value == 100: 

1049 return False 

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

1051 self.make_layout() 

1052 

1053 bar = self.bars[name] 

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

1055 bar.bar().setValue(value) 

1056 bar.time_last_update = now 

1057 

1058 if value == 100: 

1059 del self.bars[name] 

1060 if name in self.start_times: 

1061 del self.start_times[name] 

1062 self.make_layout() 

1063 for w in bar.widgets(): 

1064 w.setParent(None) 

1065 

1066 return bar.aborted 

1067 

1068 def make_layout(self): 

1069 while True: 

1070 c = self.layout.takeAt(0) 

1071 if c is None: 

1072 break 

1073 

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

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

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

1077 

1078 if not self.bars: 

1079 self.hide() 

1080 else: 

1081 self.show() 

1082 

1083 

1084def tohex(c): 

1085 return '%02x%02x%02x' % c 

1086 

1087 

1088def to01(c): 

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

1090 

1091 

1092def beautify_axes(axes): 

1093 try: 

1094 from cycler import cycler 

1095 axes.set_prop_cycle( 

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

1097 

1098 except (ImportError, KeyError): 

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

1100 

1101 xa = axes.get_xaxis() 

1102 ya = axes.get_yaxis() 

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

1104 if hasattr(xa, attr): 

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

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

1107 break 

1108 

1109 

1110class FigureFrame(qw.QFrame): 

1111 ''' 

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

1113 ''' 

1114 

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

1116 qw.QFrame.__init__(self, parent) 

1117 fgcolor = plot.tango_colors['aluminium5'] 

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

1119 

1120 font = qg.QFont() 

1121 font.setBold(True) 

1122 fontsize = font.pointSize() 

1123 

1124 import matplotlib 

1125 matplotlib.rcdefaults() 

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

1127 

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

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

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

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

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

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

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

1135 

1136 matplotlib.rc( 

1137 'font', 

1138 family='sans-serif', 

1139 weight='bold', 

1140 size=fontsize, 

1141 **{'sans-serif': [ 

1142 font.family(), 

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

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

1145 

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

1147 

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

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

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

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

1152 

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

1154 matplotlib.rc( 

1155 'axes', 

1156 facecolor='white', 

1157 edgecolor=tohex(fgcolor), 

1158 labelcolor=tohex(fgcolor)) 

1159 

1160 try: 

1161 from cycler import cycler 

1162 matplotlib.rc( 

1163 'axes', prop_cycle=cycler( 

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

1165 

1166 except (ImportError, KeyError): 

1167 try: 

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

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

1170 

1171 except KeyError: 

1172 pass 

1173 

1174 try: 

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

1176 except KeyError: 

1177 pass 

1178 

1179 try: 

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

1181 except KeyError: 

1182 pass 

1183 

1184 if figure_cls is None: 

1185 from matplotlib.figure import Figure 

1186 figure_cls = Figure 

1187 

1188 from matplotlib.backends.backend_qt5agg import \ 

1189 NavigationToolbar2QT as NavigationToolbar 

1190 

1191 from matplotlib.backends.backend_qt5agg \ 

1192 import FigureCanvasQTAgg as FigureCanvas 

1193 

1194 layout = qw.QGridLayout() 

1195 layout.setContentsMargins(0, 0, 0, 0) 

1196 self.setLayout(layout) 

1197 

1198 canvas_frame = qw.QFrame() 

1199 canvas_frame_layout = qw.QHBoxLayout() 

1200 canvas_frame_layout.setContentsMargins(0, 0, 0, 0) 

1201 canvas_frame.setLayout(canvas_frame_layout) 

1202 canvas_frame.setFrameShape(qw.QFrame.StyledPanel) 

1203 

1204 self.figure = figure_cls(dpi=dpi) 

1205 self.canvas = FigureCanvas(self.figure) 

1206 canvas_frame_layout.addWidget(self.canvas) 

1207 self.canvas.setSizePolicy( 

1208 qw.QSizePolicy( 

1209 qw.QSizePolicy.Expanding, 

1210 qw.QSizePolicy.Expanding)) 

1211 toolbar_frame = qw.QFrame() 

1212 toolbar_frame.setFrameShape(qw.QFrame.StyledPanel) 

1213 toolbar_frame_layout = qw.QHBoxLayout() 

1214 toolbar_frame_layout.setContentsMargins(0, 0, 0, 0) 

1215 toolbar_frame.setLayout(toolbar_frame_layout) 

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

1217 layout.addWidget(canvas_frame, 0, 0) 

1218 toolbar_frame_layout.addWidget(self.toolbar) 

1219 layout.addWidget(toolbar_frame, 1, 0) 

1220 self.closed = False 

1221 

1222 def gca(self): 

1223 axes = self.figure.gca() 

1224 beautify_axes(axes) 

1225 return axes 

1226 

1227 def gcf(self): 

1228 return self.figure 

1229 

1230 def draw(self): 

1231 ''' 

1232 Draw with AGG, then queue for Qt update. 

1233 ''' 

1234 self.canvas.draw() 

1235 

1236 def closeEvent(self, ev): 

1237 self.closed = True 

1238 

1239 

1240class SmartplotFrame(FigureFrame): 

1241 ''' 

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

1243 ''' 

1244 

1245 def __init__( 

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

1247 

1248 from pyrocko.plot import smartplot 

1249 

1250 FigureFrame.__init__( 

1251 self, 

1252 parent=parent, 

1253 figure_cls=smartplot.SmartplotFigure) 

1254 

1255 if plot_cls is None: 

1256 plot_cls = smartplot.Plot 

1257 

1258 self.plot = plot_cls( 

1259 *plot_args, 

1260 fig=self.figure, 

1261 call_mpl_init=False, 

1262 **plot_kwargs) 

1263 

1264 

1265class WebKitFrame(qw.QFrame): 

1266 ''' 

1267 A widget to present a html page using WebKit. 

1268 ''' 

1269 

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

1271 from pyrocko.deps import require 

1272 require('PyQt5.QtWebEngine') 

1273 from PyQt5.QtWebEngineWidgets import QWebEngineView as WebView 

1274 

1275 qw.QFrame.__init__(self, parent) 

1276 layout = qw.QGridLayout() 

1277 layout.setContentsMargins(0, 0, 0, 0) 

1278 layout.setSpacing(0) 

1279 self.setLayout(layout) 

1280 self.web_widget = WebView() 

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

1282 if url: 

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

1284 

1285 

1286class VTKFrame(qw.QFrame): 

1287 ''' 

1288 A widget to present a VTK visualization. 

1289 ''' 

1290 

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

1292 import vtk 

1293 from vtk.qt.QVTKRenderWindowInteractor import \ 

1294 QVTKRenderWindowInteractor 

1295 

1296 qw.QFrame.__init__(self, parent) 

1297 layout = qw.QGridLayout() 

1298 layout.setContentsMargins(0, 0, 0, 0) 

1299 layout.setSpacing(0) 

1300 

1301 self.setLayout(layout) 

1302 

1303 self.vtk_widget = QVTKRenderWindowInteractor(self) 

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

1305 

1306 self.renderer = vtk.vtkRenderer() 

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

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

1309 

1310 if actors: 

1311 for a in actors: 

1312 self.renderer.AddActor(a) 

1313 

1314 def init(self): 

1315 self.iren.Initialize() 

1316 

1317 def add_actor(self, actor): 

1318 self.renderer.AddActor(actor) 

1319 

1320 

1321class PixmapFrame(qw.QLabel): 

1322 ''' 

1323 A widget to preset a pixmap image. 

1324 ''' 

1325 

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

1327 

1328 qw.QLabel.__init__(self, parent) 

1329 self.setAlignment(qc.Qt.AlignCenter) 

1330 self.setContentsMargins(0, 0, 0, 0) 

1331 self.menu = qw.QMenu(self) 

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

1333 action.triggered.connect(self.save_pixmap) 

1334 self.menu.addAction(action) 

1335 

1336 if filename: 

1337 self.load_pixmap(filename) 

1338 

1339 def contextMenuEvent(self, event): 

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

1341 

1342 def load_pixmap(self, filename): 

1343 self.pixmap = qg.QPixmap(filename) 

1344 self.setPixmap(self.pixmap) 

1345 

1346 def save_pixmap(self, filename=None): 

1347 if not filename: 

1348 filename, _ = qw.QFileDialog.getSaveFileName( 

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

1350 self.pixmap.save(filename) 

1351 

1352 

1353class Projection(object): 

1354 def __init__(self): 

1355 self.xr = 0., 1. 

1356 self.ur = 0., 1. 

1357 

1358 def set_in_range(self, xmin, xmax): 

1359 if xmax == xmin: 

1360 xmax = xmin + 1. 

1361 

1362 self.xr = xmin, xmax 

1363 

1364 def get_in_range(self): 

1365 return self.xr 

1366 

1367 def set_out_range(self, umin, umax): 

1368 if umax == umin: 

1369 umax = umin + 1. 

1370 

1371 self.ur = umin, umax 

1372 

1373 def get_out_range(self): 

1374 return self.ur 

1375 

1376 def __call__(self, x): 

1377 umin, umax = self.ur 

1378 xmin, xmax = self.xr 

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

1380 

1381 def clipped(self, x): 

1382 umin, umax = self.ur 

1383 xmin, xmax = self.xr 

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

1385 

1386 def rev(self, u): 

1387 umin, umax = self.ur 

1388 xmin, xmax = self.xr 

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

1390 

1391 

1392class NoData(Exception): 

1393 pass 

1394 

1395 

1396g_working_system_time_range = util.get_working_system_time_range() 

1397 

1398g_initial_time_range = [] 

1399 

1400try: 

1401 g_initial_time_range.append( 

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

1403except Exception: 

1404 g_initial_time_range.append(g_working_system_time_range[0]) 

1405 

1406try: 

1407 g_initial_time_range.append( 

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

1409except Exception: 

1410 g_initial_time_range.append(g_working_system_time_range[1]) 

1411 

1412 

1413def four_way_arrow(position, size): 

1414 r = 5. 

1415 w = 1. 

1416 

1417 points = [ 

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

1419 for (a, b) in [ 

1420 (0, r), 

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

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

1423 (0.5*w, 0.5*w), 

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

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

1426 (r, 0), 

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

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

1429 (0.5*w, -0.5*w), 

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

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

1432 (0, -r), 

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

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

1435 (-0.5*w, -0.5*w), 

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

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

1438 (-r, 0), 

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

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

1441 (-0.5*w, 0.5*w), 

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

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

1444 

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

1446 for ipoint, point in enumerate(points): 

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

1448 

1449 return poly 

1450 

1451 

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

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

1454 return tmin 

1455 else: 

1456 return tmin + (tmax - tmin) * tposition 

1457 

1458 

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

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

1461 return tmax 

1462 else: 

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

1464 

1465 

1466class RangeEdit(qw.QFrame): 

1467 

1468 rangeChanged = qc.pyqtSignal() 

1469 focusChanged = qc.pyqtSignal() 

1470 tcursorChanged = qc.pyqtSignal() 

1471 rangeEditPressed = qc.pyqtSignal() 

1472 rangeEditReleased = qc.pyqtSignal() 

1473 

1474 def __init__(self, parent=None): 

1475 qw.QFrame.__init__(self, parent) 

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

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

1478 # self.setAutoFillBackground(True) 

1479 self.setMouseTracking(True) 

1480 poli = qw.QSizePolicy( 

1481 qw.QSizePolicy.Expanding, 

1482 qw.QSizePolicy.Fixed) 

1483 

1484 self.setSizePolicy(poli) 

1485 self.setMinimumSize(100, 3*24) 

1486 

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

1488 

1489 self._track_start = None 

1490 self._track_range = None 

1491 self._track_focus = None 

1492 self._track_what = None 

1493 

1494 self._tcursor = None 

1495 self._hover_point = None 

1496 

1497 self._provider = None 

1498 self.tmin, self.tmax = None, None 

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

1500 

1501 def set_data_provider(self, provider): 

1502 self._provider = provider 

1503 

1504 def set_data_name(self, name): 

1505 self._data_name = name 

1506 

1507 def sizeHint(self): 

1508 '' 

1509 return self._size_hint 

1510 

1511 def get_data_range(self): 

1512 if self._provider: 

1513 vals = [] 

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

1515 vals.append(data.min()) 

1516 vals.append(data.max()) 

1517 

1518 if vals: 

1519 return min(vals), max(vals) 

1520 

1521 return None, None 

1522 

1523 def get_histogram(self, projection, h): 

1524 h = int(h) 

1525 umin_w, umax_w = projection.get_out_range() 

1526 tmin_w, tmax_w = projection.get_in_range() 

1527 nbins = int(umax_w - umin_w) 

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

1529 if self._provider: 

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

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

1532 .astype(int) 

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

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

1535 

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

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

1538 for i in range(h): 

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

1540 

1541 try: 

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

1543 except TypeError: 

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

1545 bitmap = num.packbits( 

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

1547 axis=1) 

1548 

1549 return qg.QBitmap.fromData( 

1550 qc.QSize(nbins, h), 

1551 bitmap.tobytes(), 

1552 qg.QImage.Format_MonoLSB) 

1553 

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

1555 

1556 palette = self.palette() 

1557 alpha_brush = palette.highlight() 

1558 color = alpha_brush.color() 

1559 # color.setAlpha(60) 

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

1561 

1562 tmin, tmax = projection.get_in_range() 

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

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

1565 

1566 for tick_time in tick_times: 

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

1568 painter.drawLine( 

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

1570 

1571 def drawit(self, painter): 

1572 

1573 palette = self.palette() 

1574 

1575 upper_projection = self.upper_projection() 

1576 lower_projection = self.lower_projection() 

1577 

1578 upper_rect = self.upper_rect() 

1579 lower_rect = self.lower_rect() 

1580 focus_rect = self.focus_rect(upper_projection) 

1581 

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

1583 painter.fillRect(upper_rect, fill_brush) 

1584 

1585 if focus_rect: 

1586 painter.setBrush(palette.light()) 

1587 poly = qg.QPolygon(8) 

1588 poly.setPoint( 

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

1590 poly.setPoint( 

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

1592 poly.setPoint( 

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

1594 lower_rect.y() + lower_rect.height()) 

1595 poly.setPoint( 

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

1597 poly.setPoint( 

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

1599 upper_rect.y() + upper_rect.height()) 

1600 poly.setPoint( 

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

1602 poly.setPoint( 

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

1604 poly.setPoint( 

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

1606 painter.drawPolygon(poly) 

1607 else: 

1608 fill_brush = palette.light() 

1609 painter.fillRect(upper_rect, fill_brush) 

1610 

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

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

1613 # painter.drawPolygon(poly) 

1614 

1615 self.draw_time_ticks(painter, upper_projection, upper_rect) 

1616 if focus_rect and self.tduration: 

1617 self.draw_time_ticks(painter, lower_projection, lower_rect) 

1618 

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

1620 painter.setPen(xpen) 

1621 painter.drawPixmap( 

1622 0, upper_rect.x(), 

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

1624 

1625 if focus_rect and self.tduration: 

1626 painter.drawPixmap( 

1627 0, lower_rect.y(), 

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

1629 

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

1631 # painter.setPen(frame_pen) 

1632 # painter.drawRect(upper_rect) 

1633 # if self.tduration: 

1634 # painter.drawRect(lower_rect) 

1635 

1636 if self._tcursor is not None: 

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

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

1639 if focus_rect and self.tduration and lower_projection: 

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

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

1642 

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

1644 and not self.tduration and not self._track_start: 

1645 

1646 alpha_brush = palette.highlight() 

1647 color = alpha_brush.color() 

1648 color.setAlpha(30) 

1649 alpha_brush.setColor(color) 

1650 painter.fillRect(lower_rect, alpha_brush) 

1651 

1652 def upper_projection(self): 

1653 p = Projection() 

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

1655 p.set_in_range(*g_initial_time_range) 

1656 else: 

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

1658 

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

1660 return p 

1661 

1662 def lower_projection(self): 

1663 tmin_eff = self.tmin_effective() 

1664 tmax_eff = self.tmax_effective() 

1665 if None in (tmin_eff, tmax_eff): 

1666 return None 

1667 

1668 p = Projection() 

1669 p.set_in_range(tmin_eff, tmax_eff) 

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

1671 return p 

1672 

1673 def tmin_effective(self): 

1674 return tmin_effective( 

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

1676 

1677 def tmax_effective(self): 

1678 return tmax_effective( 

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

1680 

1681 def upper_rect(self): 

1682 vmin = 0 

1683 vmax = self.height() // 3 

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

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

1686 

1687 def lower_rect(self): 

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

1689 vmax = self.height() 

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

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

1692 

1693 def focus_rect(self, projection): 

1694 vmin = 0 

1695 vmax = self.height() // 3 

1696 

1697 tmin_eff = self.tmin_effective() 

1698 tmax_eff = self.tmax_effective() 

1699 if None in (tmin_eff, tmax_eff): 

1700 return None 

1701 

1702 umin = rint(projection(tmin_eff)) 

1703 umax = rint(projection(tmax_eff)) 

1704 

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

1706 

1707 def set_range(self, tmin, tmax): 

1708 if None in (tmin, tmax): 

1709 tmin = None 

1710 tmax = None 

1711 elif tmin == tmax: 

1712 tmin -= 0.5 

1713 tmax += 0.5 

1714 

1715 self.tmin = tmin 

1716 self.tmax = tmax 

1717 

1718 self.rangeChanged.emit() 

1719 self.update() 

1720 

1721 def get_range(self): 

1722 return self.tmin, self.tmax 

1723 

1724 def set_focus(self, tduration, tposition): 

1725 self.tduration = tduration 

1726 self.tposition = tposition 

1727 self.focusChanged.emit() 

1728 self.update() 

1729 

1730 def get_focus(self): 

1731 return (self.tduration, self.tposition) 

1732 

1733 def get_tcursor(self): 

1734 return self._tcursor 

1735 

1736 def update_data_range(self): 

1737 self.set_range(*self.get_data_range()) 

1738 

1739 def paintEvent(self, paint_ev): 

1740 '' 

1741 painter = qg.QPainter(self) 

1742 painter.setRenderHint(qg.QPainter.Antialiasing) 

1743 self.drawit(painter) 

1744 qw.QFrame.paintEvent(self, paint_ev) 

1745 

1746 def mousePressEvent(self, mouse_ev): 

1747 '' 

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

1749 self.rangeEditPressed.emit() 

1750 

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

1752 self.set_range(*g_initial_time_range) 

1753 

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

1755 self._track_range = self.get_range() 

1756 self._track_focus = self.get_focus() 

1757 # upper_projection = self.upper_projection() 

1758 # focus_rect = self.focus_rect(upper_projection) 

1759 upper_rect = self.upper_rect() 

1760 lower_rect = self.lower_rect() 

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

1762 self._track_what = 'global' 

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

1764 self._track_what = 'focus' 

1765 if self.tduration is None: 

1766 frac = 0.02 

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

1768 tposition = 0.5*frac 

1769 self.set_focus(tduration, tposition) 

1770 

1771 else: 

1772 if self.tduration is not None: 

1773 self._track_what = 'focus_slide' 

1774 else: 

1775 self._track_what = 'global_slide' 

1776 

1777 self.update() 

1778 

1779 def enterEvent(self, ev): 

1780 '' 

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

1782 self._hover_point = None 

1783 self.tcursorChanged.emit() 

1784 

1785 def leaveEvent(self, ev): 

1786 '' 

1787 self._tcursor = None 

1788 self._hover_point = None 

1789 self.tcursorChanged.emit() 

1790 self.update() 

1791 

1792 def mouseReleaseEvent(self, mouse_ev): 

1793 '' 

1794 if self._track_start: 

1795 self.rangeEditReleased.emit() 

1796 self.update() 

1797 

1798 self._track_start = None 

1799 self._track_range = None 

1800 self._track_focus = None 

1801 self._track_what = None 

1802 if self.tduration is not None: 

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

1804 self.set_focus(None, 0.0) 

1805 elif self.tposition < 0.: 

1806 self.set_focus(self.tduration, 0.0) 

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

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

1809 self.set_focus( 

1810 self.tduration, 1.0 - self.tduration 

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

1812 

1813 def mouseDoubleClickEvent(self, mouse_ev): 

1814 '' 

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

1816 lower_rect = self.lower_rect() 

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

1818 and self.tduration is not None: 

1819 

1820 etmin = self.tmin_effective() 

1821 etmax = self.tmax_effective() 

1822 self.set_range(etmin, etmax) 

1823 self.set_focus(None, 0.0) 

1824 

1825 upper_rect = self.upper_rect() 

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

1827 and self.tduration is not None: 

1828 

1829 self.set_focus(None, 0.0) 

1830 

1831 def mouseMoveEvent(self, mouse_ev): 

1832 '' 

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

1834 self._hover_point = point 

1835 

1836 if self._track_start is not None: 

1837 x0, y0 = self._track_start 

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

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

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

1841 tmin0, tmax0 = self._track_range 

1842 tduration0, tposition0 = self._track_focus 

1843 

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

1845 if self._track_what == 'global': 

1846 scale = math.exp(-dy) 

1847 else: 

1848 scale = 1.0 

1849 

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

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

1852 

1853 tmin = tmin0 - dt - dtr*xfrac 

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

1855 

1856 self.set_range(tmin, tmax) 

1857 

1858 tduration, tposition = self._track_focus 

1859 if tduration is not None: 

1860 etmin0 = tmin_effective( 

1861 tmin0, tmax0, tduration0, tposition0) 

1862 

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

1864 self.set_focus(tduration0, tposition) 

1865 

1866 elif self._track_what == 'focus': 

1867 if tduration0 is not None: 

1868 scale = math.exp(-dy) 

1869 

1870 dtr = tduration0 * (scale - 1.0) 

1871 dt = dx * tduration0 * scale 

1872 

1873 etmin0 = tmin_effective( 

1874 tmin0, tmax0, tduration0, tposition0) 

1875 etmax0 = tmax_effective( 

1876 tmin0, tmax0, tduration0, tposition0) 

1877 

1878 tmin = etmin0 - dt - dtr*xfrac 

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

1880 

1881 tduration = tmax - tmin 

1882 

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

1884 tposition = min( 

1885 max(0., tposition), 

1886 1.0 - tduration / (tmax0 - tmin0)) 

1887 

1888 if tduration < (tmax0 - tmin0): 

1889 self.set_focus(tduration, tposition) 

1890 else: 

1891 self.set_focus(None, tposition) 

1892 

1893 else: 

1894 tduration, tposition = tmax0 - tmin0, 0.0 

1895 self.set_focus(tduration, tposition) 

1896 self._track_focus = (tduration, tposition) 

1897 

1898 elif self._track_what == 'focus_slide': 

1899 if tduration0 is not None: 

1900 self.set_focus( 

1901 tduration0, 

1902 min( 

1903 max(0., tposition0 + dx), 

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

1905 

1906 else: 

1907 

1908 upper_rect = self.upper_rect() 

1909 lower_rect = self.lower_rect() 

1910 upper_projection = self.upper_projection() 

1911 lower_projection = self.lower_projection() 

1912 

1913 app = get_app() 

1914 have_focus = lower_projection and self.tduration is not None 

1915 

1916 if upper_rect.contains(point): 

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

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

1919 app.status( 

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

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

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

1923 if have_focus else '')) 

1924 

1925 elif lower_rect.contains(point): 

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

1927 if have_focus: 

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

1929 app.status( 

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

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

1932 else: 

1933 app.status( 

1934 'Click to activate focus time window.') 

1935 else: 

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

1937 self._tcursor = None 

1938 if have_focus: 

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

1940 else: 

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

1942 

1943 self.update() 

1944 self.tcursorChanged.emit() 

1945 

1946 

1947class StatusMessages(qw.QLabel): 

1948 def __init__(self): 

1949 qw.QLabel.__init__(self) 

1950 self._messages = {} 

1951 self._timers = {} 

1952 

1953 def set(self, key, text, timeout=5.0): 

1954 self._messages[key] = text 

1955 timer = qc.QTimer() 

1956 timer.setSingleShot(True) 

1957 timer.setInterval(int(timeout*1000)) 

1958 

1959 def clear(): 

1960 self.clear(key) 

1961 

1962 timer.timeout.connect(clear) 

1963 timer.start() 

1964 if key in self._timers: 

1965 self._timers[key].stop() 

1966 

1967 self._timers[key] = timer 

1968 self.update_label() 

1969 

1970 def clear(self, key): 

1971 try: 

1972 del self._messages[key] 

1973 except KeyError: 

1974 pass 

1975 try: 

1976 del self._timers[key] 

1977 except KeyError: 

1978 pass 

1979 

1980 self.update_label() 

1981 

1982 def update_label(self): 

1983 messages = [] 

1984 for key, text in self._messages.items(): 

1985 messages.append(text) 

1986 

1987 if messages: 

1988 message = '\u29BF ' + ' - '.join(messages) 

1989 else: 

1990 message = '' 

1991 

1992 self.setText(message) 

1993 

1994 

1995def errorize(widget): 

1996 widget.setStyleSheet(''' 

1997 QLineEdit { 

1998 background: rgb(200, 150, 150); 

1999 }''') 

2000 

2001 

2002def de_errorize(widget): 

2003 if isinstance(widget, qw.QWidget): 

2004 widget.setStyleSheet('') 

2005 

2006 

2007_call_later_timers = {} 

2008 

2009 

2010def _remove_call_later(ref): 

2011 del _call_later_timers[ref] 

2012 

2013 

2014def call_later(method, delay=0): 

2015 ref = weakref.WeakMethod(method, _remove_call_later) 

2016 previous_timer = _call_later_timers.pop(ref, None) 

2017 if previous_timer is not None: 

2018 previous_timer.stop() 

2019 

2020 def call(): 

2021 _remove_call_later(ref) 

2022 method = ref() 

2023 if method is not None: 

2024 method() 

2025 

2026 timer = qc.QTimer() 

2027 timer.setSingleShot(True) 

2028 timer.setInterval(delay) 

2029 timer.timeout.connect(call) 

2030 timer.start() 

2031 

2032 _call_later_timers[ref] = timer 

2033 

2034 

2035def time_or_none_to_str(t): 

2036 if t is None: 

2037 return '' 

2038 else: 

2039 return util.time_to_str(t) 

2040 

2041 

2042def time_to_lineedit(state, attribute, widget): 

2043 widget.setText(time_or_none_to_str(getattr(state, attribute))) 

2044 

2045 

2046def lineedit_to_time(widget, state, attribute): 

2047 s = str(widget.text()) 

2048 if not s.strip(): 

2049 setattr(state, attribute, None) 

2050 else: 

2051 try: 

2052 setattr(state, attribute, util.str_to_time_fillup(s)) 

2053 except Exception: 

2054 raise ValueError( 

2055 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')