1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import gc
8import logging
9import time
10import tempfile
11import os
12import shutil
13import platform
14from collections import defaultdict
15from subprocess import check_call
17import numpy as num
19from pyrocko import cake
20from pyrocko import guts
21from pyrocko.dataset import geonames
22from pyrocko import config
23from pyrocko import moment_tensor as pmt
24from pyrocko import util
26from pyrocko.gui.util import Progressbars, RangeEdit
27from pyrocko.gui.talkie import TalkieConnectionOwner, equal as state_equal
28from pyrocko.gui.qt_compat import qw, qc, qg
29# from pyrocko.gui import vtk_util
31from . import common, light, snapshots as snapshots_mod
33import vtk
34import vtk.qt
35vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
37from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
39from pyrocko import geometry # noqa
40from . import state as vstate, elements # noqa
42logger = logging.getLogger('pyrocko.gui.sparrow.main')
45d2r = num.pi/180.
46km = 1000.
48if platform.uname()[0] == 'Darwin':
49 g_modifier_key = '\u2318'
50else:
51 g_modifier_key = 'Ctrl'
54class ZeroFrame(qw.QFrame):
56 def sizeHint(self):
57 return qc.QSize(0, 0)
60class LocationChoice(object):
61 def __init__(self, name, lat, lon, depth=0):
62 self._name = name
63 self._lat = lat
64 self._lon = lon
65 self._depth = depth
67 def get_lat_lon_depth(self):
68 return self._lat, self._lon, self._depth
71def location_to_choices(s):
72 choices = []
73 s_vals = s.replace(',', ' ')
74 try:
75 vals = [float(x) for x in s_vals.split()]
76 if len(vals) == 3:
77 vals[2] *= km
79 choices.append(LocationChoice('', *vals))
81 except ValueError:
82 cities = geonames.get_cities_by_name(s.strip())
83 for c in cities:
84 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
86 return choices
89class NoLocationChoices(Exception):
91 def __init__(self, s):
92 self._string = s
94 def __str__(self):
95 return 'No location choices for string "%s"' % self._string
98class QVTKWidget(QVTKRenderWindowInteractor):
99 def __init__(self, viewer, *args):
100 QVTKRenderWindowInteractor.__init__(self, *args)
101 self._viewer = viewer
102 self._ctrl_state = False
104 def wheelEvent(self, event):
105 return self._viewer.myWheelEvent(event)
107 def keyPressEvent(self, event):
108 if event.key() == qc.Qt.Key_Control:
109 self._update_ctrl_state(True)
110 QVTKRenderWindowInteractor.keyPressEvent(self, event)
112 def keyReleaseEvent(self, event):
113 if event.key() == qc.Qt.Key_Control:
114 self._update_ctrl_state(False)
115 QVTKRenderWindowInteractor.keyReleaseEvent(self, event)
117 def focusInEvent(self, event):
118 self._update_ctrl_state()
119 QVTKRenderWindowInteractor.focusInEvent(self, event)
121 def focusOutEvent(self, event):
122 self._update_ctrl_state(False)
123 QVTKRenderWindowInteractor.focusOutEvent(self, event)
125 def mousePressEvent(self, event):
126 self._viewer.disable_capture()
127 QVTKRenderWindowInteractor.mousePressEvent(self, event)
129 def mouseReleaseEvent(self, event):
130 self._viewer.enable_capture()
131 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event)
133 def _update_ctrl_state(self, state=None):
134 if state is None:
135 app = common.get_app()
136 if not app:
137 return
138 state = app.keyboardModifiers() == qc.Qt.ControlModifier
139 if self._ctrl_state != state:
140 self._viewer.gui_state.next_focal_point()
141 self._ctrl_state = state
143 def container_resized(self, ev):
144 self._viewer.update_vtk_widget_size()
147class DetachedViewer(qw.QMainWindow):
149 def __init__(self, main_window, vtk_frame):
150 qw.QMainWindow.__init__(self, main_window)
151 self.main_window = main_window
152 self.setWindowTitle('Sparrow View')
153 vtk_frame.setParent(self)
154 self.setCentralWidget(vtk_frame)
156 def closeEvent(self, ev):
157 ev.ignore()
158 self.main_window.attach()
161class CenteringScrollArea(qw.QScrollArea):
162 def __init__(self):
163 qw.QScrollArea.__init__(self)
164 self.setAlignment(qc.Qt.AlignCenter)
165 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
166 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
167 self.setFrameShape(qw.QFrame.NoFrame)
169 def resizeEvent(self, ev):
170 retval = qw.QScrollArea.resizeEvent(self, ev)
171 self.widget().container_resized(ev)
172 return retval
174 def recenter(self):
175 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
176 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
178 def wheelEvent(self, *args, **kwargs):
179 return self.widget().wheelEvent(*args, **kwargs)
182class YAMLEditor(qw.QTextEdit):
184 def __init__(self, parent):
185 qw.QTextEdit.__init__(self)
186 self._parent = parent
188 def event(self, ev):
189 if isinstance(ev, qg.QKeyEvent) \
190 and ev.key() == qc.Qt.Key_Return \
191 and ev.modifiers() & qc.Qt.ShiftModifier:
192 self._parent.state_changed()
193 return True
195 return qw.QTextEdit.event(self, ev)
198class StateEditor(qw.QFrame, TalkieConnectionOwner):
199 def __init__(self, viewer, *args, **kwargs):
200 qw.QFrame.__init__(self, *args, **kwargs)
201 TalkieConnectionOwner.__init__(self)
203 layout = qw.QGridLayout()
205 self.setLayout(layout)
207 self.source_editor = YAMLEditor(self)
208 self.source_editor.setAcceptRichText(False)
209 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
210 font = qg.QFont("Monospace")
211 self.source_editor.setCurrentFont(font)
212 layout.addWidget(self.source_editor, 0, 0, 1, 2)
214 self.error_display_label = qw.QLabel('Error')
215 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
217 self.error_display = qw.QTextEdit()
218 self.error_display.setCurrentFont(font)
219 self.error_display.setReadOnly(True)
221 self.error_display.setSizePolicy(
222 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
224 self.error_display_label.hide()
225 self.error_display.hide()
227 layout.addWidget(self.error_display, 2, 0, 1, 2)
229 self.instant_updates = qw.QCheckBox('Instant Updates')
230 self.instant_updates.toggled.connect(self.state_changed)
231 layout.addWidget(self.instant_updates, 3, 0)
233 button = qw.QPushButton('Apply')
234 button.clicked.connect(self.state_changed)
235 layout.addWidget(button, 3, 1)
237 self.viewer = viewer
238 # recommended way, but resulted in a variable-width font being used:
239 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
240 self.bind_state()
241 self.source_editor.textChanged.connect(self.text_changed_handler)
242 self.destroyed.connect(self.unbind_state)
243 self.bind_state()
245 def bind_state(self, *args):
246 self.talkie_connect(self.viewer.state, '', self.update_state)
247 self.update_state()
249 def unbind_state(self):
250 self.talkie_disconnect_all()
252 def update_state(self, *args):
253 cursor = self.source_editor.textCursor()
255 cursor_position = cursor.position()
256 vsb_position = self.source_editor.verticalScrollBar().value()
257 hsb_position = self.source_editor.horizontalScrollBar().value()
259 self.source_editor.setPlainText(str(self.viewer.state))
261 cursor.setPosition(cursor_position)
262 self.source_editor.setTextCursor(cursor)
263 self.source_editor.verticalScrollBar().setValue(vsb_position)
264 self.source_editor.horizontalScrollBar().setValue(hsb_position)
266 def text_changed_handler(self, *args):
267 if self.instant_updates.isChecked():
268 self.state_changed()
270 def state_changed(self):
271 try:
272 s = self.source_editor.toPlainText()
273 state = guts.load(string=s)
274 self.viewer.set_state(state)
275 self.error_display.setPlainText('')
276 self.error_display_label.hide()
277 self.error_display.hide()
279 except Exception as e:
280 self.error_display.show()
281 self.error_display_label.show()
282 self.error_display.setPlainText(str(e))
285class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
286 def __init__(
287 self,
288 use_depth_peeling=True,
289 events=None,
290 snapshots=None,
291 instant_close=False):
293 common.set_viewer(self)
295 qw.QMainWindow.__init__(self)
296 TalkieConnectionOwner.__init__(self)
298 self.instant_close = instant_close
300 self.state = vstate.ViewerState()
301 self.gui_state = vstate.ViewerGuiState()
303 self.setWindowTitle('Sparrow')
305 self.setTabPosition(
306 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
308 self.planet_radius = cake.earthradius
309 self.feature_radius_min = cake.earthradius - 1000. * km
311 self._block_capture = 0
312 self._undo_stack = []
313 self._redo_stack = []
314 self._undo_aggregate = None
316 self._panel_togglers = {}
317 self._actors = set()
318 self._actors_2d = set()
319 self._render_window_size = (0, 0)
320 self._use_depth_peeling = use_depth_peeling
321 self._in_update_elements = False
322 self._update_elements_enabled = True
324 self._animation_tstart = None
325 self._animation_iframe = None
326 self._animation = None
328 mbar = qw.QMenuBar()
329 self.setMenuBar(mbar)
331 menu = mbar.addMenu('File')
333 menu.addAction(
334 'Export Image...',
335 self.export_image,
336 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
337 qc.Qt.ApplicationShortcut)
339 menu.addAction(
340 'Quit',
341 self.close,
342 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
343 qc.Qt.ApplicationShortcut)
345 menu = mbar.addMenu('Edit')
347 menu.addAction(
348 'Undo',
349 self.undo,
350 qg.QKeySequence(
351 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext(
352 qc.Qt.ApplicationShortcut)
354 menu.addAction(
355 'Redo',
356 self.redo,
357 qg.QKeySequence(
358 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext(
359 qc.Qt.ApplicationShortcut)
361 menu = mbar.addMenu('View')
362 menu_sizes = menu.addMenu('Size')
363 self._add_vtk_widget_size_menu_entries(menu_sizes)
365 # detached/attached
366 self.talkie_connect(
367 self.gui_state, 'detached', self.update_detached)
369 action = qw.QAction('Detach')
370 action.setCheckable(True)
371 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
372 action.setShortcutContext(qc.Qt.ApplicationShortcut)
374 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
375 menu.addAction(action)
377 self.panels_menu = mbar.addMenu('Panels')
378 self.panels_menu.addAction(
379 'Stack Panels',
380 self.stack_panels)
381 self.panels_menu.addSeparator()
383 snapshots_menu = mbar.addMenu('Snapshots')
385 menu = mbar.addMenu('Elements')
386 for name, estate in sorted([
387 ('Icosphere', elements.IcosphereState(
388 level=4,
389 smooth=True,
390 opacity=0.5,
391 ambient=0.1)),
392 ('Grid', elements.GridState()),
393 ('Stations', elements.StationsState()),
394 ('Topography', elements.TopoState()),
395 ('Custom Topography', elements.CustomTopoState()),
396 ('Catalog', elements.CatalogState()),
397 ('Coastlines', elements.CoastlinesState()),
398 ('Source', elements.SourceState()),
399 ('HUD Subtitle', elements.HudState(
400 template='Subtitle')),
401 ('HUD (tmax_effective)', elements.HudState(
402 template='tmax: {tmax_effective|date}',
403 position='top-left')),
404 ('Volcanoes', elements.VolcanoesState()),
405 ('Faults', elements.ActiveFaultsState()),
406 ('Plate bounds', elements.PlatesBoundsState()),
407 ('InSAR Surface Displacements', elements.KiteState()),
408 ('Geometry', elements.GeometryState()),
409 ('Spheroid', elements.SpheroidState())]):
411 def wrap_add_element(estate):
412 def add_element(*args):
413 new_element = guts.clone(estate)
414 new_element.element_id = elements.random_id()
415 self.state.elements.append(new_element)
416 self.state.sort_elements()
418 return add_element
420 mitem = qw.QAction(name, self)
422 mitem.triggered.connect(wrap_add_element(estate))
424 menu.addAction(mitem)
426 menu = mbar.addMenu('Help')
428 menu.addAction(
429 'Interactive Tour',
430 self.start_tour)
432 menu.addAction(
433 'Online Manual',
434 self.open_manual)
436 self.data_providers = []
437 self.elements = {}
439 self.detached_window = None
441 self.main_frame = qw.QFrame()
442 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
444 self.vtk_frame = CenteringScrollArea()
446 self.vtk_widget = QVTKWidget(self, self)
447 self.vtk_frame.setWidget(self.vtk_widget)
449 self.main_layout = qw.QVBoxLayout()
450 self.main_layout.setContentsMargins(0, 0, 0, 0)
451 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
453 pb = Progressbars(self)
454 self.progressbars = pb
455 self.main_layout.addWidget(pb)
457 self.main_frame.setLayout(self.main_layout)
459 self.vtk_frame_substitute = None
461 self.add_panel(
462 'Navigation',
463 self.controls_navigation(), visible=True,
464 where=qc.Qt.LeftDockWidgetArea)
466 self.add_panel(
467 'Time',
468 self.controls_time(), visible=True,
469 where=qc.Qt.LeftDockWidgetArea)
471 self.add_panel(
472 'Appearance',
473 self.controls_appearance(), visible=True,
474 where=qc.Qt.LeftDockWidgetArea)
476 snapshots_panel = self.controls_snapshots()
477 self.snapshots_panel = snapshots_panel
478 self.add_panel(
479 'Snapshots',
480 snapshots_panel, visible=False,
481 where=qc.Qt.LeftDockWidgetArea)
483 snapshots_panel.setup_menu(snapshots_menu)
485 self.setCentralWidget(self.main_frame)
487 self.mesh = None
489 ren = vtk.vtkRenderer()
491 # ren.SetBackground(0.15, 0.15, 0.15)
492 # ren.SetBackground(0.0, 0.0, 0.0)
493 # ren.TwoSidedLightingOn()
494 # ren.SetUseShadows(1)
496 self._lighting = None
497 self._background = None
499 self.ren = ren
500 self.update_render_settings()
501 self.update_camera()
503 renwin = self.vtk_widget.GetRenderWindow()
505 if self._use_depth_peeling:
506 renwin.SetAlphaBitPlanes(1)
507 renwin.SetMultiSamples(0)
509 ren.SetUseDepthPeeling(1)
510 ren.SetMaximumNumberOfPeels(100)
511 ren.SetOcclusionRatio(0.1)
513 ren.SetUseFXAA(1)
514 # ren.SetUseHiddenLineRemoval(1)
515 # ren.SetBackingStore(1)
517 self.renwin = renwin
519 # renwin.LineSmoothingOn()
520 # renwin.PointSmoothingOn()
521 # renwin.PolygonSmoothingOn()
523 renwin.AddRenderer(ren)
525 iren = renwin.GetInteractor()
526 iren.LightFollowCameraOn()
527 iren.SetInteractorStyle(None)
529 iren.AddObserver('LeftButtonPressEvent', self.button_event)
530 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
531 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
532 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
533 iren.AddObserver('RightButtonPressEvent', self.button_event)
534 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
535 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
536 iren.AddObserver('KeyPressEvent', self.key_down_event)
537 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
539 renwin.Render()
541 iren.Initialize()
543 self.iren = iren
545 self.rotating = False
547 self._elements = {}
548 self._elements_active = {}
550 self.talkie_connect(
551 self.state, 'elements', self.update_elements)
553 self.state.elements.append(elements.IcosphereState(
554 element_id='icosphere',
555 level=4,
556 smooth=True,
557 opacity=0.5,
558 ambient=0.1))
560 self.state.elements.append(elements.GridState(
561 element_id='grid'))
562 self.state.elements.append(elements.CoastlinesState(
563 element_id='coastlines'))
564 self.state.elements.append(elements.CrosshairState(
565 element_id='crosshair'))
567 # self.state.elements.append(elements.StationsState())
568 # self.state.elements.append(elements.SourceState())
569 # self.state.elements.append(
570 # elements.CatalogState(
571 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
572 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
574 if events:
575 self.state.elements.append(
576 elements.CatalogState(
577 selection=elements.MemoryCatalogSelection(events=events)))
579 self.state.sort_elements()
581 if snapshots:
582 snapshots_ = []
583 for obj in snapshots:
584 if isinstance(obj, str):
585 snapshots_.extend(snapshots_mod.load_snapshots(obj))
586 else:
587 snapshots_.append(obj)
589 snapshots_panel.add_snapshots(snapshots_)
590 self.raise_panel(snapshots_panel)
591 snapshots_panel.goto_snapshot(1)
593 self.timer = qc.QTimer(self)
594 self.timer.timeout.connect(self.periodical)
595 self.timer.setInterval(1000)
596 self.timer.start()
598 self._animation_saver = None
600 self.closing = False
601 self.vtk_widget.setFocus()
603 self.update_detached()
605 self.status(
606 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
608 self.status(
609 'Let\'s fly.', 2.0)
611 self.show()
612 self.windowHandle().showMaximized()
614 self.talkie_connect(
615 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
617 self.update_vtk_widget_size()
619 hatch_path = config.expand(os.path.join(
620 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
622 self.talkie_connect(self.state, '', self.capture_state)
623 self.capture_state()
625 if not os.path.exists(hatch_path):
626 with open(hatch_path, 'w') as f:
627 f.write('%s\n' % util.time_to_str(time.time()))
629 self.start_tour()
631 def status(self, message, duration=None):
632 self.statusBar().showMessage(
633 message, int((duration or 0) * 1000))
635 def disable_capture(self):
636 self._block_capture += 1
638 logger.debug('Undo capture block (+1): %i' % self._block_capture)
640 def enable_capture(self, drop=False, aggregate=None):
641 if self._block_capture > 0:
642 self._block_capture -= 1
644 logger.debug('Undo capture block (-1): %i' % self._block_capture)
646 if self._block_capture == 0 and not drop:
647 self.capture_state(aggregate=aggregate)
649 def capture_state(self, *args, aggregate=None):
650 if self._block_capture:
651 return
653 if len(self._undo_stack) == 0 or not state_equal(
654 self.state, self._undo_stack[-1]):
656 if aggregate is not None:
657 if aggregate == self._undo_aggregate:
658 self._undo_stack.pop()
660 self._undo_aggregate = aggregate
661 else:
662 self._undo_aggregate = None
664 logger.debug('Capture undo state (%i%s)\n%s' % (
665 len(self._undo_stack) + 1,
666 '' if aggregate is None else ', aggregate=%s' % aggregate,
667 '\n'.join(
668 ' - %s' % s
669 for s in self._undo_stack[-1].str_diff(
670 self.state).splitlines())
671 if len(self._undo_stack) > 0 else 'initial'))
673 self._undo_stack.append(guts.clone(self.state))
674 self._redo_stack.clear()
676 def undo(self):
677 self._undo_aggregate = None
679 if len(self._undo_stack) <= 1:
680 return
682 state = self._undo_stack.pop()
683 self._redo_stack.append(state)
684 state = self._undo_stack[-1]
686 logger.debug('Undo (%i)\n%s' % (
687 len(self._undo_stack),
688 '\n'.join(
689 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
691 self.disable_capture()
692 try:
693 self.set_state(state)
694 finally:
695 self.enable_capture(drop=True)
697 def redo(self):
698 self._undo_aggregate = None
700 if len(self._redo_stack) == 0:
701 return
703 state = self._redo_stack.pop()
704 self._undo_stack.append(state)
706 logger.debug('Redo (%i)\n%s' % (
707 len(self._redo_stack),
708 '\n'.join(
709 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
711 self.disable_capture()
712 try:
713 self.set_state(state)
714 finally:
715 self.enable_capture(drop=True)
717 def start_tour(self):
718 snapshots_ = snapshots_mod.load_snapshots(
719 'https://data.pyrocko.org/examples/'
720 'sparrow-tour-v0.1.snapshots.yaml')
721 self.snapshots_panel.add_snapshots(snapshots_)
722 self.raise_panel(self.snapshots_panel)
723 self.snapshots_panel.transition_to_next_snapshot()
725 def open_manual(self):
726 import webbrowser
727 webbrowser.open(
728 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
730 def _add_vtk_widget_size_menu_entries(self, menu):
732 group = qw.QActionGroup(menu)
733 group.setExclusive(True)
735 def set_variable_size():
736 self.gui_state.fixed_size = False
738 variable_size_action = menu.addAction('Fit Window Size')
739 variable_size_action.setCheckable(True)
740 variable_size_action.setActionGroup(group)
741 variable_size_action.triggered.connect(set_variable_size)
743 fixed_size_items = []
744 for nx, ny, label in [
745 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
746 (426, 240, ''),
747 (640, 360, ''),
748 (854, 480, '(FWVGA)'),
749 (1280, 720, '(HD)'),
750 (1920, 1080, '(Full HD)'),
751 (2560, 1440, '(Quad HD)'),
752 (3840, 2160, '(4K UHD)'),
753 (3840*2, 2160*2, '',),
754 (None, None, 'Aspect 4:3'),
755 (640, 480, '(VGA)'),
756 (800, 600, '(SVGA)'),
757 (None, None, 'Other'),
758 (512, 512, ''),
759 (1024, 1024, '')]:
761 if None in (nx, ny):
762 menu.addSection(label)
763 else:
764 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
765 action = menu.addAction(name)
766 action.setCheckable(True)
767 action.setActionGroup(group)
768 fixed_size_items.append((action, (nx, ny)))
770 def make_set_fixed_size(nx, ny):
771 def set_fixed_size():
772 self.gui_state.fixed_size = (float(nx), float(ny))
774 return set_fixed_size
776 action.triggered.connect(make_set_fixed_size(nx, ny))
778 def update_widget(*args):
779 for action, (nx, ny) in fixed_size_items:
780 action.blockSignals(True)
781 action.setChecked(
782 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
783 int(z) for z in self.gui_state.fixed_size)))
784 action.blockSignals(False)
786 variable_size_action.blockSignals(True)
787 variable_size_action.setChecked(not self.gui_state.fixed_size)
788 variable_size_action.blockSignals(False)
790 update_widget()
791 self.talkie_connect(
792 self.gui_state, 'fixed_size', update_widget)
794 def update_vtk_widget_size(self, *args):
795 if self.gui_state.fixed_size:
796 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
797 wanted_size = qc.QSize(nx, ny)
798 else:
799 wanted_size = qc.QSize(
800 self.vtk_frame.window().width(), self.vtk_frame.height())
802 current_size = self.vtk_widget.size()
804 if current_size.width() != wanted_size.width() \
805 or current_size.height() != wanted_size.height():
807 self.vtk_widget.setFixedSize(wanted_size)
809 self.vtk_frame.recenter()
810 self.check_vtk_resize()
812 def update_focal_point(self, *args):
813 if self.gui_state.focal_point == 'center':
814 self.vtk_widget.setStatusTip(
815 'Click and drag: change location. %s-click and drag: '
816 'change view plane orientation.' % g_modifier_key)
817 else:
818 self.vtk_widget.setStatusTip(
819 '%s-click and drag: change location. Click and drag: '
820 'change view plane orientation. Uncheck "Navigation: Fix" to '
821 'reverse sense.' % g_modifier_key)
823 def update_detached(self, *args):
825 if self.gui_state.detached and not self.detached_window: # detach
826 logger.debug('Detaching VTK view.')
828 self.main_layout.removeWidget(self.vtk_frame)
829 self.detached_window = DetachedViewer(self, self.vtk_frame)
830 self.detached_window.show()
831 self.vtk_widget.setFocus()
833 screens = common.get_app().screens()
834 if len(screens) > 1:
835 for screen in screens:
836 if screen is not self.screen():
837 self.detached_window.windowHandle().setScreen(screen)
838 # .setScreen() does not work reliably,
839 # therefore trying also with .move()...
840 p = screen.geometry().topLeft()
841 self.detached_window.move(p.x() + 50, p.y() + 50)
842 # ... but also does not work in notion window manager.
844 self.detached_window.windowHandle().showMaximized()
846 frame = qw.QFrame()
847 # frame.setFrameShape(qw.QFrame.NoFrame)
848 # frame.setBackgroundRole(qg.QPalette.Mid)
849 # frame.setAutoFillBackground(True)
850 frame.setSizePolicy(
851 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
853 layout = qw.QGridLayout()
854 frame.setLayout(layout)
855 self.main_layout.insertWidget(0, frame)
857 self.state_editor = StateEditor(self)
859 layout.addWidget(self.state_editor, 0, 0)
861 # attach_button = qw.QPushButton('Attach View')
862 # attach_button.clicked.connect(self.attach)
863 # layout.addWidget(
864 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
866 self.vtk_frame_substitute = frame
868 if not self.gui_state.detached and self.detached_window: # attach
869 logger.debug('Attaching VTK view.')
870 self.detached_window.hide()
871 self.vtk_frame.setParent(self)
872 if self.vtk_frame_substitute:
873 self.main_layout.removeWidget(self.vtk_frame_substitute)
874 self.state_editor.unbind_state()
875 self.vtk_frame_substitute = None
877 self.main_layout.insertWidget(0, self.vtk_frame)
878 self.detached_window = None
879 self.vtk_widget.setFocus()
881 def attach(self):
882 self.gui_state.detached = False
884 def export_image(self):
886 caption = 'Export Image'
887 fn_out, _ = qw.QFileDialog.getSaveFileName(
888 self, caption, 'image.png',
889 options=common.qfiledialog_options)
891 if fn_out:
892 self.save_image(fn_out)
894 def save_image(self, path):
896 original_fixed_size = self.gui_state.fixed_size
897 if original_fixed_size is None:
898 self.gui_state.fixed_size = (1920., 1080.)
900 wif = vtk.vtkWindowToImageFilter()
901 wif.SetInput(self.renwin)
902 wif.SetInputBufferTypeToRGBA()
903 wif.SetScale(1, 1)
904 wif.ReadFrontBufferOff()
905 writer = vtk.vtkPNGWriter()
906 writer.SetInputConnection(wif.GetOutputPort())
908 self.renwin.Render()
909 wif.Modified()
910 writer.SetFileName(path)
911 writer.Write()
913 self.gui_state.fixed_size = original_fixed_size
915 def update_render_settings(self, *args):
916 if self._lighting is None or self._lighting != self.state.lighting:
917 self.ren.RemoveAllLights()
918 for li in light.get_lights(self.state.lighting):
919 self.ren.AddLight(li)
921 self._lighting = self.state.lighting
923 if self._background is None \
924 or self._background != self.state.background:
926 self.state.background.vtk_apply(self.ren)
927 self._background = self.state.background
929 self.update_view()
931 def start_animation(self, interpolator, output_path=None):
932 if self._animation:
933 logger.debug('Aborting animation in progress to start a new one.')
934 self.stop_animation()
936 self.disable_capture()
937 self._animation = interpolator
938 if output_path is None:
939 self._animation_tstart = time.time()
940 self._animation_iframe = None
941 else:
942 self._animation_iframe = 0
943 self.showFullScreen()
944 self.update_view()
945 self.gui_state.panels_visible = False
946 self.update_view()
948 self._animation_timer = qc.QTimer(self)
949 self._animation_timer.timeout.connect(self.next_animation_frame)
950 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
951 self._animation_timer.start()
952 if output_path is not None:
953 original_fixed_size = self.gui_state.fixed_size
954 if original_fixed_size is None:
955 self.gui_state.fixed_size = (1920., 1080.)
957 wif = vtk.vtkWindowToImageFilter()
958 wif.SetInput(self.renwin)
959 wif.SetInputBufferTypeToRGBA()
960 wif.SetScale(1, 1)
961 wif.ReadFrontBufferOff()
962 writer = vtk.vtkPNGWriter()
963 temp_path = tempfile.mkdtemp()
964 self._animation_saver = (
965 wif, writer, temp_path, output_path, original_fixed_size)
966 writer.SetInputConnection(wif.GetOutputPort())
968 def next_animation_frame(self):
970 ani = self._animation
971 if not ani:
972 return
974 if self._animation_iframe is not None:
975 state = ani(
976 ani.tmin
977 + self._animation_iframe * ani.dt)
979 self._animation_iframe += 1
980 else:
981 tnow = time.time()
982 state = ani(min(
983 ani.tmax,
984 ani.tmin + (tnow - self._animation_tstart)))
986 self.set_state(state)
987 self.renwin.Render()
988 if self._animation_saver:
989 wif, writer, temp_path, _, _ = self._animation_saver
990 wif.Modified()
991 fn = os.path.join(temp_path, 'f%09i.png')
992 writer.SetFileName(fn % self._animation_iframe)
993 writer.Write()
995 if self._animation_iframe is not None:
996 t = self._animation_iframe * ani.dt
997 else:
998 t = tnow - self._animation_tstart
1000 if t > ani.tmax - ani.tmin:
1001 self.stop_animation()
1003 def stop_animation(self):
1004 if self._animation_timer:
1005 self._animation_timer.stop()
1007 if self._animation_saver:
1009 wif, writer, temp_path, output_path, original_fixed_size \
1010 = self._animation_saver
1011 self.gui_state.fixed_size = original_fixed_size
1013 fn_path = os.path.join(temp_path, 'f%09d.png')
1014 check_call([
1015 'ffmpeg', '-y',
1016 '-i', fn_path,
1017 '-c:v', 'libx264',
1018 '-preset', 'slow',
1019 '-crf', '17',
1020 '-vf', 'format=yuv420p,fps=%i' % (
1021 int(round(1.0/self._animation.dt))),
1022 output_path])
1023 shutil.rmtree(temp_path)
1025 self._animation_saver = None
1026 self._animation_saver
1028 self.showNormal()
1029 self.gui_state.panels_visible = True
1031 self._animation_tstart = None
1032 self._animation_iframe = None
1033 self._animation = None
1034 self.enable_capture()
1036 def set_state(self, state):
1037 self.disable_capture()
1038 try:
1039 self._update_elements_enabled = False
1040 self.setUpdatesEnabled(False)
1041 self.state.diff_update(state)
1042 self.state.sort_elements()
1043 self.setUpdatesEnabled(True)
1044 self._update_elements_enabled = True
1045 self.update_elements()
1046 finally:
1047 self.enable_capture()
1049 def periodical(self):
1050 pass
1052 def check_vtk_resize(self, *args):
1053 render_window_size = self.renwin.GetSize()
1054 if self._render_window_size != render_window_size:
1055 self._render_window_size = render_window_size
1056 self.resize_event(*render_window_size)
1058 def update_elements(self, *_):
1059 if not self._update_elements_enabled:
1060 return
1062 if self._in_update_elements:
1063 return
1065 self._in_update_elements = True
1066 for estate in self.state.elements:
1067 if estate.element_id not in self._elements:
1068 new_element = estate.create()
1069 logger.debug('Creating "%s" ("%s").' % (
1070 type(new_element).__name__,
1071 estate.element_id))
1072 self._elements[estate.element_id] = new_element
1074 element = self._elements[estate.element_id]
1076 if estate.element_id not in self._elements_active:
1077 logger.debug('Adding "%s" ("%s")' % (
1078 type(element).__name__,
1079 estate.element_id))
1080 element.bind_state(estate)
1081 element.set_parent(self)
1082 self._elements_active[estate.element_id] = element
1084 state_element_ids = [el.element_id for el in self.state.elements]
1085 deactivate = []
1086 for element_id, element in self._elements_active.items():
1087 if element_id not in state_element_ids:
1088 logger.debug('Removing "%s" ("%s").' % (
1089 type(element).__name__,
1090 element_id))
1091 element.unset_parent()
1092 deactivate.append(element_id)
1094 for element_id in deactivate:
1095 del self._elements_active[element_id]
1097 self._update_crosshair_bindings()
1099 self._in_update_elements = False
1101 def _update_crosshair_bindings(self):
1103 def get_crosshair_element():
1104 for element in self.state.elements:
1105 if element.element_id == 'crosshair':
1106 return element
1108 return None
1110 crosshair = get_crosshair_element()
1111 if crosshair is None or crosshair.is_connected:
1112 return
1114 def to_checkbox(state, widget):
1115 widget.blockSignals(True)
1116 widget.setChecked(state.visible)
1117 widget.blockSignals(False)
1119 def to_state(widget, state):
1120 state.visible = widget.isChecked()
1122 cb = self._crosshair_checkbox
1123 vstate.state_bind(
1124 self, crosshair, ['visible'], to_state,
1125 cb, [cb.toggled], to_checkbox)
1127 crosshair.is_connected = True
1129 def add_actor_2d(self, actor):
1130 if actor not in self._actors_2d:
1131 self.ren.AddActor2D(actor)
1132 self._actors_2d.add(actor)
1134 def remove_actor_2d(self, actor):
1135 if actor in self._actors_2d:
1136 self.ren.RemoveActor2D(actor)
1137 self._actors_2d.remove(actor)
1139 def add_actor(self, actor):
1140 if actor not in self._actors:
1141 self.ren.AddActor(actor)
1142 self._actors.add(actor)
1144 def add_actor_list(self, actorlist):
1145 for actor in actorlist:
1146 self.add_actor(actor)
1148 def remove_actor(self, actor):
1149 if actor in self._actors:
1150 self.ren.RemoveActor(actor)
1151 self._actors.remove(actor)
1153 def update_view(self):
1154 self.vtk_widget.update()
1156 def resize_event(self, size_x, size_y):
1157 self.gui_state.size = (size_x, size_y)
1159 def button_event(self, obj, event):
1160 if event == "LeftButtonPressEvent":
1161 self.rotating = True
1162 elif event == "LeftButtonReleaseEvent":
1163 self.rotating = False
1165 def mouse_move_event(self, obj, event):
1166 x0, y0 = self.iren.GetLastEventPosition()
1167 x, y = self.iren.GetEventPosition()
1169 size_x, size_y = self.renwin.GetSize()
1170 center_x = size_x / 2.0
1171 center_y = size_y / 2.0
1173 if self.rotating:
1174 self.do_rotate(x, y, x0, y0, center_x, center_y)
1176 def myWheelEvent(self, event):
1178 angle = event.angleDelta().y()
1180 if angle > 200:
1181 angle = 200
1183 if angle < -200:
1184 angle = -200
1186 self.disable_capture()
1187 try:
1188 self.do_dolly(-angle/100.)
1189 finally:
1190 self.enable_capture(aggregate='distance')
1192 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1194 dx = x0 - x
1195 dy = y0 - y
1197 phi = d2r*(self.state.strike - 90.)
1198 focp = self.gui_state.focal_point
1200 if focp == 'center':
1201 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1202 - math.sin(phi) * dx + math.cos(phi) * dy
1204 lat = self.state.lat
1205 lon = self.state.lon
1206 factor = self.state.distance / 10.0
1207 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1208 else:
1209 lat = 90. - self.state.dip
1210 lon = -self.state.strike - 90.
1211 factor = 0.5
1212 factor_lat = 1.0
1214 dlat = dy * factor
1215 dlon = dx * factor * factor_lat
1217 lat = max(min(lat + dlat, 90.), -90.)
1218 lon += dlon
1219 lon = (lon + 180.) % 360. - 180.
1221 if focp == 'center':
1222 self.state.lat = float(lat)
1223 self.state.lon = float(lon)
1224 else:
1225 self.state.dip = float(90. - lat)
1226 self.state.strike = float(-(lon + 90.))
1228 def do_dolly(self, v):
1229 self.state.distance *= float(1.0 + 0.1*v)
1231 def key_down_event(self, obj, event):
1232 k = obj.GetKeyCode()
1233 if k == 'f':
1234 self.gui_state.next_focal_point()
1236 elif k == 'r':
1237 self.reset_strike_dip()
1239 elif k == 'p':
1240 print(self.state)
1242 elif k == 'i':
1243 for elem in self.state.elements:
1244 if isinstance(elem, elements.IcosphereState):
1245 elem.visible = not elem.visible
1247 elif k == 'c':
1248 for elem in self.state.elements:
1249 if isinstance(elem, elements.CoastlinesState):
1250 elem.visible = not elem.visible
1252 elif k == 't':
1253 if not any(
1254 isinstance(elem, elements.TopoState)
1255 for elem in self.state.elements):
1257 self.state.elements.append(elements.TopoState())
1258 else:
1259 for elem in self.state.elements:
1260 if isinstance(elem, elements.TopoState):
1261 elem.visible = not elem.visible
1263 elif k == ' ':
1264 self.toggle_panel_visibility()
1266 def _state_bind(self, *args, **kwargs):
1267 vstate.state_bind(self, self.state, *args, **kwargs)
1269 def _gui_state_bind(self, *args, **kwargs):
1270 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1272 def controls_navigation(self):
1273 frame = qw.QFrame(self)
1274 frame.setSizePolicy(
1275 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1276 layout = qw.QGridLayout()
1277 frame.setLayout(layout)
1279 # lat, lon, depth
1281 layout.addWidget(
1282 qw.QLabel('Location'), 0, 0, 1, 2)
1284 le = qw.QLineEdit()
1285 le.setStatusTip(
1286 'Latitude, Longitude, Depth [km] or city name: '
1287 'Focal point location.')
1288 layout.addWidget(le, 1, 0, 1, 1)
1290 def lat_lon_depth_to_lineedit(state, widget):
1291 widget.setText('%g, %g, %g' % (
1292 state.lat, state.lon, state.depth / km))
1294 def lineedit_to_lat_lon_depth(widget, state):
1295 self.disable_capture()
1296 try:
1297 s = str(widget.text())
1298 choices = location_to_choices(s)
1299 if len(choices) > 0:
1300 self.state.lat, self.state.lon, self.state.depth = \
1301 choices[0].get_lat_lon_depth()
1302 else:
1303 raise NoLocationChoices(s)
1305 finally:
1306 self.enable_capture()
1308 self._state_bind(
1309 ['lat', 'lon', 'depth'],
1310 lineedit_to_lat_lon_depth,
1311 le, [le.editingFinished, le.returnPressed],
1312 lat_lon_depth_to_lineedit)
1314 self.lat_lon_lineedit = le
1316 # focal point
1318 cb = qw.QCheckBox('Fix')
1319 cb.setStatusTip(
1320 'Fix location. Orbit focal point without pressing %s.'
1321 % g_modifier_key)
1322 layout.addWidget(cb, 1, 1, 1, 1)
1324 def focal_point_to_checkbox(state, widget):
1325 widget.blockSignals(True)
1326 widget.setChecked(self.gui_state.focal_point != 'center')
1327 widget.blockSignals(False)
1329 def checkbox_to_focal_point(widget, state):
1330 self.gui_state.focal_point = \
1331 'target' if widget.isChecked() else 'center'
1333 self._gui_state_bind(
1334 ['focal_point'], checkbox_to_focal_point,
1335 cb, [cb.toggled], focal_point_to_checkbox)
1337 self.focal_point_checkbox = cb
1339 self.talkie_connect(
1340 self.gui_state, 'focal_point', self.update_focal_point)
1342 self.update_focal_point()
1344 # strike, dip
1346 layout.addWidget(
1347 qw.QLabel('View Plane'), 2, 0, 1, 2)
1349 le = qw.QLineEdit()
1350 le.setStatusTip(
1351 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1352 'direction.')
1353 layout.addWidget(le, 3, 0, 1, 1)
1355 def strike_dip_to_lineedit(state, widget):
1356 widget.setText('%g, %g' % (state.strike, state.dip))
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
1386 but = qw.QPushButton('Reset')
1387 but.setStatusTip('Reset to north-up map view.')
1388 but.clicked.connect(self.reset_strike_dip)
1389 layout.addWidget(but, 3, 1, 1, 1)
1391 # crosshair
1393 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1394 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1396 # camera bindings
1397 self.talkie_connect(
1398 self.state,
1399 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1400 self.update_camera)
1402 self.talkie_connect(
1403 self.gui_state, 'panels_visible', self.update_panel_visibility)
1405 return frame
1407 def controls_time(self):
1408 frame = qw.QFrame(self)
1409 frame.setSizePolicy(
1410 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1412 layout = qw.QGridLayout()
1413 frame.setLayout(layout)
1415 layout.addWidget(qw.QLabel('Min'), 0, 0)
1416 le_tmin = qw.QLineEdit()
1417 layout.addWidget(le_tmin, 0, 1)
1419 layout.addWidget(qw.QLabel('Max'), 1, 0)
1420 le_tmax = qw.QLineEdit()
1421 layout.addWidget(le_tmax, 1, 1)
1423 label_tcursor = qw.QLabel()
1425 label_tcursor.setSizePolicy(
1426 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1428 layout.addWidget(label_tcursor, 2, 1)
1429 self._label_tcursor = label_tcursor
1431 def time_to_lineedit(state, attribute, widget):
1432 widget.setText(
1433 common.time_or_none_to_str(getattr(state, attribute)))
1435 def lineedit_to_time(widget, state, attribute):
1436 from pyrocko.util import str_to_time_fillup
1438 s = str(widget.text())
1439 if not s.strip():
1440 setattr(state, attribute, None)
1441 else:
1442 try:
1443 setattr(state, attribute, str_to_time_fillup(s))
1444 except Exception:
1445 raise ValueError(
1446 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1448 self._state_bind(
1449 ['tmin'], lineedit_to_time, le_tmin,
1450 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1451 attribute='tmin')
1452 self._state_bind(
1453 ['tmax'], lineedit_to_time, le_tmax,
1454 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1455 attribute='tmax')
1457 self.tmin_lineedit = le_tmin
1458 self.tmax_lineedit = le_tmax
1460 range_edit = RangeEdit()
1461 range_edit.rangeEditPressed.connect(self.disable_capture)
1462 range_edit.rangeEditReleased.connect(self.enable_capture)
1463 range_edit.set_data_provider(self)
1464 range_edit.set_data_name('time')
1466 xblock = [False]
1468 def range_to_range_edit(state, widget):
1469 if not xblock[0]:
1470 widget.blockSignals(True)
1471 widget.set_focus(state.tduration, state.tposition)
1472 widget.set_range(state.tmin, state.tmax)
1473 widget.blockSignals(False)
1475 def range_edit_to_range(widget, state):
1476 xblock[0] = True
1477 self.state.tduration, self.state.tposition = widget.get_focus()
1478 self.state.tmin, self.state.tmax = widget.get_range()
1479 xblock[0] = False
1481 self._state_bind(
1482 ['tmin', 'tmax', 'tduration', 'tposition'],
1483 range_edit_to_range,
1484 range_edit,
1485 [range_edit.rangeChanged, range_edit.focusChanged],
1486 range_to_range_edit)
1488 def handle_tcursor_changed():
1489 self.gui_state.tcursor = range_edit.get_tcursor()
1491 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1493 layout.addWidget(range_edit, 3, 0, 1, 2)
1495 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1496 le_focus = qw.QLineEdit()
1497 layout.addWidget(le_focus, 4, 1)
1499 def focus_to_lineedit(state, widget):
1500 if state.tduration is None:
1501 widget.setText('')
1502 else:
1503 widget.setText('%s, %g' % (
1504 guts.str_duration(state.tduration),
1505 state.tposition))
1507 def lineedit_to_focus(widget, state):
1508 s = str(widget.text())
1509 w = [x.strip() for x in s.split(',')]
1510 try:
1511 if len(w) == 0 or not w[0]:
1512 state.tduration = None
1513 state.tposition = 0.0
1514 else:
1515 state.tduration = guts.parse_duration(w[0])
1516 if len(w) > 1:
1517 state.tposition = float(w[1])
1518 else:
1519 state.tposition = 0.0
1521 except Exception:
1522 raise ValueError('need two values: <duration>, <position>')
1524 self._state_bind(
1525 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1526 [le_focus.editingFinished, le_focus.returnPressed],
1527 focus_to_lineedit)
1529 label_effective_tmin = qw.QLabel()
1530 label_effective_tmax = qw.QLabel()
1532 label_effective_tmin.setSizePolicy(
1533 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1534 label_effective_tmax.setSizePolicy(
1535 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1536 label_effective_tmin.setMinimumSize(
1537 qg.QFontMetrics(label_effective_tmin.font()).width(
1538 '0000-00-00 00:00:00.000 '), 0)
1540 layout.addWidget(label_effective_tmin, 5, 1)
1541 layout.addWidget(label_effective_tmax, 6, 1)
1543 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1544 self.talkie_connect(
1545 self.state, var, self.update_effective_time_labels)
1547 self._label_effective_tmin = label_effective_tmin
1548 self._label_effective_tmax = label_effective_tmax
1550 self.talkie_connect(
1551 self.gui_state, 'tcursor', self.update_tcursor)
1553 return frame
1555 def controls_appearance(self):
1556 frame = qw.QFrame(self)
1557 frame.setSizePolicy(
1558 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1559 layout = qw.QGridLayout()
1560 frame.setLayout(layout)
1562 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1564 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1565 layout.addWidget(cb, 0, 1)
1566 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1568 self.talkie_connect(
1569 self.state, 'lighting', self.update_render_settings)
1571 # background
1573 layout.addWidget(qw.QLabel('Background'), 1, 0)
1575 cb = common.strings_to_combobox(
1576 ['black', 'white', 'skyblue1 - white'])
1578 layout.addWidget(cb, 1, 1)
1579 vstate.state_bind_combobox_background(
1580 self, self.state, 'background', cb)
1582 self.talkie_connect(
1583 self.state, 'background', self.update_render_settings)
1585 return frame
1587 def controls_snapshots(self):
1588 return snapshots_mod.SnapshotsPanel(self)
1590 def update_effective_time_labels(self, *args):
1591 tmin = self.state.tmin_effective
1592 tmax = self.state.tmax_effective
1594 stmin = common.time_or_none_to_str(tmin)
1595 stmax = common.time_or_none_to_str(tmax)
1597 self._label_effective_tmin.setText(stmin)
1598 self._label_effective_tmax.setText(stmax)
1600 def update_tcursor(self, *args):
1601 tcursor = self.gui_state.tcursor
1602 stcursor = common.time_or_none_to_str(tcursor)
1603 self._label_tcursor.setText(stcursor)
1605 def reset_strike_dip(self, *args):
1606 self.state.strike = 90.
1607 self.state.dip = 0
1608 self.gui_state.focal_point = 'center'
1610 def get_camera_geometry(self):
1612 def rtp2xyz(rtp):
1613 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1615 radius = 1.0 - self.state.depth / self.planet_radius
1617 cam_rtp = num.array([
1618 radius+self.state.distance,
1619 self.state.lat * d2r + 0.5*num.pi,
1620 self.state.lon * d2r])
1621 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1622 cam, up, foc = \
1623 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1625 foc_rtp = num.array([
1626 radius,
1627 self.state.lat * d2r + 0.5*num.pi,
1628 self.state.lon * d2r])
1630 foc = rtp2xyz(foc_rtp)
1632 rot_world = pmt.euler_to_matrix(
1633 -(self.state.lat-90.)*d2r,
1634 (self.state.lon+90.)*d2r,
1635 0.0*d2r).T
1637 rot_cam = pmt.euler_to_matrix(
1638 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1640 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1642 cam = foc + num.dot(rot, cam - foc)
1643 up = num.dot(rot, up)
1644 return cam, up, foc
1646 def update_camera(self, *args):
1647 cam, up, foc = self.get_camera_geometry()
1648 camera = self.ren.GetActiveCamera()
1649 camera.SetPosition(*cam)
1650 camera.SetFocalPoint(*foc)
1651 camera.SetViewUp(*up)
1653 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1655 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1656 self.feature_radius_min / self.planet_radius)**2))
1658 # if horizon == 0.0:
1659 # horizon = 2.0 + self.state.distance
1661 # clip_dist = max(min(self.state.distance*5., max(
1662 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1663 # , math.sqrt(num.sum(cam**2)))
1664 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1665 # clip_dist = feature_horizon
1667 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1669 self.camera_params = (
1670 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1672 self.update_view()
1674 def add_panel(
1675 self, title_label, panel,
1676 visible=False,
1677 # volatile=False,
1678 tabify=True,
1679 where=qc.Qt.RightDockWidgetArea,
1680 remove=None,
1681 title_controls=[]):
1683 dockwidget = common.MyDockWidget(
1684 self, title_label, title_controls=title_controls)
1686 if not visible:
1687 dockwidget.hide()
1689 if not self.gui_state.panels_visible:
1690 dockwidget.block()
1692 dockwidget.setWidget(panel)
1694 panel.setParent(dockwidget)
1696 dockwidgets = self.findChildren(common.MyDockWidget)
1697 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1699 self.addDockWidget(where, dockwidget)
1701 nwrap = 4
1702 if dws and len(dws) >= nwrap and tabify:
1703 self.tabifyDockWidget(
1704 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1706 mitem = dockwidget.toggleViewAction()
1708 def update_label(*args):
1709 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1710 self.update_slug_abbreviated_lengths()
1712 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1713 dockwidget.titlebar._title_label.title_changed.connect(
1714 self.update_slug_abbreviated_lengths)
1716 update_label()
1718 self._panel_togglers[dockwidget] = mitem
1719 self.panels_menu.addAction(mitem)
1720 if visible:
1721 dockwidget.setVisible(True)
1722 dockwidget.setFocus()
1723 dockwidget.raise_()
1725 def stack_panels(self):
1726 dockwidgets = self.findChildren(common.MyDockWidget)
1727 by_area = defaultdict(list)
1728 for dw in dockwidgets:
1729 area = self.dockWidgetArea(dw)
1730 by_area[area].append(dw)
1732 for dockwidgets in by_area.values():
1733 dw_last = None
1734 for dw in dockwidgets:
1735 if dw_last is not None:
1736 self.tabifyDockWidget(dw_last, dw)
1738 dw_last = dw
1740 def update_slug_abbreviated_lengths(self):
1741 dockwidgets = self.findChildren(common.MyDockWidget)
1742 title_labels = []
1743 for dw in dockwidgets:
1744 title_labels.append(dw.titlebar._title_label)
1746 by_title = defaultdict(list)
1747 for tl in title_labels:
1748 by_title[tl.get_title()].append(tl)
1750 for group in by_title.values():
1751 slugs = [tl.get_slug() for tl in group]
1753 n = max(len(slug) for slug in slugs)
1754 nunique = len(set(slugs))
1756 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1757 n -= 1
1759 if n > 0:
1760 n = max(3, n)
1762 for tl in group:
1763 tl.set_slug_abbreviated_length(n)
1765 def raise_panel(self, panel):
1766 dockwidget = panel.parent()
1767 dockwidget.setVisible(True)
1768 dockwidget.setFocus()
1769 dockwidget.raise_()
1771 def toggle_panel_visibility(self):
1772 self.gui_state.panels_visible = not self.gui_state.panels_visible
1774 def update_panel_visibility(self, *args):
1775 self.setUpdatesEnabled(False)
1776 mbar = self.menuBar()
1777 sbar = self.statusBar()
1778 dockwidgets = self.findChildren(common.MyDockWidget)
1780 # Set height to zero instead of hiding so that shortcuts still work
1781 # otherwise one would have to mess around with separate QShortcut
1782 # objects.
1783 mbar.setFixedHeight(
1784 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1786 sbar.setVisible(self.gui_state.panels_visible)
1787 for dockwidget in dockwidgets:
1788 dockwidget.setBlocked(not self.gui_state.panels_visible)
1790 self.setUpdatesEnabled(True)
1792 def remove_panel(self, panel):
1793 dockwidget = panel.parent()
1794 self.removeDockWidget(dockwidget)
1795 dockwidget.setParent(None)
1796 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1798 def register_data_provider(self, provider):
1799 if provider not in self.data_providers:
1800 self.data_providers.append(provider)
1802 def unregister_data_provider(self, provider):
1803 if provider in self.data_providers:
1804 self.data_providers.remove(provider)
1806 def iter_data(self, name):
1807 for provider in self.data_providers:
1808 for data in provider.iter_data(name):
1809 yield data
1811 def confirm_close(self):
1812 ret = qw.QMessageBox.question(
1813 self,
1814 'Sparrow',
1815 'Close Sparrow window?',
1816 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1817 qw.QMessageBox.Ok)
1819 return ret == qw.QMessageBox.Ok
1821 def closeEvent(self, event):
1822 if self.instant_close or self.confirm_close():
1823 self.attach()
1824 self.closing = True
1825 event.accept()
1826 else:
1827 event.ignore()
1829 def is_closing(self):
1830 return self.closing
1833def main(*args, **kwargs):
1835 from pyrocko import util
1836 from pyrocko.gui import util as gui_util
1837 from . import common
1838 util.setup_logging('sparrow', 'info')
1840 global win
1842 app = gui_util.get_app()
1843 win = SparrowViewer(*args, **kwargs)
1844 app.set_main_window(win)
1846 gui_util.app.install_sigint_handler()
1848 try:
1849 gui_util.app.exec_()
1850 finally:
1851 gui_util.app.uninstall_sigint_handler()
1852 app.unset_main_window()
1853 common.set_viewer(None)
1854 del win
1855 gc.collect()