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.dataset 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 widget.setText('%g, %g, %g' % (
1282 state.lat, state.lon, state.depth / km))
1284 def lineedit_to_lat_lon_depth(widget, state):
1285 self.disable_capture()
1286 try:
1287 s = str(widget.text())
1288 choices = location_to_choices(s)
1289 if len(choices) > 0:
1290 self.state.lat, self.state.lon, self.state.depth = \
1291 choices[0].get_lat_lon_depth()
1292 else:
1293 raise NoLocationChoices(s)
1295 finally:
1296 self.enable_capture()
1298 self._state_bind(
1299 ['lat', 'lon', 'depth'],
1300 lineedit_to_lat_lon_depth,
1301 le, [le.editingFinished, le.returnPressed],
1302 lat_lon_depth_to_lineedit)
1304 self.lat_lon_lineedit = le
1306 # focal point
1308 cb = qw.QCheckBox('Fix')
1309 cb.setStatusTip(
1310 'Fix location. Orbit focal point without pressing %s.'
1311 % g_modifier_key)
1312 layout.addWidget(cb, 1, 1, 1, 1)
1314 def focal_point_to_checkbox(state, widget):
1315 widget.blockSignals(True)
1316 widget.setChecked(self.gui_state.focal_point != 'center')
1317 widget.blockSignals(False)
1319 def checkbox_to_focal_point(widget, state):
1320 self.gui_state.focal_point = \
1321 'target' if widget.isChecked() else 'center'
1323 self._gui_state_bind(
1324 ['focal_point'], checkbox_to_focal_point,
1325 cb, [cb.toggled], focal_point_to_checkbox)
1327 self.focal_point_checkbox = cb
1329 self.talkie_connect(
1330 self.gui_state, 'focal_point', self.update_focal_point)
1332 self.update_focal_point()
1334 # strike, dip
1336 layout.addWidget(
1337 qw.QLabel('View Plane'), 2, 0, 1, 2)
1339 le = qw.QLineEdit()
1340 le.setStatusTip(
1341 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1342 'direction.')
1343 layout.addWidget(le, 3, 0, 1, 1)
1345 def strike_dip_to_lineedit(state, widget):
1346 widget.setText('%g, %g' % (state.strike, state.dip))
1348 def lineedit_to_strike_dip(widget, state):
1349 s = str(widget.text())
1350 string_to_strike_dip = {
1351 'east': (0., 90.),
1352 'west': (180., 90.),
1353 'south': (90., 90.),
1354 'north': (270., 90.),
1355 'top': (90., 0.),
1356 'bottom': (90., 180.)}
1358 self.disable_capture()
1359 if s in string_to_strike_dip:
1360 state.strike, state.dip = string_to_strike_dip[s]
1362 s = s.replace(',', ' ')
1363 try:
1364 state.strike, state.dip = map(float, s.split())
1365 except Exception:
1366 raise ValueError('need two numerical values: <strike>, <dip>')
1367 finally:
1368 self.enable_capture()
1370 self._state_bind(
1371 ['strike', 'dip'], lineedit_to_strike_dip,
1372 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1374 self.strike_dip_lineedit = le
1376 but = qw.QPushButton('Reset')
1377 but.setStatusTip('Reset to north-up map view.')
1378 but.clicked.connect(self.reset_strike_dip)
1379 layout.addWidget(but, 3, 1, 1, 1)
1381 # crosshair
1383 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1384 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1386 # camera bindings
1387 self.talkie_connect(
1388 self.state,
1389 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1390 self.update_camera)
1392 self.talkie_connect(
1393 self.gui_state, 'panels_visible', self.update_panel_visibility)
1395 return frame
1397 def controls_time(self):
1398 frame = qw.QFrame(self)
1399 frame.setSizePolicy(
1400 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1402 layout = qw.QGridLayout()
1403 frame.setLayout(layout)
1405 layout.addWidget(qw.QLabel('Min'), 0, 0)
1406 le_tmin = qw.QLineEdit()
1407 layout.addWidget(le_tmin, 0, 1)
1409 layout.addWidget(qw.QLabel('Max'), 1, 0)
1410 le_tmax = qw.QLineEdit()
1411 layout.addWidget(le_tmax, 1, 1)
1413 label_tcursor = qw.QLabel()
1415 label_tcursor.setSizePolicy(
1416 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1418 layout.addWidget(label_tcursor, 2, 1)
1419 self._label_tcursor = label_tcursor
1421 def time_to_lineedit(state, attribute, widget):
1422 widget.setText(
1423 common.time_or_none_to_str(getattr(state, attribute)))
1425 def lineedit_to_time(widget, state, attribute):
1426 from pyrocko.util import str_to_time_fillup
1428 s = str(widget.text())
1429 if not s.strip():
1430 setattr(state, attribute, None)
1431 else:
1432 try:
1433 setattr(state, attribute, str_to_time_fillup(s))
1434 except Exception:
1435 raise ValueError(
1436 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1438 self._state_bind(
1439 ['tmin'], lineedit_to_time, le_tmin,
1440 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1441 attribute='tmin')
1442 self._state_bind(
1443 ['tmax'], lineedit_to_time, le_tmax,
1444 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1445 attribute='tmax')
1447 self.tmin_lineedit = le_tmin
1448 self.tmax_lineedit = le_tmax
1450 range_edit = RangeEdit()
1451 range_edit.rangeEditPressed.connect(self.disable_capture)
1452 range_edit.rangeEditReleased.connect(self.enable_capture)
1453 range_edit.set_data_provider(self)
1454 range_edit.set_data_name('time')
1456 xblock = [False]
1458 def range_to_range_edit(state, widget):
1459 if not xblock[0]:
1460 widget.blockSignals(True)
1461 widget.set_focus(state.tduration, state.tposition)
1462 widget.set_range(state.tmin, state.tmax)
1463 widget.blockSignals(False)
1465 def range_edit_to_range(widget, state):
1466 xblock[0] = True
1467 self.state.tduration, self.state.tposition = widget.get_focus()
1468 self.state.tmin, self.state.tmax = widget.get_range()
1469 xblock[0] = False
1471 self._state_bind(
1472 ['tmin', 'tmax', 'tduration', 'tposition'],
1473 range_edit_to_range,
1474 range_edit,
1475 [range_edit.rangeChanged, range_edit.focusChanged],
1476 range_to_range_edit)
1478 def handle_tcursor_changed():
1479 self.gui_state.tcursor = range_edit.get_tcursor()
1481 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1483 layout.addWidget(range_edit, 3, 0, 1, 2)
1485 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1486 le_focus = qw.QLineEdit()
1487 layout.addWidget(le_focus, 4, 1)
1489 def focus_to_lineedit(state, widget):
1490 if state.tduration is None:
1491 widget.setText('')
1492 else:
1493 widget.setText('%s, %g' % (
1494 guts.str_duration(state.tduration),
1495 state.tposition))
1497 def lineedit_to_focus(widget, state):
1498 s = str(widget.text())
1499 w = [x.strip() for x in s.split(',')]
1500 try:
1501 if len(w) == 0 or not w[0]:
1502 state.tduration = None
1503 state.tposition = 0.0
1504 else:
1505 state.tduration = guts.parse_duration(w[0])
1506 if len(w) > 1:
1507 state.tposition = float(w[1])
1508 else:
1509 state.tposition = 0.0
1511 except Exception:
1512 raise ValueError('need two values: <duration>, <position>')
1514 self._state_bind(
1515 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1516 [le_focus.editingFinished, le_focus.returnPressed],
1517 focus_to_lineedit)
1519 label_effective_tmin = qw.QLabel()
1520 label_effective_tmax = qw.QLabel()
1522 label_effective_tmin.setSizePolicy(
1523 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1524 label_effective_tmax.setSizePolicy(
1525 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1526 label_effective_tmin.setMinimumSize(
1527 qg.QFontMetrics(label_effective_tmin.font()).width(
1528 '0000-00-00 00:00:00.000 '), 0)
1530 layout.addWidget(label_effective_tmin, 5, 1)
1531 layout.addWidget(label_effective_tmax, 6, 1)
1533 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1534 self.talkie_connect(
1535 self.state, var, self.update_effective_time_labels)
1537 self._label_effective_tmin = label_effective_tmin
1538 self._label_effective_tmax = label_effective_tmax
1540 self.talkie_connect(
1541 self.gui_state, 'tcursor', self.update_tcursor)
1543 return frame
1545 def controls_appearance(self):
1546 frame = qw.QFrame(self)
1547 frame.setSizePolicy(
1548 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1549 layout = qw.QGridLayout()
1550 frame.setLayout(layout)
1552 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1554 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1555 layout.addWidget(cb, 0, 1)
1556 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1558 self.talkie_connect(
1559 self.state, 'lighting', self.update_render_settings)
1561 # background
1563 layout.addWidget(qw.QLabel('Background'), 1, 0)
1565 cb = common.strings_to_combobox(
1566 ['black', 'white', 'skyblue1 - white'])
1568 layout.addWidget(cb, 1, 1)
1569 vstate.state_bind_combobox_background(
1570 self, self.state, 'background', cb)
1572 self.talkie_connect(
1573 self.state, 'background', self.update_render_settings)
1575 return frame
1577 def controls_snapshots(self):
1578 return snapshots_mod.SnapshotsPanel(self)
1580 def update_effective_time_labels(self, *args):
1581 tmin = self.state.tmin_effective
1582 tmax = self.state.tmax_effective
1584 stmin = common.time_or_none_to_str(tmin)
1585 stmax = common.time_or_none_to_str(tmax)
1587 self._label_effective_tmin.setText(stmin)
1588 self._label_effective_tmax.setText(stmax)
1590 def update_tcursor(self, *args):
1591 tcursor = self.gui_state.tcursor
1592 stcursor = common.time_or_none_to_str(tcursor)
1593 self._label_tcursor.setText(stcursor)
1595 def reset_strike_dip(self, *args):
1596 self.state.strike = 90.
1597 self.state.dip = 0
1598 self.gui_state.focal_point = 'center'
1600 def get_camera_geometry(self):
1602 def rtp2xyz(rtp):
1603 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1605 radius = 1.0 - self.state.depth / self.planet_radius
1607 cam_rtp = num.array([
1608 radius+self.state.distance,
1609 self.state.lat * d2r + 0.5*num.pi,
1610 self.state.lon * d2r])
1611 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1612 cam, up, foc = \
1613 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1615 foc_rtp = num.array([
1616 radius,
1617 self.state.lat * d2r + 0.5*num.pi,
1618 self.state.lon * d2r])
1620 foc = rtp2xyz(foc_rtp)
1622 rot_world = pmt.euler_to_matrix(
1623 -(self.state.lat-90.)*d2r,
1624 (self.state.lon+90.)*d2r,
1625 0.0*d2r).T
1627 rot_cam = pmt.euler_to_matrix(
1628 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1630 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1632 cam = foc + num.dot(rot, cam - foc)
1633 up = num.dot(rot, up)
1634 return cam, up, foc
1636 def update_camera(self, *args):
1637 cam, up, foc = self.get_camera_geometry()
1638 camera = self.ren.GetActiveCamera()
1639 camera.SetPosition(*cam)
1640 camera.SetFocalPoint(*foc)
1641 camera.SetViewUp(*up)
1643 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1645 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1646 self.feature_radius_min / self.planet_radius)**2))
1648 # if horizon == 0.0:
1649 # horizon = 2.0 + self.state.distance
1651 # clip_dist = max(min(self.state.distance*5., max(
1652 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1653 # , math.sqrt(num.sum(cam**2)))
1654 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1655 # clip_dist = feature_horizon
1657 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1659 self.camera_params = (
1660 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1662 self.update_view()
1664 def add_panel(
1665 self, title_label, panel,
1666 visible=False,
1667 # volatile=False,
1668 tabify=True,
1669 where=qc.Qt.RightDockWidgetArea,
1670 remove=None,
1671 title_controls=[]):
1673 dockwidget = common.MyDockWidget(
1674 self, title_label, title_controls=title_controls)
1676 if not visible:
1677 dockwidget.hide()
1679 if not self.gui_state.panels_visible:
1680 dockwidget.block()
1682 dockwidget.setWidget(panel)
1684 panel.setParent(dockwidget)
1686 dockwidgets = self.findChildren(common.MyDockWidget)
1687 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1689 self.addDockWidget(where, dockwidget)
1691 nwrap = 4
1692 if dws and len(dws) >= nwrap and tabify:
1693 self.tabifyDockWidget(
1694 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1696 mitem = dockwidget.toggleViewAction()
1698 def update_label(*args):
1699 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1700 self.update_slug_abbreviated_lengths()
1702 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1703 dockwidget.titlebar._title_label.title_changed.connect(
1704 self.update_slug_abbreviated_lengths)
1706 update_label()
1708 self._panel_togglers[dockwidget] = mitem
1709 self.panels_menu.addAction(mitem)
1710 if visible:
1711 dockwidget.setVisible(True)
1712 dockwidget.setFocus()
1713 dockwidget.raise_()
1715 def stack_panels(self):
1716 dockwidgets = self.findChildren(common.MyDockWidget)
1717 by_area = defaultdict(list)
1718 for dw in dockwidgets:
1719 area = self.dockWidgetArea(dw)
1720 by_area[area].append(dw)
1722 for dockwidgets in by_area.values():
1723 dw_last = None
1724 for dw in dockwidgets:
1725 if dw_last is not None:
1726 self.tabifyDockWidget(dw_last, dw)
1728 dw_last = dw
1730 def update_slug_abbreviated_lengths(self):
1731 dockwidgets = self.findChildren(common.MyDockWidget)
1732 title_labels = []
1733 for dw in dockwidgets:
1734 title_labels.append(dw.titlebar._title_label)
1736 by_title = defaultdict(list)
1737 for tl in title_labels:
1738 by_title[tl.get_title()].append(tl)
1740 for group in by_title.values():
1741 slugs = [tl.get_slug() for tl in group]
1743 n = max(len(slug) for slug in slugs)
1744 nunique = len(set(slugs))
1746 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1747 n -= 1
1749 if n > 0:
1750 n = max(3, n)
1752 for tl in group:
1753 tl.set_slug_abbreviated_length(n)
1755 def raise_panel(self, panel):
1756 dockwidget = panel.parent()
1757 dockwidget.setVisible(True)
1758 dockwidget.setFocus()
1759 dockwidget.raise_()
1761 def toggle_panel_visibility(self):
1762 self.gui_state.panels_visible = not self.gui_state.panels_visible
1764 def update_panel_visibility(self, *args):
1765 self.setUpdatesEnabled(False)
1766 mbar = self.menuBar()
1767 sbar = self.statusBar()
1768 dockwidgets = self.findChildren(common.MyDockWidget)
1770 # Set height to zero instead of hiding so that shortcuts still work
1771 # otherwise one would have to mess around with separate QShortcut
1772 # objects.
1773 mbar.setFixedHeight(
1774 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1776 sbar.setVisible(self.gui_state.panels_visible)
1777 for dockwidget in dockwidgets:
1778 dockwidget.setBlocked(not self.gui_state.panels_visible)
1780 self.setUpdatesEnabled(True)
1782 def remove_panel(self, panel):
1783 dockwidget = panel.parent()
1784 self.removeDockWidget(dockwidget)
1785 dockwidget.setParent(None)
1786 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1788 def register_data_provider(self, provider):
1789 if provider not in self.data_providers:
1790 self.data_providers.append(provider)
1792 def unregister_data_provider(self, provider):
1793 if provider in self.data_providers:
1794 self.data_providers.remove(provider)
1796 def iter_data(self, name):
1797 for provider in self.data_providers:
1798 for data in provider.iter_data(name):
1799 yield data
1801 def closeEvent(self, event):
1802 self.attach()
1803 event.accept()
1804 self.closing = True
1805 common.get_app().set_main_window(None)
1807 def is_closing(self):
1808 return self.closing
1811class SparrowApp(qw.QApplication):
1812 def __init__(self):
1813 qw.QApplication.__init__(self, ['Sparrow'])
1814 self.lastWindowClosed.connect(self.myQuit)
1815 self._main_window = None
1816 self.setApplicationDisplayName('Sparrow')
1817 self.setDesktopFileName('Sparrow')
1819 def install_sigint_handler(self):
1820 self._old_signal_handler = signal.signal(
1821 signal.SIGINT, self.myCloseAllWindows)
1823 def uninstall_sigint_handler(self):
1824 signal.signal(signal.SIGINT, self._old_signal_handler)
1826 def myQuit(self, *args):
1827 self.quit()
1829 def myCloseAllWindows(self, *args):
1830 self.closeAllWindows()
1832 def set_main_window(self, win):
1833 self._main_window = win
1835 def get_main_window(self):
1836 return self._main_window
1838 def get_progressbars(self):
1839 if self._main_window:
1840 return self._main_window.progressbars
1841 else:
1842 return None
1844 def status(self, message, duration=None):
1845 win = self.get_main_window()
1846 if not win:
1847 return
1849 win.statusBar().showMessage(
1850 message, int((duration or 0) * 1000))
1853def main(*args, **kwargs):
1855 from pyrocko import util
1856 from pyrocko.gui import util as gui_util
1857 util.setup_logging('sparrow', 'info')
1859 global win
1861 if gui_util.app is None:
1862 gui_util.app = SparrowApp()
1864 # try:
1865 # from qt_material import apply_stylesheet
1866 #
1867 # apply_stylesheet(app, theme='dark_teal.xml')
1868 #
1869 #
1870 # import qdarkgraystyle
1871 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1872 # import qdarkstyle
1873 #
1874 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1875 #
1876 #
1877 # except ImportError:
1878 # logger.info(
1879 # 'Module qdarkgraystyle not available.\n'
1880 # 'If wanted, install qdarkstyle with "pip install '
1881 # 'qdarkgraystyle".')
1882 #
1883 win = SparrowViewer(*args, **kwargs)
1885 gui_util.app.install_sigint_handler()
1886 gui_util.app.exec_()
1887 gui_util.app.uninstall_sigint_handler()
1889 del win
1891 gc.collect()
1893 del gui_util.app