1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import signal
8import gc
9import logging
10import time
11import tempfile
12import os
13import shutil
14import platform
15from collections import defaultdict
16from subprocess import check_call
18import numpy as num
20from pyrocko import cake
21from pyrocko import guts
22from pyrocko import geonames
23from pyrocko import config
24from pyrocko import moment_tensor as pmt
25from pyrocko import util
27from pyrocko.gui.util import Progressbars, RangeEdit
28from pyrocko.gui.talkie import TalkieConnectionOwner, equal as state_equal
29from pyrocko.gui.qt_compat import qw, qc, qg
30# from pyrocko.gui import vtk_util
32from . import common, light, snapshots as snapshots_mod
34import vtk
35import vtk.qt
36vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
38from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
40from pyrocko import geometry # noqa
41from . import state as vstate, elements # noqa
43logger = logging.getLogger('pyrocko.gui.sparrow.main')
46d2r = num.pi/180.
47km = 1000.
49if platform.uname()[0] == 'Darwin':
50 g_modifier_key = '\u2318'
51else:
52 g_modifier_key = 'Ctrl'
55class ZeroFrame(qw.QFrame):
57 def sizeHint(self):
58 return qc.QSize(0, 0)
61class LocationChoice(object):
62 def __init__(self, name, lat, lon, depth=0):
63 self._name = name
64 self._lat = lat
65 self._lon = lon
66 self._depth = depth
68 def get_lat_lon_depth(self):
69 return self._lat, self._lon, self._depth
72def location_to_choices(s):
73 choices = []
74 s_vals = s.replace(',', ' ')
75 try:
76 vals = [float(x) for x in s_vals.split()]
77 if len(vals) == 3:
78 vals[2] *= km
80 choices.append(LocationChoice('', *vals))
82 except ValueError:
83 cities = geonames.get_cities_by_name(s.strip())
84 for c in cities:
85 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
87 return choices
90class NoLocationChoices(Exception):
92 def __init__(self, s):
93 self._string = s
95 def __str__(self):
96 return 'No location choices for string "%s"' % self._string
99class QVTKWidget(QVTKRenderWindowInteractor):
100 def __init__(self, viewer, *args):
101 QVTKRenderWindowInteractor.__init__(self, *args)
102 self._viewer = viewer
103 self._ctrl_state = False
105 def wheelEvent(self, event):
106 return self._viewer.myWheelEvent(event)
108 def keyPressEvent(self, event):
109 if event.key() == qc.Qt.Key_Control:
110 self._update_ctrl_state(True)
111 QVTKRenderWindowInteractor.keyPressEvent(self, event)
113 def keyReleaseEvent(self, event):
114 if event.key() == qc.Qt.Key_Control:
115 self._update_ctrl_state(False)
116 QVTKRenderWindowInteractor.keyReleaseEvent(self, event)
118 def focusInEvent(self, event):
119 self._update_ctrl_state()
120 QVTKRenderWindowInteractor.focusInEvent(self, event)
122 def focusOutEvent(self, event):
123 self._update_ctrl_state(False)
124 QVTKRenderWindowInteractor.focusOutEvent(self, event)
126 def mousePressEvent(self, event):
127 self._viewer.disable_capture()
128 QVTKRenderWindowInteractor.mousePressEvent(self, event)
130 def mouseReleaseEvent(self, event):
131 self._viewer.enable_capture()
132 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event)
134 def _update_ctrl_state(self, state=None):
135 if state is None:
136 app = common.get_app()
137 if not app:
138 return
139 state = app.keyboardModifiers() == qc.Qt.ControlModifier
140 if self._ctrl_state != state:
141 self._viewer.gui_state.next_focal_point()
142 self._ctrl_state = state
144 def container_resized(self, ev):
145 self._viewer.update_vtk_widget_size()
148class DetachedViewer(qw.QMainWindow):
150 def __init__(self, main_window, vtk_frame):
151 qw.QMainWindow.__init__(self, main_window)
152 self.main_window = main_window
153 self.setWindowTitle('Sparrow View')
154 vtk_frame.setParent(self)
155 self.setCentralWidget(vtk_frame)
157 def closeEvent(self, ev):
158 ev.ignore()
159 self.main_window.attach()
162class CenteringScrollArea(qw.QScrollArea):
163 def __init__(self):
164 qw.QScrollArea.__init__(self)
165 self.setAlignment(qc.Qt.AlignCenter)
166 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
167 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
168 self.setFrameShape(qw.QFrame.NoFrame)
170 def resizeEvent(self, ev):
171 retval = qw.QScrollArea.resizeEvent(self, ev)
172 self.widget().container_resized(ev)
173 return retval
175 def recenter(self):
176 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
177 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
179 def wheelEvent(self, *args, **kwargs):
180 return self.widget().wheelEvent(*args, **kwargs)
183class YAMLEditor(qw.QTextEdit):
185 def __init__(self, parent):
186 qw.QTextEdit.__init__(self)
187 self._parent = parent
189 def event(self, ev):
190 if isinstance(ev, qg.QKeyEvent) \
191 and ev.key() == qc.Qt.Key_Return \
192 and ev.modifiers() & qc.Qt.ShiftModifier:
193 self._parent.state_changed()
194 return True
196 return qw.QTextEdit.event(self, ev)
199class StateEditor(qw.QFrame, TalkieConnectionOwner):
200 def __init__(self, viewer, *args, **kwargs):
201 qw.QFrame.__init__(self, *args, **kwargs)
202 TalkieConnectionOwner.__init__(self)
204 layout = qw.QGridLayout()
206 self.setLayout(layout)
208 self.source_editor = YAMLEditor(self)
209 self.source_editor.setAcceptRichText(False)
210 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
211 font = qg.QFont("Monospace")
212 self.source_editor.setCurrentFont(font)
213 layout.addWidget(self.source_editor, 0, 0, 1, 2)
215 self.error_display_label = qw.QLabel('Error')
216 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
218 self.error_display = qw.QTextEdit()
219 self.error_display.setCurrentFont(font)
220 self.error_display.setReadOnly(True)
222 self.error_display.setSizePolicy(
223 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
225 self.error_display_label.hide()
226 self.error_display.hide()
228 layout.addWidget(self.error_display, 2, 0, 1, 2)
230 self.instant_updates = qw.QCheckBox('Instant Updates')
231 self.instant_updates.toggled.connect(self.state_changed)
232 layout.addWidget(self.instant_updates, 3, 0)
234 button = qw.QPushButton('Apply')
235 button.clicked.connect(self.state_changed)
236 layout.addWidget(button, 3, 1)
238 self.viewer = viewer
239 # recommended way, but resulted in a variable-width font being used:
240 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
241 self.bind_state()
242 self.source_editor.textChanged.connect(self.text_changed_handler)
243 self.destroyed.connect(self.unbind_state)
244 self.bind_state()
246 def bind_state(self, *args):
247 self.talkie_connect(self.viewer.state, '', self.update_state)
248 self.update_state()
250 def unbind_state(self):
251 self.talkie_disconnect_all()
253 def update_state(self, *args):
254 cursor = self.source_editor.textCursor()
256 cursor_position = cursor.position()
257 vsb_position = self.source_editor.verticalScrollBar().value()
258 hsb_position = self.source_editor.horizontalScrollBar().value()
260 self.source_editor.setPlainText(str(self.viewer.state))
262 cursor.setPosition(cursor_position)
263 self.source_editor.setTextCursor(cursor)
264 self.source_editor.verticalScrollBar().setValue(vsb_position)
265 self.source_editor.horizontalScrollBar().setValue(hsb_position)
267 def text_changed_handler(self, *args):
268 if self.instant_updates.isChecked():
269 self.state_changed()
271 def state_changed(self):
272 try:
273 s = self.source_editor.toPlainText()
274 state = guts.load(string=s)
275 self.viewer.set_state(state)
276 self.error_display.setPlainText('')
277 self.error_display_label.hide()
278 self.error_display.hide()
280 except Exception as e:
281 self.error_display.show()
282 self.error_display_label.show()
283 self.error_display.setPlainText(str(e))
286class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
287 def __init__(self, use_depth_peeling=True, events=None, snapshots=None):
288 qw.QMainWindow.__init__(self)
289 TalkieConnectionOwner.__init__(self)
291 common.get_app().set_main_window(self)
293 self.state = vstate.ViewerState()
294 self.gui_state = vstate.ViewerGuiState()
296 self.setWindowTitle('Sparrow')
298 self.setTabPosition(
299 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
301 self.planet_radius = cake.earthradius
302 self.feature_radius_min = cake.earthradius - 1000. * km
304 self._block_capture = 0
305 self._undo_stack = []
306 self._redo_stack = []
307 self._undo_aggregate = None
309 self._panel_togglers = {}
310 self._actors = set()
311 self._actors_2d = set()
312 self._render_window_size = (0, 0)
313 self._use_depth_peeling = use_depth_peeling
314 self._in_update_elements = False
315 self._update_elements_enabled = True
317 self._animation_tstart = None
318 self._animation_iframe = None
319 self._animation = None
321 mbar = qw.QMenuBar()
322 self.setMenuBar(mbar)
324 menu = mbar.addMenu('File')
326 menu.addAction(
327 'Export Image...',
328 self.export_image,
329 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
330 qc.Qt.ApplicationShortcut)
332 menu.addAction(
333 'Quit',
334 self.request_quit,
335 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
336 qc.Qt.ApplicationShortcut)
338 menu = mbar.addMenu('Edit')
340 menu.addAction(
341 'Undo',
342 self.undo,
343 qg.QKeySequence(
344 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext(
345 qc.Qt.ApplicationShortcut)
347 menu.addAction(
348 'Redo',
349 self.redo,
350 qg.QKeySequence(
351 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext(
352 qc.Qt.ApplicationShortcut)
354 menu = mbar.addMenu('View')
355 menu_sizes = menu.addMenu('Size')
356 self._add_vtk_widget_size_menu_entries(menu_sizes)
358 # detached/attached
359 self.talkie_connect(
360 self.gui_state, 'detached', self.update_detached)
362 action = qw.QAction('Detach')
363 action.setCheckable(True)
364 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
365 action.setShortcutContext(qc.Qt.ApplicationShortcut)
367 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
368 menu.addAction(action)
370 self.panels_menu = mbar.addMenu('Panels')
371 self.panels_menu.addAction(
372 'Stack Panels',
373 self.stack_panels)
374 self.panels_menu.addSeparator()
376 snapshots_menu = mbar.addMenu('Snapshots')
378 menu = mbar.addMenu('Elements')
379 for name, estate in sorted([
380 ('Icosphere', elements.IcosphereState(
381 level=4,
382 smooth=True,
383 opacity=0.5,
384 ambient=0.1)),
385 ('Grid', elements.GridState()),
386 ('Stations', elements.StationsState()),
387 ('Topography', elements.TopoState()),
388 ('Custom Topography', elements.CustomTopoState()),
389 ('Catalog', elements.CatalogState()),
390 ('Coastlines', elements.CoastlinesState()),
391 ('Source', elements.SourceState()),
392 ('HUD Subtitle', elements.HudState(
393 template='Subtitle')),
394 ('HUD (tmax_effective)', elements.HudState(
395 template='tmax: {tmax_effective|date}',
396 position='top-left')),
397 ('Volcanoes', elements.VolcanoesState()),
398 ('Faults', elements.ActiveFaultsState()),
399 ('Plate bounds', elements.PlatesBoundsState()),
400 ('InSAR Surface Displacements', elements.KiteState()),
401 ('Geometry', elements.GeometryState()),
402 ('Spheroid', elements.SpheroidState())]):
404 def wrap_add_element(estate):
405 def add_element(*args):
406 new_element = guts.clone(estate)
407 new_element.element_id = elements.random_id()
408 self.state.elements.append(new_element)
409 self.state.sort_elements()
411 return add_element
413 mitem = qw.QAction(name, self)
415 mitem.triggered.connect(wrap_add_element(estate))
417 menu.addAction(mitem)
419 menu = mbar.addMenu('Help')
421 menu.addAction(
422 'Interactive Tour',
423 self.start_tour)
425 menu.addAction(
426 'Online Manual',
427 self.open_manual)
429 self.data_providers = []
430 self.elements = {}
432 self.detached_window = None
434 self.main_frame = qw.QFrame()
435 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
437 self.vtk_frame = CenteringScrollArea()
439 self.vtk_widget = QVTKWidget(self, self)
440 self.vtk_frame.setWidget(self.vtk_widget)
442 self.main_layout = qw.QVBoxLayout()
443 self.main_layout.setContentsMargins(0, 0, 0, 0)
444 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
446 pb = Progressbars(self)
447 self.progressbars = pb
448 self.main_layout.addWidget(pb)
450 self.main_frame.setLayout(self.main_layout)
452 self.vtk_frame_substitute = None
454 self.add_panel(
455 'Navigation',
456 self.controls_navigation(), visible=True,
457 where=qc.Qt.LeftDockWidgetArea)
459 self.add_panel(
460 'Time',
461 self.controls_time(), visible=True,
462 where=qc.Qt.LeftDockWidgetArea)
464 self.add_panel(
465 'Appearance',
466 self.controls_appearance(), visible=True,
467 where=qc.Qt.LeftDockWidgetArea)
469 snapshots_panel = self.controls_snapshots()
470 self.snapshots_panel = snapshots_panel
471 self.add_panel(
472 'Snapshots',
473 snapshots_panel, visible=False,
474 where=qc.Qt.LeftDockWidgetArea)
476 snapshots_panel.setup_menu(snapshots_menu)
478 self.setCentralWidget(self.main_frame)
480 self.mesh = None
482 ren = vtk.vtkRenderer()
484 # ren.SetBackground(0.15, 0.15, 0.15)
485 # ren.SetBackground(0.0, 0.0, 0.0)
486 # ren.TwoSidedLightingOn()
487 # ren.SetUseShadows(1)
489 self._lighting = None
490 self._background = None
492 self.ren = ren
493 self.update_render_settings()
494 self.update_camera()
496 renwin = self.vtk_widget.GetRenderWindow()
498 if self._use_depth_peeling:
499 renwin.SetAlphaBitPlanes(1)
500 renwin.SetMultiSamples(0)
502 ren.SetUseDepthPeeling(1)
503 ren.SetMaximumNumberOfPeels(100)
504 ren.SetOcclusionRatio(0.1)
506 ren.SetUseFXAA(1)
507 # ren.SetUseHiddenLineRemoval(1)
508 # ren.SetBackingStore(1)
510 self.renwin = renwin
512 # renwin.LineSmoothingOn()
513 # renwin.PointSmoothingOn()
514 # renwin.PolygonSmoothingOn()
516 renwin.AddRenderer(ren)
518 iren = renwin.GetInteractor()
519 iren.LightFollowCameraOn()
520 iren.SetInteractorStyle(None)
522 iren.AddObserver('LeftButtonPressEvent', self.button_event)
523 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
524 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
525 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
526 iren.AddObserver('RightButtonPressEvent', self.button_event)
527 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
528 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
529 iren.AddObserver('KeyPressEvent', self.key_down_event)
530 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
532 renwin.Render()
534 iren.Initialize()
536 self.iren = iren
538 self.rotating = False
540 self._elements = {}
541 self._elements_active = {}
543 self.talkie_connect(
544 self.state, 'elements', self.update_elements)
546 self.state.elements.append(elements.IcosphereState(
547 element_id='icosphere',
548 level=4,
549 smooth=True,
550 opacity=0.5,
551 ambient=0.1))
553 self.state.elements.append(elements.GridState(
554 element_id='grid'))
555 self.state.elements.append(elements.CoastlinesState(
556 element_id='coastlines'))
557 self.state.elements.append(elements.CrosshairState(
558 element_id='crosshair'))
560 # self.state.elements.append(elements.StationsState())
561 # self.state.elements.append(elements.SourceState())
562 # self.state.elements.append(
563 # elements.CatalogState(
564 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
565 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
567 if events:
568 self.state.elements.append(
569 elements.CatalogState(
570 selection=elements.MemoryCatalogSelection(events=events)))
572 self.state.sort_elements()
574 if snapshots:
575 snapshots_ = []
576 for obj in snapshots:
577 if isinstance(obj, str):
578 snapshots_.extend(snapshots_mod.load_snapshots(obj))
579 else:
580 snapshots_.append(obj)
582 snapshots_panel.add_snapshots(snapshots_)
583 self.raise_panel(snapshots_panel)
584 snapshots_panel.goto_snapshot(1)
586 self.timer = qc.QTimer(self)
587 self.timer.timeout.connect(self.periodical)
588 self.timer.setInterval(1000)
589 self.timer.start()
591 self._animation_saver = None
593 self.closing = False
594 self.vtk_widget.setFocus()
596 self.update_detached()
598 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
599 common.get_app().status('Let\'s fly.', 2.0)
601 self.show()
602 self.windowHandle().showMaximized()
604 self.talkie_connect(
605 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
607 self.update_vtk_widget_size()
609 hatch_path = config.expand(os.path.join(
610 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
612 self.talkie_connect(self.state, '', self.capture_state)
613 self.capture_state()
615 if not os.path.exists(hatch_path):
616 with open(hatch_path, 'w') as f:
617 f.write('%s\n' % util.time_to_str(time.time()))
619 self.start_tour()
621 def disable_capture(self):
622 self._block_capture += 1
624 logger.debug('Undo capture block (+1): %i' % self._block_capture)
626 def enable_capture(self, drop=False, aggregate=None):
627 if self._block_capture > 0:
628 self._block_capture -= 1
630 logger.debug('Undo capture block (-1): %i' % self._block_capture)
632 if self._block_capture == 0 and not drop:
633 self.capture_state(aggregate=aggregate)
635 def capture_state(self, *args, aggregate=None):
636 if self._block_capture:
637 return
639 if len(self._undo_stack) == 0 or not state_equal(
640 self.state, self._undo_stack[-1]):
642 if aggregate is not None:
643 if aggregate == self._undo_aggregate:
644 self._undo_stack.pop()
646 self._undo_aggregate = aggregate
647 else:
648 self._undo_aggregate = None
650 logger.debug('Capture undo state (%i%s)\n%s' % (
651 len(self._undo_stack) + 1,
652 '' if aggregate is None else ', aggregate=%s' % aggregate,
653 '\n'.join(
654 ' - %s' % s
655 for s in self._undo_stack[-1].str_diff(
656 self.state).splitlines())
657 if len(self._undo_stack) > 0 else 'initial'))
659 self._undo_stack.append(guts.clone(self.state))
660 self._redo_stack.clear()
662 def undo(self):
663 self._undo_aggregate = None
665 if len(self._undo_stack) <= 1:
666 return
668 state = self._undo_stack.pop()
669 self._redo_stack.append(state)
670 state = self._undo_stack[-1]
672 logger.debug('Undo (%i)\n%s' % (
673 len(self._undo_stack),
674 '\n'.join(
675 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
677 self.disable_capture()
678 try:
679 self.set_state(state)
680 finally:
681 self.enable_capture(drop=True)
683 def redo(self):
684 self._undo_aggregate = None
686 if len(self._redo_stack) == 0:
687 return
689 state = self._redo_stack.pop()
690 self._undo_stack.append(state)
692 logger.debug('Redo (%i)\n%s' % (
693 len(self._redo_stack),
694 '\n'.join(
695 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
697 self.disable_capture()
698 try:
699 self.set_state(state)
700 finally:
701 self.enable_capture(drop=True)
703 def start_tour(self):
704 snapshots_ = snapshots_mod.load_snapshots(
705 'https://data.pyrocko.org/examples/'
706 'sparrow-tour-v0.1.snapshots.yaml')
707 self.snapshots_panel.add_snapshots(snapshots_)
708 self.raise_panel(self.snapshots_panel)
709 self.snapshots_panel.transition_to_next_snapshot()
711 def open_manual(self):
712 import webbrowser
713 webbrowser.open(
714 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
716 def _add_vtk_widget_size_menu_entries(self, menu):
718 group = qw.QActionGroup(menu)
719 group.setExclusive(True)
721 def set_variable_size():
722 self.gui_state.fixed_size = False
724 variable_size_action = menu.addAction('Fit Window Size')
725 variable_size_action.setCheckable(True)
726 variable_size_action.setActionGroup(group)
727 variable_size_action.triggered.connect(set_variable_size)
729 fixed_size_items = []
730 for nx, ny, label in [
731 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
732 (426, 240, ''),
733 (640, 360, ''),
734 (854, 480, '(FWVGA)'),
735 (1280, 720, '(HD)'),
736 (1920, 1080, '(Full HD)'),
737 (2560, 1440, '(Quad HD)'),
738 (3840, 2160, '(4K UHD)'),
739 (3840*2, 2160*2, '',),
740 (None, None, 'Aspect 4:3'),
741 (640, 480, '(VGA)'),
742 (800, 600, '(SVGA)'),
743 (None, None, 'Other'),
744 (512, 512, ''),
745 (1024, 1024, '')]:
747 if None in (nx, ny):
748 menu.addSection(label)
749 else:
750 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
751 action = menu.addAction(name)
752 action.setCheckable(True)
753 action.setActionGroup(group)
754 fixed_size_items.append((action, (nx, ny)))
756 def make_set_fixed_size(nx, ny):
757 def set_fixed_size():
758 self.gui_state.fixed_size = (float(nx), float(ny))
760 return set_fixed_size
762 action.triggered.connect(make_set_fixed_size(nx, ny))
764 def update_widget(*args):
765 for action, (nx, ny) in fixed_size_items:
766 action.blockSignals(True)
767 action.setChecked(
768 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
769 int(z) for z in self.gui_state.fixed_size)))
770 action.blockSignals(False)
772 variable_size_action.blockSignals(True)
773 variable_size_action.setChecked(not self.gui_state.fixed_size)
774 variable_size_action.blockSignals(False)
776 update_widget()
777 self.talkie_connect(
778 self.gui_state, 'fixed_size', update_widget)
780 def update_vtk_widget_size(self, *args):
781 if self.gui_state.fixed_size:
782 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
783 wanted_size = qc.QSize(nx, ny)
784 else:
785 wanted_size = qc.QSize(
786 self.vtk_frame.window().width(), self.vtk_frame.height())
788 current_size = self.vtk_widget.size()
790 if current_size.width() != wanted_size.width() \
791 or current_size.height() != wanted_size.height():
793 self.vtk_widget.setFixedSize(wanted_size)
795 self.vtk_frame.recenter()
796 self.check_vtk_resize()
798 def update_focal_point(self, *args):
799 if self.gui_state.focal_point == 'center':
800 self.vtk_widget.setStatusTip(
801 'Click and drag: change location. %s-click and drag: '
802 'change view plane orientation.' % g_modifier_key)
803 else:
804 self.vtk_widget.setStatusTip(
805 '%s-click and drag: change location. Click and drag: '
806 'change view plane orientation. Uncheck "Navigation: Fix" to '
807 'reverse sense.' % g_modifier_key)
809 def update_detached(self, *args):
811 if self.gui_state.detached and not self.detached_window: # detach
812 logger.debug('Detaching VTK view.')
814 self.main_layout.removeWidget(self.vtk_frame)
815 self.detached_window = DetachedViewer(self, self.vtk_frame)
816 self.detached_window.show()
817 self.vtk_widget.setFocus()
819 screens = common.get_app().screens()
820 if len(screens) > 1:
821 for screen in screens:
822 if screen is not self.screen():
823 self.detached_window.windowHandle().setScreen(screen)
824 # .setScreen() does not work reliably,
825 # therefore trying also with .move()...
826 p = screen.geometry().topLeft()
827 self.detached_window.move(p.x() + 50, p.y() + 50)
828 # ... but also does not work in notion window manager.
830 self.detached_window.windowHandle().showMaximized()
832 frame = qw.QFrame()
833 # frame.setFrameShape(qw.QFrame.NoFrame)
834 # frame.setBackgroundRole(qg.QPalette.Mid)
835 # frame.setAutoFillBackground(True)
836 frame.setSizePolicy(
837 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
839 layout = qw.QGridLayout()
840 frame.setLayout(layout)
841 self.main_layout.insertWidget(0, frame)
843 self.state_editor = StateEditor(self)
845 layout.addWidget(self.state_editor, 0, 0)
847 # attach_button = qw.QPushButton('Attach View')
848 # attach_button.clicked.connect(self.attach)
849 # layout.addWidget(
850 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
852 self.vtk_frame_substitute = frame
854 if not self.gui_state.detached and self.detached_window: # attach
855 logger.debug('Attaching VTK view.')
856 self.detached_window.hide()
857 self.vtk_frame.setParent(self)
858 if self.vtk_frame_substitute:
859 self.main_layout.removeWidget(self.vtk_frame_substitute)
860 self.state_editor.unbind_state()
861 self.vtk_frame_substitute = None
863 self.main_layout.insertWidget(0, self.vtk_frame)
864 self.detached_window = None
865 self.vtk_widget.setFocus()
867 def attach(self):
868 self.gui_state.detached = False
870 def export_image(self):
872 caption = 'Export Image'
873 fn_out, _ = qw.QFileDialog.getSaveFileName(
874 self, caption, 'image.png',
875 options=common.qfiledialog_options)
877 if fn_out:
878 self.save_image(fn_out)
880 def save_image(self, path):
882 original_fixed_size = self.gui_state.fixed_size
883 if original_fixed_size is None:
884 self.gui_state.fixed_size = (1920., 1080.)
886 wif = vtk.vtkWindowToImageFilter()
887 wif.SetInput(self.renwin)
888 wif.SetInputBufferTypeToRGBA()
889 wif.SetScale(1, 1)
890 wif.ReadFrontBufferOff()
891 writer = vtk.vtkPNGWriter()
892 writer.SetInputConnection(wif.GetOutputPort())
894 self.renwin.Render()
895 wif.Modified()
896 writer.SetFileName(path)
897 writer.Write()
899 self.gui_state.fixed_size = original_fixed_size
901 def update_render_settings(self, *args):
902 if self._lighting is None or self._lighting != self.state.lighting:
903 self.ren.RemoveAllLights()
904 for li in light.get_lights(self.state.lighting):
905 self.ren.AddLight(li)
907 self._lighting = self.state.lighting
909 if self._background is None \
910 or self._background != self.state.background:
912 self.state.background.vtk_apply(self.ren)
913 self._background = self.state.background
915 self.update_view()
917 def start_animation(self, interpolator, output_path=None):
918 if self._animation:
919 logger.debug('Aborting animation in progress to start a new one.')
920 self.stop_animation()
922 self.disable_capture()
923 self._animation = interpolator
924 if output_path is None:
925 self._animation_tstart = time.time()
926 self._animation_iframe = None
927 else:
928 self._animation_iframe = 0
929 self.showFullScreen()
930 self.update_view()
931 self.gui_state.panels_visible = False
932 self.update_view()
934 self._animation_timer = qc.QTimer(self)
935 self._animation_timer.timeout.connect(self.next_animation_frame)
936 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
937 self._animation_timer.start()
938 if output_path is not None:
939 original_fixed_size = self.gui_state.fixed_size
940 if original_fixed_size is None:
941 self.gui_state.fixed_size = (1920., 1080.)
943 wif = vtk.vtkWindowToImageFilter()
944 wif.SetInput(self.renwin)
945 wif.SetInputBufferTypeToRGBA()
946 wif.SetScale(1, 1)
947 wif.ReadFrontBufferOff()
948 writer = vtk.vtkPNGWriter()
949 temp_path = tempfile.mkdtemp()
950 self._animation_saver = (
951 wif, writer, temp_path, output_path, original_fixed_size)
952 writer.SetInputConnection(wif.GetOutputPort())
954 def next_animation_frame(self):
956 ani = self._animation
957 if not ani:
958 return
960 if self._animation_iframe is not None:
961 state = ani(
962 ani.tmin
963 + self._animation_iframe * ani.dt)
965 self._animation_iframe += 1
966 else:
967 tnow = time.time()
968 state = ani(min(
969 ani.tmax,
970 ani.tmin + (tnow - self._animation_tstart)))
972 self.set_state(state)
973 self.renwin.Render()
974 if self._animation_saver:
975 wif, writer, temp_path, _, _ = self._animation_saver
976 wif.Modified()
977 fn = os.path.join(temp_path, 'f%09i.png')
978 writer.SetFileName(fn % self._animation_iframe)
979 writer.Write()
981 if self._animation_iframe is not None:
982 t = self._animation_iframe * ani.dt
983 else:
984 t = tnow - self._animation_tstart
986 if t > ani.tmax - ani.tmin:
987 self.stop_animation()
989 def stop_animation(self):
990 if self._animation_timer:
991 self._animation_timer.stop()
993 if self._animation_saver:
995 wif, writer, temp_path, output_path, original_fixed_size \
996 = self._animation_saver
997 self.gui_state.fixed_size = original_fixed_size
999 fn_path = os.path.join(temp_path, 'f%09d.png')
1000 check_call([
1001 'ffmpeg', '-y',
1002 '-i', fn_path,
1003 '-c:v', 'libx264',
1004 '-preset', 'slow',
1005 '-crf', '17',
1006 '-vf', 'format=yuv420p,fps=%i' % (
1007 int(round(1.0/self._animation.dt))),
1008 output_path])
1009 shutil.rmtree(temp_path)
1011 self._animation_saver = None
1012 self._animation_saver
1014 self.showNormal()
1015 self.gui_state.panels_visible = True
1017 self._animation_tstart = None
1018 self._animation_iframe = None
1019 self._animation = None
1020 self.enable_capture()
1022 def set_state(self, state):
1023 self.disable_capture()
1024 try:
1025 self._update_elements_enabled = False
1026 self.setUpdatesEnabled(False)
1027 self.state.diff_update(state)
1028 self.state.sort_elements()
1029 self.setUpdatesEnabled(True)
1030 self._update_elements_enabled = True
1031 self.update_elements()
1032 finally:
1033 self.enable_capture()
1035 def periodical(self):
1036 pass
1038 def request_quit(self):
1039 app = common.get_app()
1040 app.myQuit()
1042 def check_vtk_resize(self, *args):
1043 render_window_size = self.renwin.GetSize()
1044 if self._render_window_size != render_window_size:
1045 self._render_window_size = render_window_size
1046 self.resize_event(*render_window_size)
1048 def update_elements(self, *_):
1049 if not self._update_elements_enabled:
1050 return
1052 if self._in_update_elements:
1053 return
1055 self._in_update_elements = True
1056 for estate in self.state.elements:
1057 if estate.element_id not in self._elements:
1058 new_element = estate.create()
1059 logger.debug('Creating "%s" ("%s").' % (
1060 type(new_element).__name__,
1061 estate.element_id))
1062 self._elements[estate.element_id] = new_element
1064 element = self._elements[estate.element_id]
1066 if estate.element_id not in self._elements_active:
1067 logger.debug('Adding "%s" ("%s")' % (
1068 type(element).__name__,
1069 estate.element_id))
1070 element.bind_state(estate)
1071 element.set_parent(self)
1072 self._elements_active[estate.element_id] = element
1074 state_element_ids = [el.element_id for el in self.state.elements]
1075 deactivate = []
1076 for element_id, element in self._elements_active.items():
1077 if element_id not in state_element_ids:
1078 logger.debug('Removing "%s" ("%s").' % (
1079 type(element).__name__,
1080 element_id))
1081 element.unset_parent()
1082 deactivate.append(element_id)
1084 for element_id in deactivate:
1085 del self._elements_active[element_id]
1087 self._update_crosshair_bindings()
1089 self._in_update_elements = False
1091 def _update_crosshair_bindings(self):
1093 def get_crosshair_element():
1094 for element in self.state.elements:
1095 if element.element_id == 'crosshair':
1096 return element
1098 return None
1100 crosshair = get_crosshair_element()
1101 if crosshair is None or crosshair.is_connected:
1102 return
1104 def to_checkbox(state, widget):
1105 widget.blockSignals(True)
1106 widget.setChecked(state.visible)
1107 widget.blockSignals(False)
1109 def to_state(widget, state):
1110 state.visible = widget.isChecked()
1112 cb = self._crosshair_checkbox
1113 vstate.state_bind(
1114 self, crosshair, ['visible'], to_state,
1115 cb, [cb.toggled], to_checkbox)
1117 crosshair.is_connected = True
1119 def add_actor_2d(self, actor):
1120 if actor not in self._actors_2d:
1121 self.ren.AddActor2D(actor)
1122 self._actors_2d.add(actor)
1124 def remove_actor_2d(self, actor):
1125 if actor in self._actors_2d:
1126 self.ren.RemoveActor2D(actor)
1127 self._actors_2d.remove(actor)
1129 def add_actor(self, actor):
1130 if actor not in self._actors:
1131 self.ren.AddActor(actor)
1132 self._actors.add(actor)
1134 def add_actor_list(self, actorlist):
1135 for actor in actorlist:
1136 self.add_actor(actor)
1138 def remove_actor(self, actor):
1139 if actor in self._actors:
1140 self.ren.RemoveActor(actor)
1141 self._actors.remove(actor)
1143 def update_view(self):
1144 self.vtk_widget.update()
1146 def resize_event(self, size_x, size_y):
1147 self.gui_state.size = (size_x, size_y)
1149 def button_event(self, obj, event):
1150 if event == "LeftButtonPressEvent":
1151 self.rotating = True
1152 elif event == "LeftButtonReleaseEvent":
1153 self.rotating = False
1155 def mouse_move_event(self, obj, event):
1156 x0, y0 = self.iren.GetLastEventPosition()
1157 x, y = self.iren.GetEventPosition()
1159 size_x, size_y = self.renwin.GetSize()
1160 center_x = size_x / 2.0
1161 center_y = size_y / 2.0
1163 if self.rotating:
1164 self.do_rotate(x, y, x0, y0, center_x, center_y)
1166 def myWheelEvent(self, event):
1168 angle = event.angleDelta().y()
1170 if angle > 200:
1171 angle = 200
1173 if angle < -200:
1174 angle = -200
1176 self.disable_capture()
1177 try:
1178 self.do_dolly(-angle/100.)
1179 finally:
1180 self.enable_capture(aggregate='distance')
1182 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1184 dx = x0 - x
1185 dy = y0 - y
1187 phi = d2r*(self.state.strike - 90.)
1188 focp = self.gui_state.focal_point
1190 if focp == 'center':
1191 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1192 - math.sin(phi) * dx + math.cos(phi) * dy
1194 lat = self.state.lat
1195 lon = self.state.lon
1196 factor = self.state.distance / 10.0
1197 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1198 else:
1199 lat = 90. - self.state.dip
1200 lon = -self.state.strike - 90.
1201 factor = 0.5
1202 factor_lat = 1.0
1204 dlat = dy * factor
1205 dlon = dx * factor * factor_lat
1207 lat = max(min(lat + dlat, 90.), -90.)
1208 lon += dlon
1209 lon = (lon + 180.) % 360. - 180.
1211 if focp == 'center':
1212 self.state.lat = float(lat)
1213 self.state.lon = float(lon)
1214 else:
1215 self.state.dip = float(90. - lat)
1216 self.state.strike = float(-(lon + 90.))
1218 def do_dolly(self, v):
1219 self.state.distance *= float(1.0 + 0.1*v)
1221 def key_down_event(self, obj, event):
1222 k = obj.GetKeyCode()
1223 if k == 'f':
1224 self.gui_state.next_focal_point()
1226 elif k == 'r':
1227 self.reset_strike_dip()
1229 elif k == 'p':
1230 print(self.state)
1232 elif k == 'i':
1233 for elem in self.state.elements:
1234 if isinstance(elem, elements.IcosphereState):
1235 elem.visible = not elem.visible
1237 elif k == 'c':
1238 for elem in self.state.elements:
1239 if isinstance(elem, elements.CoastlinesState):
1240 elem.visible = not elem.visible
1242 elif k == 't':
1243 if not any(
1244 isinstance(elem, elements.TopoState)
1245 for elem in self.state.elements):
1247 self.state.elements.append(elements.TopoState())
1248 else:
1249 for elem in self.state.elements:
1250 if isinstance(elem, elements.TopoState):
1251 elem.visible = not elem.visible
1253 elif k == ' ':
1254 self.toggle_panel_visibility()
1256 def _state_bind(self, *args, **kwargs):
1257 vstate.state_bind(self, self.state, *args, **kwargs)
1259 def _gui_state_bind(self, *args, **kwargs):
1260 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1262 def controls_navigation(self):
1263 frame = qw.QFrame(self)
1264 frame.setSizePolicy(
1265 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1266 layout = qw.QGridLayout()
1267 frame.setLayout(layout)
1269 # lat, lon, depth
1271 layout.addWidget(
1272 qw.QLabel('Location'), 0, 0, 1, 2)
1274 le = qw.QLineEdit()
1275 le.setStatusTip(
1276 'Latitude, Longitude, Depth [km] or city name: '
1277 'Focal point location.')
1278 layout.addWidget(le, 1, 0, 1, 1)
1280 def lat_lon_depth_to_lineedit(state, widget):
1281 sel = str(widget.selectedText()) == str(widget.text())
1282 widget.setText('%g, %g, %g' % (
1283 state.lat, state.lon, state.depth / km))
1285 if sel:
1286 widget.selectAll()
1288 def lineedit_to_lat_lon_depth(widget, state):
1289 self.disable_capture()
1290 try:
1291 s = str(widget.text())
1292 choices = location_to_choices(s)
1293 if len(choices) > 0:
1294 self.state.lat, self.state.lon, self.state.depth = \
1295 choices[0].get_lat_lon_depth()
1296 else:
1297 raise NoLocationChoices(s)
1299 finally:
1300 self.enable_capture()
1302 self._state_bind(
1303 ['lat', 'lon', 'depth'],
1304 lineedit_to_lat_lon_depth,
1305 le, [le.editingFinished, le.returnPressed],
1306 lat_lon_depth_to_lineedit)
1308 self.lat_lon_lineedit = le
1310 self.lat_lon_lineedit.returnPressed.connect(
1311 lambda *args: self.lat_lon_lineedit.selectAll())
1313 # focal point
1315 cb = qw.QCheckBox('Fix')
1316 cb.setStatusTip(
1317 'Fix location. Orbit focal point without pressing %s.'
1318 % g_modifier_key)
1319 layout.addWidget(cb, 1, 1, 1, 1)
1321 def focal_point_to_checkbox(state, widget):
1322 widget.blockSignals(True)
1323 widget.setChecked(self.gui_state.focal_point != 'center')
1324 widget.blockSignals(False)
1326 def checkbox_to_focal_point(widget, state):
1327 self.gui_state.focal_point = \
1328 'target' if widget.isChecked() else 'center'
1330 self._gui_state_bind(
1331 ['focal_point'], checkbox_to_focal_point,
1332 cb, [cb.toggled], focal_point_to_checkbox)
1334 self.focal_point_checkbox = cb
1336 self.talkie_connect(
1337 self.gui_state, 'focal_point', self.update_focal_point)
1339 self.update_focal_point()
1341 # strike, dip
1343 layout.addWidget(
1344 qw.QLabel('View Plane'), 2, 0, 1, 2)
1346 le = qw.QLineEdit()
1347 le.setStatusTip(
1348 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1349 'direction.')
1350 layout.addWidget(le, 3, 0, 1, 1)
1352 def strike_dip_to_lineedit(state, widget):
1353 sel = widget.selectedText() == widget.text()
1354 widget.setText('%g, %g' % (state.strike, state.dip))
1355 if sel:
1356 widget.selectAll()
1358 def lineedit_to_strike_dip(widget, state):
1359 s = str(widget.text())
1360 string_to_strike_dip = {
1361 'east': (0., 90.),
1362 'west': (180., 90.),
1363 'south': (90., 90.),
1364 'north': (270., 90.),
1365 'top': (90., 0.),
1366 'bottom': (90., 180.)}
1368 self.disable_capture()
1369 if s in string_to_strike_dip:
1370 state.strike, state.dip = string_to_strike_dip[s]
1372 s = s.replace(',', ' ')
1373 try:
1374 state.strike, state.dip = map(float, s.split())
1375 except Exception:
1376 raise ValueError('need two numerical values: <strike>, <dip>')
1377 finally:
1378 self.enable_capture()
1380 self._state_bind(
1381 ['strike', 'dip'], lineedit_to_strike_dip,
1382 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1384 self.strike_dip_lineedit = le
1385 self.strike_dip_lineedit.returnPressed.connect(
1386 lambda *args: self.strike_dip_lineedit.selectAll())
1388 but = qw.QPushButton('Reset')
1389 but.setStatusTip('Reset to north-up map view.')
1390 but.clicked.connect(self.reset_strike_dip)
1391 layout.addWidget(but, 3, 1, 1, 1)
1393 # crosshair
1395 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1396 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1398 # camera bindings
1399 self.talkie_connect(
1400 self.state,
1401 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1402 self.update_camera)
1404 self.talkie_connect(
1405 self.gui_state, 'panels_visible', self.update_panel_visibility)
1407 return frame
1409 def controls_time(self):
1410 frame = qw.QFrame(self)
1411 frame.setSizePolicy(
1412 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1414 layout = qw.QGridLayout()
1415 frame.setLayout(layout)
1417 layout.addWidget(qw.QLabel('Min'), 0, 0)
1418 le_tmin = qw.QLineEdit()
1419 layout.addWidget(le_tmin, 0, 1)
1421 layout.addWidget(qw.QLabel('Max'), 1, 0)
1422 le_tmax = qw.QLineEdit()
1423 layout.addWidget(le_tmax, 1, 1)
1425 label_tcursor = qw.QLabel()
1427 label_tcursor.setSizePolicy(
1428 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1430 layout.addWidget(label_tcursor, 2, 1)
1431 self._label_tcursor = label_tcursor
1433 def time_to_lineedit(state, attribute, widget):
1434 sel = widget.selectedText() == widget.text() \
1435 and widget.text() != ''
1437 widget.setText(
1438 common.time_or_none_to_str(getattr(state, attribute)))
1440 if sel:
1441 widget.selectAll()
1443 def lineedit_to_time(widget, state, attribute):
1444 from pyrocko.util import str_to_time_fillup
1446 s = str(widget.text())
1447 if not s.strip():
1448 setattr(state, attribute, None)
1449 else:
1450 try:
1451 setattr(state, attribute, str_to_time_fillup(s))
1452 except Exception:
1453 raise ValueError(
1454 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1456 self._state_bind(
1457 ['tmin'], lineedit_to_time, le_tmin,
1458 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1459 attribute='tmin')
1460 self._state_bind(
1461 ['tmax'], lineedit_to_time, le_tmax,
1462 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1463 attribute='tmax')
1465 self.tmin_lineedit = le_tmin
1466 self.tmax_lineedit = le_tmax
1468 range_edit = RangeEdit()
1469 range_edit.rangeEditPressed.connect(self.disable_capture)
1470 range_edit.rangeEditReleased.connect(self.enable_capture)
1471 range_edit.set_data_provider(self)
1472 range_edit.set_data_name('time')
1474 xblock = [False]
1476 def range_to_range_edit(state, widget):
1477 if not xblock[0]:
1478 widget.blockSignals(True)
1479 widget.set_focus(state.tduration, state.tposition)
1480 widget.set_range(state.tmin, state.tmax)
1481 widget.blockSignals(False)
1483 def range_edit_to_range(widget, state):
1484 xblock[0] = True
1485 self.state.tduration, self.state.tposition = widget.get_focus()
1486 self.state.tmin, self.state.tmax = widget.get_range()
1487 xblock[0] = False
1489 self._state_bind(
1490 ['tmin', 'tmax', 'tduration', 'tposition'],
1491 range_edit_to_range,
1492 range_edit,
1493 [range_edit.rangeChanged, range_edit.focusChanged],
1494 range_to_range_edit)
1496 def handle_tcursor_changed():
1497 self.gui_state.tcursor = range_edit.get_tcursor()
1499 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1501 layout.addWidget(range_edit, 3, 0, 1, 2)
1503 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1504 le_focus = qw.QLineEdit()
1505 layout.addWidget(le_focus, 4, 1)
1507 def focus_to_lineedit(state, widget):
1508 sel = widget.selectedText() == widget.text() \
1509 and widget.text() != ''
1511 if state.tduration is None:
1512 widget.setText('')
1513 else:
1514 widget.setText('%s, %g' % (
1515 guts.str_duration(state.tduration),
1516 state.tposition))
1518 if sel:
1519 widget.selectAll()
1521 def lineedit_to_focus(widget, state):
1522 s = str(widget.text())
1523 w = [x.strip() for x in s.split(',')]
1524 try:
1525 if len(w) == 0 or not w[0]:
1526 state.tduration = None
1527 state.tposition = 0.0
1528 else:
1529 state.tduration = guts.parse_duration(w[0])
1530 if len(w) > 1:
1531 state.tposition = float(w[1])
1532 else:
1533 state.tposition = 0.0
1535 except Exception:
1536 raise ValueError('need two values: <duration>, <position>')
1538 self._state_bind(
1539 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1540 [le_focus.editingFinished, le_focus.returnPressed],
1541 focus_to_lineedit)
1543 label_effective_tmin = qw.QLabel()
1544 label_effective_tmax = qw.QLabel()
1546 label_effective_tmin.setSizePolicy(
1547 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1548 label_effective_tmax.setSizePolicy(
1549 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1550 label_effective_tmin.setMinimumSize(
1551 qg.QFontMetrics(label_effective_tmin.font()).width(
1552 '0000-00-00 00:00:00.000 '), 0)
1554 layout.addWidget(label_effective_tmin, 5, 1)
1555 layout.addWidget(label_effective_tmax, 6, 1)
1557 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1558 self.talkie_connect(
1559 self.state, var, self.update_effective_time_labels)
1561 self._label_effective_tmin = label_effective_tmin
1562 self._label_effective_tmax = label_effective_tmax
1564 self.talkie_connect(
1565 self.gui_state, 'tcursor', self.update_tcursor)
1567 return frame
1569 def controls_appearance(self):
1570 frame = qw.QFrame(self)
1571 frame.setSizePolicy(
1572 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1573 layout = qw.QGridLayout()
1574 frame.setLayout(layout)
1576 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1578 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1579 layout.addWidget(cb, 0, 1)
1580 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1582 self.talkie_connect(
1583 self.state, 'lighting', self.update_render_settings)
1585 # background
1587 layout.addWidget(qw.QLabel('Background'), 1, 0)
1589 cb = common.strings_to_combobox(
1590 ['black', 'white', 'skyblue1 - white'])
1592 layout.addWidget(cb, 1, 1)
1593 vstate.state_bind_combobox_background(
1594 self, self.state, 'background', cb)
1596 self.talkie_connect(
1597 self.state, 'background', self.update_render_settings)
1599 return frame
1601 def controls_snapshots(self):
1602 return snapshots_mod.SnapshotsPanel(self)
1604 def update_effective_time_labels(self, *args):
1605 tmin = self.state.tmin_effective
1606 tmax = self.state.tmax_effective
1608 stmin = common.time_or_none_to_str(tmin)
1609 stmax = common.time_or_none_to_str(tmax)
1611 self._label_effective_tmin.setText(stmin)
1612 self._label_effective_tmax.setText(stmax)
1614 def update_tcursor(self, *args):
1615 tcursor = self.gui_state.tcursor
1616 stcursor = common.time_or_none_to_str(tcursor)
1617 self._label_tcursor.setText(stcursor)
1619 def reset_strike_dip(self, *args):
1620 self.state.strike = 90.
1621 self.state.dip = 0
1622 self.gui_state.focal_point = 'center'
1624 def get_camera_geometry(self):
1626 def rtp2xyz(rtp):
1627 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1629 radius = 1.0 - self.state.depth / self.planet_radius
1631 cam_rtp = num.array([
1632 radius+self.state.distance,
1633 self.state.lat * d2r + 0.5*num.pi,
1634 self.state.lon * d2r])
1635 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1636 cam, up, foc = \
1637 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1639 foc_rtp = num.array([
1640 radius,
1641 self.state.lat * d2r + 0.5*num.pi,
1642 self.state.lon * d2r])
1644 foc = rtp2xyz(foc_rtp)
1646 rot_world = pmt.euler_to_matrix(
1647 -(self.state.lat-90.)*d2r,
1648 (self.state.lon+90.)*d2r,
1649 0.0*d2r).T
1651 rot_cam = pmt.euler_to_matrix(
1652 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1654 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1656 cam = foc + num.dot(rot, cam - foc)
1657 up = num.dot(rot, up)
1658 return cam, up, foc
1660 def update_camera(self, *args):
1661 cam, up, foc = self.get_camera_geometry()
1662 camera = self.ren.GetActiveCamera()
1663 camera.SetPosition(*cam)
1664 camera.SetFocalPoint(*foc)
1665 camera.SetViewUp(*up)
1667 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1669 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1670 self.feature_radius_min / self.planet_radius)**2))
1672 # if horizon == 0.0:
1673 # horizon = 2.0 + self.state.distance
1675 # clip_dist = max(min(self.state.distance*5., max(
1676 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1677 # , math.sqrt(num.sum(cam**2)))
1678 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1679 # clip_dist = feature_horizon
1681 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1683 self.camera_params = (
1684 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1686 self.update_view()
1688 def add_panel(
1689 self, title_label, panel,
1690 visible=False,
1691 # volatile=False,
1692 tabify=True,
1693 where=qc.Qt.RightDockWidgetArea,
1694 remove=None,
1695 title_controls=[]):
1697 dockwidget = common.MyDockWidget(
1698 self, title_label, title_controls=title_controls)
1700 if not visible:
1701 dockwidget.hide()
1703 if not self.gui_state.panels_visible:
1704 dockwidget.block()
1706 dockwidget.setWidget(panel)
1708 panel.setParent(dockwidget)
1710 dockwidgets = self.findChildren(common.MyDockWidget)
1711 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1713 self.addDockWidget(where, dockwidget)
1715 nwrap = 4
1716 if dws and len(dws) >= nwrap and tabify:
1717 self.tabifyDockWidget(
1718 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1720 mitem = dockwidget.toggleViewAction()
1722 def update_label(*args):
1723 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1724 self.update_slug_abbreviated_lengths()
1726 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1727 dockwidget.titlebar._title_label.title_changed.connect(
1728 self.update_slug_abbreviated_lengths)
1730 update_label()
1732 self._panel_togglers[dockwidget] = mitem
1733 self.panels_menu.addAction(mitem)
1734 if visible:
1735 dockwidget.setVisible(True)
1736 dockwidget.setFocus()
1737 dockwidget.raise_()
1739 def stack_panels(self):
1740 dockwidgets = self.findChildren(common.MyDockWidget)
1741 by_area = defaultdict(list)
1742 for dw in dockwidgets:
1743 area = self.dockWidgetArea(dw)
1744 by_area[area].append(dw)
1746 for dockwidgets in by_area.values():
1747 dw_last = None
1748 for dw in dockwidgets:
1749 if dw_last is not None:
1750 self.tabifyDockWidget(dw_last, dw)
1752 dw_last = dw
1754 def update_slug_abbreviated_lengths(self):
1755 dockwidgets = self.findChildren(common.MyDockWidget)
1756 title_labels = []
1757 for dw in dockwidgets:
1758 title_labels.append(dw.titlebar._title_label)
1760 by_title = defaultdict(list)
1761 for tl in title_labels:
1762 by_title[tl.get_title()].append(tl)
1764 for group in by_title.values():
1765 slugs = [tl.get_slug() for tl in group]
1767 n = max(len(slug) for slug in slugs)
1768 nunique = len(set(slugs))
1770 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1771 n -= 1
1773 if n > 0:
1774 n = max(3, n)
1776 for tl in group:
1777 tl.set_slug_abbreviated_length(n)
1779 def raise_panel(self, panel):
1780 dockwidget = panel.parent()
1781 dockwidget.setVisible(True)
1782 dockwidget.setFocus()
1783 dockwidget.raise_()
1785 def toggle_panel_visibility(self):
1786 self.gui_state.panels_visible = not self.gui_state.panels_visible
1788 def update_panel_visibility(self, *args):
1789 self.setUpdatesEnabled(False)
1790 mbar = self.menuBar()
1791 sbar = self.statusBar()
1792 dockwidgets = self.findChildren(common.MyDockWidget)
1794 # Set height to zero instead of hiding so that shortcuts still work
1795 # otherwise one would have to mess around with separate QShortcut
1796 # objects.
1797 mbar.setFixedHeight(
1798 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1800 sbar.setVisible(self.gui_state.panels_visible)
1801 for dockwidget in dockwidgets:
1802 dockwidget.setBlocked(not self.gui_state.panels_visible)
1804 self.setUpdatesEnabled(True)
1806 def remove_panel(self, panel):
1807 dockwidget = panel.parent()
1808 self.removeDockWidget(dockwidget)
1809 dockwidget.setParent(None)
1810 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1812 def register_data_provider(self, provider):
1813 if provider not in self.data_providers:
1814 self.data_providers.append(provider)
1816 def unregister_data_provider(self, provider):
1817 if provider in self.data_providers:
1818 self.data_providers.remove(provider)
1820 def iter_data(self, name):
1821 for provider in self.data_providers:
1822 for data in provider.iter_data(name):
1823 yield data
1825 def closeEvent(self, event):
1826 self.attach()
1827 event.accept()
1828 self.closing = True
1829 common.get_app().set_main_window(None)
1831 def is_closing(self):
1832 return self.closing
1835class SparrowApp(qw.QApplication):
1836 def __init__(self):
1837 qw.QApplication.__init__(self, ['Sparrow'])
1838 self.lastWindowClosed.connect(self.myQuit)
1839 self._main_window = None
1840 self.setApplicationDisplayName('Sparrow')
1841 self.setDesktopFileName('Sparrow')
1843 def install_sigint_handler(self):
1844 self._old_signal_handler = signal.signal(
1845 signal.SIGINT, self.myCloseAllWindows)
1847 def uninstall_sigint_handler(self):
1848 signal.signal(signal.SIGINT, self._old_signal_handler)
1850 def myQuit(self, *args):
1851 self.quit()
1853 def myCloseAllWindows(self, *args):
1854 self.closeAllWindows()
1856 def set_main_window(self, win):
1857 self._main_window = win
1859 def get_main_window(self):
1860 return self._main_window
1862 def get_progressbars(self):
1863 if self._main_window:
1864 return self._main_window.progressbars
1865 else:
1866 return None
1868 def status(self, message, duration=None):
1869 win = self.get_main_window()
1870 if not win:
1871 return
1873 win.statusBar().showMessage(
1874 message, int((duration or 0) * 1000))
1877def main(*args, **kwargs):
1879 from pyrocko import util
1880 from pyrocko.gui import util as gui_util
1881 util.setup_logging('sparrow', 'info')
1883 global win
1885 if gui_util.app is None:
1886 gui_util.app = SparrowApp()
1888 # try:
1889 # from qt_material import apply_stylesheet
1890 #
1891 # apply_stylesheet(app, theme='dark_teal.xml')
1892 #
1893 #
1894 # import qdarkgraystyle
1895 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1896 # import qdarkstyle
1897 #
1898 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1899 #
1900 #
1901 # except ImportError:
1902 # logger.info(
1903 # 'Module qdarkgraystyle not available.\n'
1904 # 'If wanted, install qdarkstyle with "pip install '
1905 # 'qdarkgraystyle".')
1906 #
1907 win = SparrowViewer(*args, **kwargs)
1909 gui_util.app.install_sigint_handler()
1910 gui_util.app.exec_()
1911 gui_util.app.uninstall_sigint_handler()
1913 del win
1915 gc.collect()
1917 del gui_util.app