1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import signal
8import gc
9import logging
10import time
11import tempfile
12import os
13import shutil
14import platform
15from collections import defaultdict
16from subprocess import check_call
18import numpy as num
20from pyrocko import cake
21from pyrocko import guts
22from pyrocko import geonames
23from pyrocko import config
24from pyrocko import moment_tensor as pmt
25from pyrocko import util
27from pyrocko.gui.util import Progressbars, RangeEdit
28from pyrocko.gui.talkie import TalkieConnectionOwner, equal as state_equal
29from pyrocko.gui.qt_compat import qw, qc, qg
30# from pyrocko.gui import vtk_util
32from . import common, light, snapshots as snapshots_mod
34import vtk
35import vtk.qt
36vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
38from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
40from pyrocko import geometry # noqa
41from . import state as vstate, elements # noqa
43logger = logging.getLogger('pyrocko.gui.sparrow.main')
46d2r = num.pi/180.
47km = 1000.
49if platform.uname()[0] == 'Darwin':
50 g_modifier_key = '\u2318'
51else:
52 g_modifier_key = 'Ctrl'
55class ZeroFrame(qw.QFrame):
57 def sizeHint(self):
58 return qc.QSize(0, 0)
61class LocationChoice(object):
62 def __init__(self, name, lat, lon, depth=0):
63 self._name = name
64 self._lat = lat
65 self._lon = lon
66 self._depth = depth
68 def get_lat_lon_depth(self):
69 return self._lat, self._lon, self._depth
72def location_to_choices(s):
73 choices = []
74 s_vals = s.replace(',', ' ')
75 try:
76 vals = [float(x) for x in s_vals.split()]
77 if len(vals) == 3:
78 vals[2] *= km
80 choices.append(LocationChoice('', *vals))
82 except ValueError:
83 cities = geonames.get_cities_by_name(s.strip())
84 for c in cities:
85 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
87 return choices
90class NoLocationChoices(Exception):
92 def __init__(self, s):
93 self._string = s
95 def __str__(self):
96 return 'No location choices for string "%s"' % self._string
99class QVTKWidget(QVTKRenderWindowInteractor):
100 def __init__(self, viewer, *args):
101 QVTKRenderWindowInteractor.__init__(self, *args)
102 self._viewer = viewer
103 self._ctrl_state = False
105 def wheelEvent(self, event):
106 return self._viewer.myWheelEvent(event)
108 def keyPressEvent(self, event):
109 if event.key() == qc.Qt.Key_Control:
110 self._update_ctrl_state(True)
111 QVTKRenderWindowInteractor.keyPressEvent(self, event)
113 def keyReleaseEvent(self, event):
114 if event.key() == qc.Qt.Key_Control:
115 self._update_ctrl_state(False)
116 QVTKRenderWindowInteractor.keyReleaseEvent(self, event)
118 def focusInEvent(self, event):
119 self._update_ctrl_state()
120 QVTKRenderWindowInteractor.focusInEvent(self, event)
122 def focusOutEvent(self, event):
123 self._update_ctrl_state(False)
124 QVTKRenderWindowInteractor.focusOutEvent(self, event)
126 def mousePressEvent(self, event):
127 self._viewer.disable_capture()
128 QVTKRenderWindowInteractor.mousePressEvent(self, event)
130 def mouseReleaseEvent(self, event):
131 self._viewer.enable_capture()
132 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event)
134 def _update_ctrl_state(self, state=None):
135 if state is None:
136 app = common.get_app()
137 if not app:
138 return
139 state = app.keyboardModifiers() == qc.Qt.ControlModifier
140 if self._ctrl_state != state:
141 self._viewer.gui_state.next_focal_point()
142 self._ctrl_state = state
144 def container_resized(self, ev):
145 self._viewer.update_vtk_widget_size()
148class DetachedViewer(qw.QMainWindow):
150 def __init__(self, main_window, vtk_frame):
151 qw.QMainWindow.__init__(self, main_window)
152 self.main_window = main_window
153 self.setWindowTitle('Sparrow View')
154 vtk_frame.setParent(self)
155 self.setCentralWidget(vtk_frame)
157 def closeEvent(self, ev):
158 ev.ignore()
159 self.main_window.attach()
162class CenteringScrollArea(qw.QScrollArea):
163 def __init__(self):
164 qw.QScrollArea.__init__(self)
165 self.setAlignment(qc.Qt.AlignCenter)
166 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
167 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
168 self.setFrameShape(qw.QFrame.NoFrame)
170 def resizeEvent(self, ev):
171 retval = qw.QScrollArea.resizeEvent(self, ev)
172 self.widget().container_resized(ev)
173 return retval
175 def recenter(self):
176 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
177 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
179 def wheelEvent(self, *args, **kwargs):
180 return self.widget().wheelEvent(*args, **kwargs)
183class YAMLEditor(qw.QTextEdit):
185 def __init__(self, parent):
186 qw.QTextEdit.__init__(self)
187 self._parent = parent
189 def event(self, ev):
190 if isinstance(ev, qg.QKeyEvent) \
191 and ev.key() == qc.Qt.Key_Return \
192 and ev.modifiers() & qc.Qt.ShiftModifier:
193 self._parent.state_changed()
194 return True
196 return qw.QTextEdit.event(self, ev)
199class StateEditor(qw.QFrame, TalkieConnectionOwner):
200 def __init__(self, viewer, *args, **kwargs):
201 qw.QFrame.__init__(self, *args, **kwargs)
202 TalkieConnectionOwner.__init__(self)
204 layout = qw.QGridLayout()
206 self.setLayout(layout)
208 self.source_editor = YAMLEditor(self)
209 self.source_editor.setAcceptRichText(False)
210 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
211 font = qg.QFont("Monospace")
212 self.source_editor.setCurrentFont(font)
213 layout.addWidget(self.source_editor, 0, 0, 1, 2)
215 self.error_display_label = qw.QLabel('Error')
216 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
218 self.error_display = qw.QTextEdit()
219 self.error_display.setCurrentFont(font)
220 self.error_display.setReadOnly(True)
222 self.error_display.setSizePolicy(
223 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
225 self.error_display_label.hide()
226 self.error_display.hide()
228 layout.addWidget(self.error_display, 2, 0, 1, 2)
230 self.instant_updates = qw.QCheckBox('Instant Updates')
231 self.instant_updates.toggled.connect(self.state_changed)
232 layout.addWidget(self.instant_updates, 3, 0)
234 button = qw.QPushButton('Apply')
235 button.clicked.connect(self.state_changed)
236 layout.addWidget(button, 3, 1)
238 self.viewer = viewer
239 # recommended way, but resulted in a variable-width font being used:
240 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
241 self.bind_state()
242 self.source_editor.textChanged.connect(self.text_changed_handler)
243 self.destroyed.connect(self.unbind_state)
244 self.bind_state()
246 def bind_state(self, *args):
247 self.talkie_connect(self.viewer.state, '', self.update_state)
248 self.update_state()
250 def unbind_state(self):
251 self.talkie_disconnect_all()
253 def update_state(self, *args):
254 cursor = self.source_editor.textCursor()
256 cursor_position = cursor.position()
257 vsb_position = self.source_editor.verticalScrollBar().value()
258 hsb_position = self.source_editor.horizontalScrollBar().value()
260 self.source_editor.setPlainText(str(self.viewer.state))
262 cursor.setPosition(cursor_position)
263 self.source_editor.setTextCursor(cursor)
264 self.source_editor.verticalScrollBar().setValue(vsb_position)
265 self.source_editor.horizontalScrollBar().setValue(hsb_position)
267 def text_changed_handler(self, *args):
268 if self.instant_updates.isChecked():
269 self.state_changed()
271 def state_changed(self):
272 try:
273 s = self.source_editor.toPlainText()
274 state = guts.load(string=s)
275 self.viewer.set_state(state)
276 self.error_display.setPlainText('')
277 self.error_display_label.hide()
278 self.error_display.hide()
280 except Exception as e:
281 self.error_display.show()
282 self.error_display_label.show()
283 self.error_display.setPlainText(str(e))
286class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
287 def __init__(self, use_depth_peeling=True, events=None, snapshots=None):
288 qw.QMainWindow.__init__(self)
289 TalkieConnectionOwner.__init__(self)
291 common.get_app().set_main_window(self)
293 self.state = vstate.ViewerState()
294 self.gui_state = vstate.ViewerGuiState()
296 self.setWindowTitle('Sparrow')
298 self.setTabPosition(
299 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
301 self.planet_radius = cake.earthradius
302 self.feature_radius_min = cake.earthradius - 1000. * km
304 self._block_capture = 0
305 self._undo_stack = []
306 self._redo_stack = []
307 self._undo_aggregate = None
309 self._panel_togglers = {}
310 self._actors = set()
311 self._actors_2d = set()
312 self._render_window_size = (0, 0)
313 self._use_depth_peeling = use_depth_peeling
314 self._in_update_elements = False
315 self._update_elements_enabled = True
317 self._animation_tstart = None
318 self._animation_iframe = None
319 self._animation = None
321 mbar = qw.QMenuBar()
322 self.setMenuBar(mbar)
324 menu = mbar.addMenu('File')
326 menu.addAction(
327 'Export Image...',
328 self.export_image,
329 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
330 qc.Qt.ApplicationShortcut)
332 menu.addAction(
333 'Quit',
334 self.request_quit,
335 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
336 qc.Qt.ApplicationShortcut)
338 menu = mbar.addMenu('Edit')
340 menu.addAction(
341 'Undo',
342 self.undo,
343 qg.QKeySequence(
344 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext(
345 qc.Qt.ApplicationShortcut)
347 menu.addAction(
348 'Redo',
349 self.redo,
350 qg.QKeySequence(
351 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext(
352 qc.Qt.ApplicationShortcut)
354 menu = mbar.addMenu('View')
355 menu_sizes = menu.addMenu('Size')
356 self._add_vtk_widget_size_menu_entries(menu_sizes)
358 # detached/attached
359 self.talkie_connect(
360 self.gui_state, 'detached', self.update_detached)
362 action = qw.QAction('Detach')
363 action.setCheckable(True)
364 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
365 action.setShortcutContext(qc.Qt.ApplicationShortcut)
367 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
368 menu.addAction(action)
370 self.panels_menu = mbar.addMenu('Panels')
371 self.panels_menu.addAction(
372 'Stack Panels',
373 self.stack_panels)
374 self.panels_menu.addSeparator()
376 snapshots_menu = mbar.addMenu('Snapshots')
378 menu = mbar.addMenu('Elements')
379 for name, estate in sorted([
380 ('Icosphere', elements.IcosphereState(
381 level=4,
382 smooth=True,
383 opacity=0.5,
384 ambient=0.1)),
385 ('Grid', elements.GridState()),
386 ('Stations', elements.StationsState()),
387 ('Topography', elements.TopoState()),
388 ('Custom Topography', elements.CustomTopoState()),
389 ('Catalog', elements.CatalogState()),
390 ('Coastlines', elements.CoastlinesState()),
391 ('Source', elements.SourceState()),
392 ('HUD Subtitle', elements.HudState(
393 template='Subtitle')),
394 ('HUD (tmax_effective)', elements.HudState(
395 template='tmax: {tmax_effective|date}',
396 position='top-left')),
397 ('AxesBox', elements.AxesBoxState()),
398 ('Volcanoes', elements.VolcanoesState()),
399 ('Faults', elements.ActiveFaultsState()),
400 ('Plate bounds', elements.PlatesBoundsState()),
401 ('InSAR Surface Displacements', elements.KiteState()),
402 ('Geometry', elements.GeometryState()),
403 ('Spheroid', elements.SpheroidState())]):
405 def wrap_add_element(estate):
406 def add_element(*args):
407 new_element = guts.clone(estate)
408 new_element.element_id = elements.random_id()
409 self.state.elements.append(new_element)
410 self.state.sort_elements()
412 return add_element
414 mitem = qw.QAction(name, self)
416 mitem.triggered.connect(wrap_add_element(estate))
418 menu.addAction(mitem)
420 menu = mbar.addMenu('Help')
422 menu.addAction(
423 'Interactive Tour',
424 self.start_tour)
426 menu.addAction(
427 'Online Manual',
428 self.open_manual)
430 self.data_providers = []
431 self.elements = {}
433 self.detached_window = None
435 self.main_frame = qw.QFrame()
436 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
438 self.vtk_frame = CenteringScrollArea()
440 self.vtk_widget = QVTKWidget(self, self)
441 self.vtk_frame.setWidget(self.vtk_widget)
443 self.main_layout = qw.QVBoxLayout()
444 self.main_layout.setContentsMargins(0, 0, 0, 0)
445 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
447 pb = Progressbars(self)
448 self.progressbars = pb
449 self.main_layout.addWidget(pb)
451 self.main_frame.setLayout(self.main_layout)
453 self.vtk_frame_substitute = None
455 self.add_panel(
456 'Navigation',
457 self.controls_navigation(), visible=True,
458 where=qc.Qt.LeftDockWidgetArea)
460 self.add_panel(
461 'Time',
462 self.controls_time(), visible=True,
463 where=qc.Qt.LeftDockWidgetArea)
465 self.add_panel(
466 'Appearance',
467 self.controls_appearance(), visible=True,
468 where=qc.Qt.LeftDockWidgetArea)
470 snapshots_panel = self.controls_snapshots()
471 self.snapshots_panel = snapshots_panel
472 self.add_panel(
473 'Snapshots',
474 snapshots_panel, visible=False,
475 where=qc.Qt.LeftDockWidgetArea)
477 snapshots_panel.setup_menu(snapshots_menu)
479 self.setCentralWidget(self.main_frame)
481 self.mesh = None
483 ren = vtk.vtkRenderer()
485 # ren.SetBackground(0.15, 0.15, 0.15)
486 # ren.SetBackground(0.0, 0.0, 0.0)
487 # ren.TwoSidedLightingOn()
488 # ren.SetUseShadows(1)
490 self._lighting = None
491 self._background = None
493 self.ren = ren
494 self.update_render_settings()
495 self.update_camera()
497 renwin = self.vtk_widget.GetRenderWindow()
499 if self._use_depth_peeling:
500 renwin.SetAlphaBitPlanes(1)
501 renwin.SetMultiSamples(0)
503 ren.SetUseDepthPeeling(1)
504 ren.SetMaximumNumberOfPeels(100)
505 ren.SetOcclusionRatio(0.1)
507 ren.SetUseFXAA(1)
508 # ren.SetUseHiddenLineRemoval(1)
509 # ren.SetBackingStore(1)
511 self.renwin = renwin
513 # renwin.LineSmoothingOn()
514 # renwin.PointSmoothingOn()
515 # renwin.PolygonSmoothingOn()
517 renwin.AddRenderer(ren)
519 iren = renwin.GetInteractor()
520 iren.LightFollowCameraOn()
521 iren.SetInteractorStyle(None)
523 iren.AddObserver('LeftButtonPressEvent', self.button_event)
524 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
525 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
526 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
527 iren.AddObserver('RightButtonPressEvent', self.button_event)
528 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
529 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
530 iren.AddObserver('KeyPressEvent', self.key_down_event)
531 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
533 renwin.Render()
535 iren.Initialize()
537 self.iren = iren
539 self.rotating = False
541 self._elements = {}
542 self._elements_active = {}
544 self.talkie_connect(
545 self.state, 'elements', self.update_elements)
547 self.state.elements.append(elements.IcosphereState(
548 element_id='icosphere',
549 level=4,
550 smooth=True,
551 opacity=0.5,
552 ambient=0.1))
554 self.state.elements.append(elements.GridState(
555 element_id='grid'))
556 self.state.elements.append(elements.CoastlinesState(
557 element_id='coastlines'))
558 self.state.elements.append(elements.CrosshairState(
559 element_id='crosshair'))
561 # self.state.elements.append(elements.StationsState())
562 # self.state.elements.append(elements.SourceState())
563 # self.state.elements.append(
564 # elements.CatalogState(
565 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
566 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
568 if events:
569 self.state.elements.append(
570 elements.CatalogState(
571 selection=elements.MemoryCatalogSelection(events=events)))
573 self.state.sort_elements()
575 if snapshots:
576 snapshots_ = []
577 for obj in snapshots:
578 if isinstance(obj, str):
579 snapshots_.extend(snapshots_mod.load_snapshots(obj))
580 else:
581 snapshots_.append(obj)
583 snapshots_panel.add_snapshots(snapshots_)
584 self.raise_panel(snapshots_panel)
585 snapshots_panel.goto_snapshot(1)
587 self.timer = qc.QTimer(self)
588 self.timer.timeout.connect(self.periodical)
589 self.timer.setInterval(1000)
590 self.timer.start()
592 self._animation_saver = None
594 self.closing = False
595 self.vtk_widget.setFocus()
597 self.update_detached()
599 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
600 common.get_app().status('Let\'s fly.', 2.0)
602 self.show()
603 self.windowHandle().showMaximized()
605 self.talkie_connect(
606 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
608 self.update_vtk_widget_size()
610 hatch_path = config.expand(os.path.join(
611 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
613 self.talkie_connect(self.state, '', self.capture_state)
614 self.capture_state()
616 if not os.path.exists(hatch_path):
617 with open(hatch_path, 'w') as f:
618 f.write('%s\n' % util.time_to_str(time.time()))
620 self.start_tour()
622 def disable_capture(self):
623 self._block_capture += 1
625 logger.debug('Undo capture block (+1): %i' % self._block_capture)
627 def enable_capture(self, drop=False, aggregate=None):
628 if self._block_capture > 0:
629 self._block_capture -= 1
631 logger.debug('Undo capture block (-1): %i' % self._block_capture)
633 if self._block_capture == 0 and not drop:
634 self.capture_state(aggregate=aggregate)
636 def capture_state(self, *args, aggregate=None):
637 if self._block_capture:
638 return
640 if len(self._undo_stack) == 0 or not state_equal(
641 self.state, self._undo_stack[-1]):
643 if aggregate is not None:
644 if aggregate == self._undo_aggregate:
645 self._undo_stack.pop()
647 self._undo_aggregate = aggregate
648 else:
649 self._undo_aggregate = None
651 logger.debug('Capture undo state (%i%s)\n%s' % (
652 len(self._undo_stack) + 1,
653 '' if aggregate is None else ', aggregate=%s' % aggregate,
654 '\n'.join(
655 ' - %s' % s
656 for s in self._undo_stack[-1].str_diff(
657 self.state).splitlines())
658 if len(self._undo_stack) > 0 else 'initial'))
660 self._undo_stack.append(guts.clone(self.state))
661 self._redo_stack.clear()
663 def undo(self):
664 self._undo_aggregate = None
666 if len(self._undo_stack) <= 1:
667 return
669 state = self._undo_stack.pop()
670 self._redo_stack.append(state)
671 state = self._undo_stack[-1]
673 logger.debug('Undo (%i)\n%s' % (
674 len(self._undo_stack),
675 '\n'.join(
676 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
678 self.disable_capture()
679 try:
680 self.set_state(state)
681 finally:
682 self.enable_capture(drop=True)
684 def redo(self):
685 self._undo_aggregate = None
687 if len(self._redo_stack) == 0:
688 return
690 state = self._redo_stack.pop()
691 self._undo_stack.append(state)
693 logger.debug('Redo (%i)\n%s' % (
694 len(self._redo_stack),
695 '\n'.join(
696 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
698 self.disable_capture()
699 try:
700 self.set_state(state)
701 finally:
702 self.enable_capture(drop=True)
704 def start_tour(self):
705 snapshots_ = snapshots_mod.load_snapshots(
706 'https://data.pyrocko.org/examples/'
707 'sparrow-tour-v0.1.snapshots.yaml')
708 self.snapshots_panel.add_snapshots(snapshots_)
709 self.raise_panel(self.snapshots_panel)
710 self.snapshots_panel.transition_to_next_snapshot()
712 def open_manual(self):
713 import webbrowser
714 webbrowser.open(
715 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
717 def _add_vtk_widget_size_menu_entries(self, menu):
719 group = qw.QActionGroup(menu)
720 group.setExclusive(True)
722 def set_variable_size():
723 self.gui_state.fixed_size = False
725 variable_size_action = menu.addAction('Fit Window Size')
726 variable_size_action.setCheckable(True)
727 variable_size_action.setActionGroup(group)
728 variable_size_action.triggered.connect(set_variable_size)
730 fixed_size_items = []
731 for nx, ny, label in [
732 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
733 (426, 240, ''),
734 (640, 360, ''),
735 (854, 480, '(FWVGA)'),
736 (1280, 720, '(HD)'),
737 (1920, 1080, '(Full HD)'),
738 (2560, 1440, '(Quad HD)'),
739 (3840, 2160, '(4K UHD)'),
740 (3840*2, 2160*2, '',),
741 (None, None, 'Aspect 4:3'),
742 (640, 480, '(VGA)'),
743 (800, 600, '(SVGA)'),
744 (None, None, 'Other'),
745 (512, 512, ''),
746 (1024, 1024, '')]:
748 if None in (nx, ny):
749 menu.addSection(label)
750 else:
751 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
752 action = menu.addAction(name)
753 action.setCheckable(True)
754 action.setActionGroup(group)
755 fixed_size_items.append((action, (nx, ny)))
757 def make_set_fixed_size(nx, ny):
758 def set_fixed_size():
759 self.gui_state.fixed_size = (float(nx), float(ny))
761 return set_fixed_size
763 action.triggered.connect(make_set_fixed_size(nx, ny))
765 def update_widget(*args):
766 for action, (nx, ny) in fixed_size_items:
767 action.blockSignals(True)
768 action.setChecked(
769 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
770 int(z) for z in self.gui_state.fixed_size)))
771 action.blockSignals(False)
773 variable_size_action.blockSignals(True)
774 variable_size_action.setChecked(not self.gui_state.fixed_size)
775 variable_size_action.blockSignals(False)
777 update_widget()
778 self.talkie_connect(
779 self.gui_state, 'fixed_size', update_widget)
781 def update_vtk_widget_size(self, *args):
782 if self.gui_state.fixed_size:
783 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
784 wanted_size = qc.QSize(nx, ny)
785 else:
786 wanted_size = qc.QSize(
787 self.vtk_frame.window().width(), self.vtk_frame.height())
789 current_size = self.vtk_widget.size()
791 if current_size.width() != wanted_size.width() \
792 or current_size.height() != wanted_size.height():
794 self.vtk_widget.setFixedSize(wanted_size)
796 self.vtk_frame.recenter()
797 self.check_vtk_resize()
799 def update_focal_point(self, *args):
800 if self.gui_state.focal_point == 'center':
801 self.vtk_widget.setStatusTip(
802 'Click and drag: change location. %s-click and drag: '
803 'change view plane orientation.' % g_modifier_key)
804 else:
805 self.vtk_widget.setStatusTip(
806 '%s-click and drag: change location. Click and drag: '
807 'change view plane orientation. Uncheck "Navigation: Fix" to '
808 'reverse sense.' % g_modifier_key)
810 def update_detached(self, *args):
812 if self.gui_state.detached and not self.detached_window: # detach
813 logger.debug('Detaching VTK view.')
815 self.main_layout.removeWidget(self.vtk_frame)
816 self.detached_window = DetachedViewer(self, self.vtk_frame)
817 self.detached_window.show()
818 self.vtk_widget.setFocus()
820 screens = common.get_app().screens()
821 if len(screens) > 1:
822 for screen in screens:
823 if screen is not self.screen():
824 self.detached_window.windowHandle().setScreen(screen)
825 # .setScreen() does not work reliably,
826 # therefore trying also with .move()...
827 p = screen.geometry().topLeft()
828 self.detached_window.move(p.x() + 50, p.y() + 50)
829 # ... but also does not work in notion window manager.
831 self.detached_window.windowHandle().showMaximized()
833 frame = qw.QFrame()
834 # frame.setFrameShape(qw.QFrame.NoFrame)
835 # frame.setBackgroundRole(qg.QPalette.Mid)
836 # frame.setAutoFillBackground(True)
837 frame.setSizePolicy(
838 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
840 layout = qw.QGridLayout()
841 frame.setLayout(layout)
842 self.main_layout.insertWidget(0, frame)
844 self.state_editor = StateEditor(self)
846 layout.addWidget(self.state_editor, 0, 0)
848 # attach_button = qw.QPushButton('Attach View')
849 # attach_button.clicked.connect(self.attach)
850 # layout.addWidget(
851 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
853 self.vtk_frame_substitute = frame
855 if not self.gui_state.detached and self.detached_window: # attach
856 logger.debug('Attaching VTK view.')
857 self.detached_window.hide()
858 self.vtk_frame.setParent(self)
859 if self.vtk_frame_substitute:
860 self.main_layout.removeWidget(self.vtk_frame_substitute)
861 self.state_editor.unbind_state()
862 self.vtk_frame_substitute = None
864 self.main_layout.insertWidget(0, self.vtk_frame)
865 self.detached_window = None
866 self.vtk_widget.setFocus()
868 def attach(self):
869 self.gui_state.detached = False
871 def export_image(self):
873 caption = 'Export Image'
874 fn_out, _ = qw.QFileDialog.getSaveFileName(
875 self, caption, 'image.png',
876 options=common.qfiledialog_options)
878 if fn_out:
879 self.save_image(fn_out)
881 def save_image(self, path):
883 original_fixed_size = self.gui_state.fixed_size
884 if original_fixed_size is None:
885 self.gui_state.fixed_size = (1920., 1080.)
887 wif = vtk.vtkWindowToImageFilter()
888 wif.SetInput(self.renwin)
889 wif.SetInputBufferTypeToRGBA()
890 wif.SetScale(1, 1)
891 wif.ReadFrontBufferOff()
892 writer = vtk.vtkPNGWriter()
893 writer.SetInputConnection(wif.GetOutputPort())
895 self.renwin.Render()
896 wif.Modified()
897 writer.SetFileName(path)
898 writer.Write()
900 self.gui_state.fixed_size = original_fixed_size
902 def update_render_settings(self, *args):
903 if self._lighting is None or self._lighting != self.state.lighting:
904 self.ren.RemoveAllLights()
905 for li in light.get_lights(self.state.lighting):
906 self.ren.AddLight(li)
908 self._lighting = self.state.lighting
910 if self._background is None \
911 or self._background != self.state.background:
913 self.state.background.vtk_apply(self.ren)
914 self._background = self.state.background
916 self.update_view()
918 def start_animation(self, interpolator, output_path=None):
919 if self._animation:
920 logger.debug('Aborting animation in progress to start a new one.')
921 self.stop_animation()
923 self.disable_capture()
924 self._animation = interpolator
925 if output_path is None:
926 self._animation_tstart = time.time()
927 self._animation_iframe = None
928 else:
929 self._animation_iframe = 0
930 self.showFullScreen()
931 self.update_view()
932 self.gui_state.panels_visible = False
933 self.update_view()
935 self._animation_timer = qc.QTimer(self)
936 self._animation_timer.timeout.connect(self.next_animation_frame)
937 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
938 self._animation_timer.start()
939 if output_path is not None:
940 original_fixed_size = self.gui_state.fixed_size
941 if original_fixed_size is None:
942 self.gui_state.fixed_size = (1920., 1080.)
944 wif = vtk.vtkWindowToImageFilter()
945 wif.SetInput(self.renwin)
946 wif.SetInputBufferTypeToRGBA()
947 wif.SetScale(1, 1)
948 wif.ReadFrontBufferOff()
949 writer = vtk.vtkPNGWriter()
950 temp_path = tempfile.mkdtemp()
951 self._animation_saver = (
952 wif, writer, temp_path, output_path, original_fixed_size)
953 writer.SetInputConnection(wif.GetOutputPort())
955 def next_animation_frame(self):
957 ani = self._animation
958 if not ani:
959 return
961 if self._animation_iframe is not None:
962 state = ani(
963 ani.tmin
964 + self._animation_iframe * ani.dt)
966 self._animation_iframe += 1
967 else:
968 tnow = time.time()
969 state = ani(min(
970 ani.tmax,
971 ani.tmin + (tnow - self._animation_tstart)))
973 self.set_state(state)
974 self.renwin.Render()
975 if self._animation_saver:
976 wif, writer, temp_path, _, _ = self._animation_saver
977 wif.Modified()
978 fn = os.path.join(temp_path, 'f%09i.png')
979 writer.SetFileName(fn % self._animation_iframe)
980 writer.Write()
982 if self._animation_iframe is not None:
983 t = self._animation_iframe * ani.dt
984 else:
985 t = tnow - self._animation_tstart
987 if t > ani.tmax - ani.tmin:
988 self.stop_animation()
990 def stop_animation(self):
991 if self._animation_timer:
992 self._animation_timer.stop()
994 if self._animation_saver:
996 wif, writer, temp_path, output_path, original_fixed_size \
997 = self._animation_saver
998 self.gui_state.fixed_size = original_fixed_size
1000 fn_path = os.path.join(temp_path, 'f%09d.png')
1001 check_call([
1002 'ffmpeg', '-y',
1003 '-i', fn_path,
1004 '-c:v', 'libx264',
1005 '-preset', 'slow',
1006 '-crf', '17',
1007 '-vf', 'format=yuv420p,fps=%i' % (
1008 int(round(1.0/self._animation.dt))),
1009 output_path])
1010 shutil.rmtree(temp_path)
1012 self._animation_saver = None
1013 self._animation_saver
1015 self.showNormal()
1016 self.gui_state.panels_visible = True
1018 self._animation_tstart = None
1019 self._animation_iframe = None
1020 self._animation = None
1021 self.enable_capture()
1023 def set_state(self, state):
1024 self.disable_capture()
1025 try:
1026 self._update_elements_enabled = False
1027 self.setUpdatesEnabled(False)
1028 self.state.diff_update(state)
1029 self.state.sort_elements()
1030 self.setUpdatesEnabled(True)
1031 self._update_elements_enabled = True
1032 self.update_elements()
1033 finally:
1034 self.enable_capture()
1036 def periodical(self):
1037 pass
1039 def request_quit(self):
1040 app = common.get_app()
1041 app.myQuit()
1043 def check_vtk_resize(self, *args):
1044 render_window_size = self.renwin.GetSize()
1045 if self._render_window_size != render_window_size:
1046 self._render_window_size = render_window_size
1047 self.resize_event(*render_window_size)
1049 def update_elements(self, *_):
1050 if not self._update_elements_enabled:
1051 return
1053 if self._in_update_elements:
1054 return
1056 self._in_update_elements = True
1057 for estate in self.state.elements:
1058 if estate.element_id not in self._elements:
1059 new_element = estate.create()
1060 logger.debug('Creating "%s" ("%s").' % (
1061 type(new_element).__name__,
1062 estate.element_id))
1063 self._elements[estate.element_id] = new_element
1065 element = self._elements[estate.element_id]
1067 if estate.element_id not in self._elements_active:
1068 logger.debug('Adding "%s" ("%s")' % (
1069 type(element).__name__,
1070 estate.element_id))
1071 element.bind_state(estate)
1072 element.set_parent(self)
1073 self._elements_active[estate.element_id] = element
1075 state_element_ids = [el.element_id for el in self.state.elements]
1076 deactivate = []
1077 for element_id, element in self._elements_active.items():
1078 if element_id not in state_element_ids:
1079 logger.debug('Removing "%s" ("%s").' % (
1080 type(element).__name__,
1081 element_id))
1082 element.unset_parent()
1083 deactivate.append(element_id)
1085 for element_id in deactivate:
1086 del self._elements_active[element_id]
1088 self._update_crosshair_bindings()
1090 self._in_update_elements = False
1092 def _update_crosshair_bindings(self):
1094 def get_crosshair_element():
1095 for element in self.state.elements:
1096 if element.element_id == 'crosshair':
1097 return element
1099 return None
1101 crosshair = get_crosshair_element()
1102 if crosshair is None or crosshair.is_connected:
1103 return
1105 def to_checkbox(state, widget):
1106 widget.blockSignals(True)
1107 widget.setChecked(state.visible)
1108 widget.blockSignals(False)
1110 def to_state(widget, state):
1111 state.visible = widget.isChecked()
1113 cb = self._crosshair_checkbox
1114 vstate.state_bind(
1115 self, crosshair, ['visible'], to_state,
1116 cb, [cb.toggled], to_checkbox)
1118 crosshair.is_connected = True
1120 def add_actor_2d(self, actor):
1121 if actor not in self._actors_2d:
1122 self.ren.AddActor2D(actor)
1123 self._actors_2d.add(actor)
1125 def remove_actor_2d(self, actor):
1126 if actor in self._actors_2d:
1127 self.ren.RemoveActor2D(actor)
1128 self._actors_2d.remove(actor)
1130 def add_actor(self, actor):
1131 if actor not in self._actors:
1132 self.ren.AddActor(actor)
1133 self._actors.add(actor)
1135 def add_actor_list(self, actorlist):
1136 for actor in actorlist:
1137 self.add_actor(actor)
1139 def remove_actor(self, actor):
1140 if actor in self._actors:
1141 self.ren.RemoveActor(actor)
1142 self._actors.remove(actor)
1144 def update_view(self):
1145 self.vtk_widget.update()
1147 def resize_event(self, size_x, size_y):
1148 self.gui_state.size = (size_x, size_y)
1150 def button_event(self, obj, event):
1151 if event == "LeftButtonPressEvent":
1152 self.rotating = True
1153 elif event == "LeftButtonReleaseEvent":
1154 self.rotating = False
1156 def mouse_move_event(self, obj, event):
1157 x0, y0 = self.iren.GetLastEventPosition()
1158 x, y = self.iren.GetEventPosition()
1160 size_x, size_y = self.renwin.GetSize()
1161 center_x = size_x / 2.0
1162 center_y = size_y / 2.0
1164 if self.rotating:
1165 self.do_rotate(x, y, x0, y0, center_x, center_y)
1167 def myWheelEvent(self, event):
1169 angle = event.angleDelta().y()
1171 if angle > 200:
1172 angle = 200
1174 if angle < -200:
1175 angle = -200
1177 self.disable_capture()
1178 try:
1179 self.do_dolly(-angle/100.)
1180 finally:
1181 self.enable_capture(aggregate='distance')
1183 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1185 dx = x0 - x
1186 dy = y0 - y
1188 phi = d2r*(self.state.strike - 90.)
1189 focp = self.gui_state.focal_point
1191 if focp == 'center':
1192 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1193 - math.sin(phi) * dx + math.cos(phi) * dy
1195 lat = self.state.lat
1196 lon = self.state.lon
1197 factor = self.state.distance / 10.0
1198 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1199 else:
1200 lat = 90. - self.state.dip
1201 lon = -self.state.strike - 90.
1202 factor = 0.5
1203 factor_lat = 1.0
1205 dlat = dy * factor
1206 dlon = dx * factor * factor_lat
1208 lat = max(min(lat + dlat, 90.), -90.)
1209 lon += dlon
1210 lon = (lon + 180.) % 360. - 180.
1212 if focp == 'center':
1213 self.state.lat = float(lat)
1214 self.state.lon = float(lon)
1215 else:
1216 self.state.dip = float(90. - lat)
1217 self.state.strike = float(-(lon + 90.))
1219 def do_dolly(self, v):
1220 self.state.distance *= float(1.0 + 0.1*v)
1222 def key_down_event(self, obj, event):
1223 k = obj.GetKeyCode()
1224 if k == 'f':
1225 self.gui_state.next_focal_point()
1227 elif k == 'r':
1228 self.reset_strike_dip()
1230 elif k == 'p':
1231 print(self.state)
1233 elif k == 'i':
1234 for elem in self.state.elements:
1235 if isinstance(elem, elements.IcosphereState):
1236 elem.visible = not elem.visible
1238 elif k == 'c':
1239 for elem in self.state.elements:
1240 if isinstance(elem, elements.CoastlinesState):
1241 elem.visible = not elem.visible
1243 elif k == 't':
1244 if not any(
1245 isinstance(elem, elements.TopoState)
1246 for elem in self.state.elements):
1248 self.state.elements.append(elements.TopoState())
1249 else:
1250 for elem in self.state.elements:
1251 if isinstance(elem, elements.TopoState):
1252 elem.visible = not elem.visible
1254 elif k == ' ':
1255 self.toggle_panel_visibility()
1257 def _state_bind(self, *args, **kwargs):
1258 vstate.state_bind(self, self.state, *args, **kwargs)
1260 def _gui_state_bind(self, *args, **kwargs):
1261 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1263 def controls_navigation(self):
1264 frame = qw.QFrame(self)
1265 frame.setSizePolicy(
1266 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1267 layout = qw.QGridLayout()
1268 frame.setLayout(layout)
1270 # lat, lon, depth
1272 layout.addWidget(
1273 qw.QLabel('Location'), 0, 0, 1, 2)
1275 le = qw.QLineEdit()
1276 le.setStatusTip(
1277 'Latitude, Longitude, Depth [km] or city name: '
1278 'Focal point location.')
1279 layout.addWidget(le, 1, 0, 1, 1)
1281 def lat_lon_depth_to_lineedit(state, widget):
1282 widget.setText('%g, %g, %g' % (
1283 state.lat, state.lon, state.depth / km))
1285 def lineedit_to_lat_lon_depth(widget, state):
1286 self.disable_capture()
1287 try:
1288 s = str(widget.text())
1289 choices = location_to_choices(s)
1290 if len(choices) > 0:
1291 self.state.lat, self.state.lon, self.state.depth = \
1292 choices[0].get_lat_lon_depth()
1293 else:
1294 raise NoLocationChoices(s)
1296 finally:
1297 self.enable_capture()
1299 self._state_bind(
1300 ['lat', 'lon', 'depth'],
1301 lineedit_to_lat_lon_depth,
1302 le, [le.editingFinished, le.returnPressed],
1303 lat_lon_depth_to_lineedit)
1305 self.lat_lon_lineedit = le
1307 # focal point
1309 cb = qw.QCheckBox('Fix')
1310 cb.setStatusTip(
1311 'Fix location. Orbit focal point without pressing %s.'
1312 % g_modifier_key)
1313 layout.addWidget(cb, 1, 1, 1, 1)
1315 def focal_point_to_checkbox(state, widget):
1316 widget.blockSignals(True)
1317 widget.setChecked(self.gui_state.focal_point != 'center')
1318 widget.blockSignals(False)
1320 def checkbox_to_focal_point(widget, state):
1321 self.gui_state.focal_point = \
1322 'target' if widget.isChecked() else 'center'
1324 self._gui_state_bind(
1325 ['focal_point'], checkbox_to_focal_point,
1326 cb, [cb.toggled], focal_point_to_checkbox)
1328 self.focal_point_checkbox = cb
1330 self.talkie_connect(
1331 self.gui_state, 'focal_point', self.update_focal_point)
1333 self.update_focal_point()
1335 # strike, dip
1337 layout.addWidget(
1338 qw.QLabel('View Plane'), 2, 0, 1, 2)
1340 le = qw.QLineEdit()
1341 le.setStatusTip(
1342 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1343 'direction.')
1344 layout.addWidget(le, 3, 0, 1, 1)
1346 def strike_dip_to_lineedit(state, widget):
1347 widget.setText('%g, %g' % (state.strike, state.dip))
1349 def lineedit_to_strike_dip(widget, state):
1350 s = str(widget.text())
1351 string_to_strike_dip = {
1352 'east': (0., 90.),
1353 'west': (180., 90.),
1354 'south': (90., 90.),
1355 'north': (270., 90.),
1356 'top': (90., 0.),
1357 'bottom': (90., 180.)}
1359 self.disable_capture()
1360 if s in string_to_strike_dip:
1361 state.strike, state.dip = string_to_strike_dip[s]
1363 s = s.replace(',', ' ')
1364 try:
1365 state.strike, state.dip = map(float, s.split())
1366 except Exception:
1367 raise ValueError('need two numerical values: <strike>, <dip>')
1368 finally:
1369 self.enable_capture()
1371 self._state_bind(
1372 ['strike', 'dip'], lineedit_to_strike_dip,
1373 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1375 self.strike_dip_lineedit = le
1377 but = qw.QPushButton('Reset')
1378 but.setStatusTip('Reset to north-up map view.')
1379 but.clicked.connect(self.reset_strike_dip)
1380 layout.addWidget(but, 3, 1, 1, 1)
1382 # crosshair
1384 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1385 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1387 # camera bindings
1388 self.talkie_connect(
1389 self.state,
1390 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1391 self.update_camera)
1393 self.talkie_connect(
1394 self.gui_state, 'panels_visible', self.update_panel_visibility)
1396 return frame
1398 def controls_time(self):
1399 frame = qw.QFrame(self)
1400 frame.setSizePolicy(
1401 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1403 layout = qw.QGridLayout()
1404 frame.setLayout(layout)
1406 layout.addWidget(qw.QLabel('Min'), 0, 0)
1407 le_tmin = qw.QLineEdit()
1408 layout.addWidget(le_tmin, 0, 1)
1410 layout.addWidget(qw.QLabel('Max'), 1, 0)
1411 le_tmax = qw.QLineEdit()
1412 layout.addWidget(le_tmax, 1, 1)
1414 label_tcursor = qw.QLabel()
1416 label_tcursor.setSizePolicy(
1417 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1419 layout.addWidget(label_tcursor, 2, 1)
1420 self._label_tcursor = label_tcursor
1422 def time_to_lineedit(state, attribute, widget):
1423 widget.setText(
1424 common.time_or_none_to_str(getattr(state, attribute)))
1426 def lineedit_to_time(widget, state, attribute):
1427 from pyrocko.util import str_to_time_fillup
1429 s = str(widget.text())
1430 if not s.strip():
1431 setattr(state, attribute, None)
1432 else:
1433 try:
1434 setattr(state, attribute, str_to_time_fillup(s))
1435 except Exception:
1436 raise ValueError(
1437 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1439 self._state_bind(
1440 ['tmin'], lineedit_to_time, le_tmin,
1441 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1442 attribute='tmin')
1443 self._state_bind(
1444 ['tmax'], lineedit_to_time, le_tmax,
1445 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1446 attribute='tmax')
1448 self.tmin_lineedit = le_tmin
1449 self.tmax_lineedit = le_tmax
1451 range_edit = RangeEdit()
1452 range_edit.rangeEditPressed.connect(self.disable_capture)
1453 range_edit.rangeEditReleased.connect(self.enable_capture)
1454 range_edit.set_data_provider(self)
1455 range_edit.set_data_name('time')
1457 xblock = [False]
1459 def range_to_range_edit(state, widget):
1460 if not xblock[0]:
1461 widget.blockSignals(True)
1462 widget.set_focus(state.tduration, state.tposition)
1463 widget.set_range(state.tmin, state.tmax)
1464 widget.blockSignals(False)
1466 def range_edit_to_range(widget, state):
1467 xblock[0] = True
1468 self.state.tduration, self.state.tposition = widget.get_focus()
1469 self.state.tmin, self.state.tmax = widget.get_range()
1470 xblock[0] = False
1472 self._state_bind(
1473 ['tmin', 'tmax', 'tduration', 'tposition'],
1474 range_edit_to_range,
1475 range_edit,
1476 [range_edit.rangeChanged, range_edit.focusChanged],
1477 range_to_range_edit)
1479 def handle_tcursor_changed():
1480 self.gui_state.tcursor = range_edit.get_tcursor()
1482 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1484 layout.addWidget(range_edit, 3, 0, 1, 2)
1486 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1487 le_focus = qw.QLineEdit()
1488 layout.addWidget(le_focus, 4, 1)
1490 def focus_to_lineedit(state, widget):
1491 if state.tduration is None:
1492 widget.setText('')
1493 else:
1494 widget.setText('%s, %g' % (
1495 guts.str_duration(state.tduration),
1496 state.tposition))
1498 def lineedit_to_focus(widget, state):
1499 s = str(widget.text())
1500 w = [x.strip() for x in s.split(',')]
1501 try:
1502 if len(w) == 0 or not w[0]:
1503 state.tduration = None
1504 state.tposition = 0.0
1505 else:
1506 state.tduration = guts.parse_duration(w[0])
1507 if len(w) > 1:
1508 state.tposition = float(w[1])
1509 else:
1510 state.tposition = 0.0
1512 except Exception:
1513 raise ValueError('need two values: <duration>, <position>')
1515 self._state_bind(
1516 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1517 [le_focus.editingFinished, le_focus.returnPressed],
1518 focus_to_lineedit)
1520 label_effective_tmin = qw.QLabel()
1521 label_effective_tmax = qw.QLabel()
1523 label_effective_tmin.setSizePolicy(
1524 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1525 label_effective_tmax.setSizePolicy(
1526 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1527 label_effective_tmin.setMinimumSize(
1528 qg.QFontMetrics(label_effective_tmin.font()).width(
1529 '0000-00-00 00:00:00.000 '), 0)
1531 layout.addWidget(label_effective_tmin, 5, 1)
1532 layout.addWidget(label_effective_tmax, 6, 1)
1534 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1535 self.talkie_connect(
1536 self.state, var, self.update_effective_time_labels)
1538 self._label_effective_tmin = label_effective_tmin
1539 self._label_effective_tmax = label_effective_tmax
1541 self.talkie_connect(
1542 self.gui_state, 'tcursor', self.update_tcursor)
1544 return frame
1546 def controls_appearance(self):
1547 frame = qw.QFrame(self)
1548 frame.setSizePolicy(
1549 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1550 layout = qw.QGridLayout()
1551 frame.setLayout(layout)
1553 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1555 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1556 layout.addWidget(cb, 0, 1)
1557 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1559 self.talkie_connect(
1560 self.state, 'lighting', self.update_render_settings)
1562 # background
1564 layout.addWidget(qw.QLabel('Background'), 1, 0)
1566 cb = common.strings_to_combobox(
1567 ['black', 'white', 'skyblue1 - white'])
1569 layout.addWidget(cb, 1, 1)
1570 vstate.state_bind_combobox_background(
1571 self, self.state, 'background', cb)
1573 self.talkie_connect(
1574 self.state, 'background', self.update_render_settings)
1576 return frame
1578 def controls_snapshots(self):
1579 return snapshots_mod.SnapshotsPanel(self)
1581 def update_effective_time_labels(self, *args):
1582 tmin = self.state.tmin_effective
1583 tmax = self.state.tmax_effective
1585 stmin = common.time_or_none_to_str(tmin)
1586 stmax = common.time_or_none_to_str(tmax)
1588 self._label_effective_tmin.setText(stmin)
1589 self._label_effective_tmax.setText(stmax)
1591 def update_tcursor(self, *args):
1592 tcursor = self.gui_state.tcursor
1593 stcursor = common.time_or_none_to_str(tcursor)
1594 self._label_tcursor.setText(stcursor)
1596 def reset_strike_dip(self, *args):
1597 self.state.strike = 90.
1598 self.state.dip = 0
1599 self.gui_state.focal_point = 'center'
1601 def get_camera_geometry(self):
1603 def rtp2xyz(rtp):
1604 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1606 radius = 1.0 - self.state.depth / self.planet_radius
1608 cam_rtp = num.array([
1609 radius+self.state.distance,
1610 self.state.lat * d2r + 0.5*num.pi,
1611 self.state.lon * d2r])
1612 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1613 cam, up, foc = \
1614 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1616 foc_rtp = num.array([
1617 radius,
1618 self.state.lat * d2r + 0.5*num.pi,
1619 self.state.lon * d2r])
1621 foc = rtp2xyz(foc_rtp)
1623 rot_world = pmt.euler_to_matrix(
1624 -(self.state.lat-90.)*d2r,
1625 (self.state.lon+90.)*d2r,
1626 0.0*d2r).T
1628 rot_cam = pmt.euler_to_matrix(
1629 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1631 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1633 cam = foc + num.dot(rot, cam - foc)
1634 up = num.dot(rot, up)
1635 return cam, up, foc
1637 def update_camera(self, *args):
1638 cam, up, foc = self.get_camera_geometry()
1639 camera = self.ren.GetActiveCamera()
1640 camera.SetPosition(*cam)
1641 camera.SetFocalPoint(*foc)
1642 camera.SetViewUp(*up)
1644 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1646 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1647 self.feature_radius_min / self.planet_radius)**2))
1649 # if horizon == 0.0:
1650 # horizon = 2.0 + self.state.distance
1652 # clip_dist = max(min(self.state.distance*5., max(
1653 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1654 # , math.sqrt(num.sum(cam**2)))
1655 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1656 # clip_dist = feature_horizon
1658 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1660 self.camera_params = (
1661 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1663 self.update_view()
1665 def add_panel(
1666 self, title_label, panel,
1667 visible=False,
1668 # volatile=False,
1669 tabify=True,
1670 where=qc.Qt.RightDockWidgetArea,
1671 remove=None,
1672 title_controls=[]):
1674 dockwidget = common.MyDockWidget(
1675 self, title_label, title_controls=title_controls)
1677 if not visible:
1678 dockwidget.hide()
1680 if not self.gui_state.panels_visible:
1681 dockwidget.block()
1683 dockwidget.setWidget(panel)
1685 panel.setParent(dockwidget)
1687 dockwidgets = self.findChildren(common.MyDockWidget)
1688 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1690 self.addDockWidget(where, dockwidget)
1692 nwrap = 4
1693 if dws and len(dws) >= nwrap and tabify:
1694 self.tabifyDockWidget(
1695 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1697 mitem = dockwidget.toggleViewAction()
1699 def update_label(*args):
1700 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1701 self.update_slug_abbreviated_lengths()
1703 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1704 dockwidget.titlebar._title_label.title_changed.connect(
1705 self.update_slug_abbreviated_lengths)
1707 update_label()
1709 self._panel_togglers[dockwidget] = mitem
1710 self.panels_menu.addAction(mitem)
1711 if visible:
1712 dockwidget.setVisible(True)
1713 dockwidget.setFocus()
1714 dockwidget.raise_()
1716 def stack_panels(self):
1717 dockwidgets = self.findChildren(common.MyDockWidget)
1718 by_area = defaultdict(list)
1719 for dw in dockwidgets:
1720 area = self.dockWidgetArea(dw)
1721 by_area[area].append(dw)
1723 for dockwidgets in by_area.values():
1724 dw_last = None
1725 for dw in dockwidgets:
1726 if dw_last is not None:
1727 self.tabifyDockWidget(dw_last, dw)
1729 dw_last = dw
1731 def update_slug_abbreviated_lengths(self):
1732 dockwidgets = self.findChildren(common.MyDockWidget)
1733 title_labels = []
1734 for dw in dockwidgets:
1735 title_labels.append(dw.titlebar._title_label)
1737 by_title = defaultdict(list)
1738 for tl in title_labels:
1739 by_title[tl.get_title()].append(tl)
1741 for group in by_title.values():
1742 slugs = [tl.get_slug() for tl in group]
1744 n = max(len(slug) for slug in slugs)
1745 nunique = len(set(slugs))
1747 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1748 n -= 1
1750 if n > 0:
1751 n = max(3, n)
1753 for tl in group:
1754 tl.set_slug_abbreviated_length(n)
1756 def raise_panel(self, panel):
1757 dockwidget = panel.parent()
1758 dockwidget.setVisible(True)
1759 dockwidget.setFocus()
1760 dockwidget.raise_()
1762 def toggle_panel_visibility(self):
1763 self.gui_state.panels_visible = not self.gui_state.panels_visible
1765 def update_panel_visibility(self, *args):
1766 self.setUpdatesEnabled(False)
1767 mbar = self.menuBar()
1768 sbar = self.statusBar()
1769 dockwidgets = self.findChildren(common.MyDockWidget)
1771 # Set height to zero instead of hiding so that shortcuts still work
1772 # otherwise one would have to mess around with separate QShortcut
1773 # objects.
1774 mbar.setFixedHeight(
1775 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1777 sbar.setVisible(self.gui_state.panels_visible)
1778 for dockwidget in dockwidgets:
1779 dockwidget.setBlocked(not self.gui_state.panels_visible)
1781 self.setUpdatesEnabled(True)
1783 def remove_panel(self, panel):
1784 dockwidget = panel.parent()
1785 self.removeDockWidget(dockwidget)
1786 dockwidget.setParent(None)
1787 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1789 def register_data_provider(self, provider):
1790 if provider not in self.data_providers:
1791 self.data_providers.append(provider)
1793 def unregister_data_provider(self, provider):
1794 if provider in self.data_providers:
1795 self.data_providers.remove(provider)
1797 def iter_data(self, name):
1798 for provider in self.data_providers:
1799 for data in provider.iter_data(name):
1800 yield data
1802 def closeEvent(self, event):
1803 self.attach()
1804 event.accept()
1805 self.closing = True
1806 common.get_app().set_main_window(None)
1808 def is_closing(self):
1809 return self.closing
1812class SparrowApp(qw.QApplication):
1813 def __init__(self):
1814 qw.QApplication.__init__(self, ['Sparrow'])
1815 self.lastWindowClosed.connect(self.myQuit)
1816 self._main_window = None
1817 self.setApplicationDisplayName('Sparrow')
1818 self.setDesktopFileName('Sparrow')
1820 def install_sigint_handler(self):
1821 self._old_signal_handler = signal.signal(
1822 signal.SIGINT, self.myCloseAllWindows)
1824 def uninstall_sigint_handler(self):
1825 signal.signal(signal.SIGINT, self._old_signal_handler)
1827 def myQuit(self, *args):
1828 self.quit()
1830 def myCloseAllWindows(self, *args):
1831 self.closeAllWindows()
1833 def set_main_window(self, win):
1834 self._main_window = win
1836 def get_main_window(self):
1837 return self._main_window
1839 def get_progressbars(self):
1840 if self._main_window:
1841 return self._main_window.progressbars
1842 else:
1843 return None
1845 def status(self, message, duration=None):
1846 win = self.get_main_window()
1847 if not win:
1848 return
1850 win.statusBar().showMessage(
1851 message, int((duration or 0) * 1000))
1854def main(*args, **kwargs):
1856 from pyrocko import util
1857 from pyrocko.gui import util as gui_util
1858 util.setup_logging('sparrow', 'info')
1860 global win
1862 if gui_util.app is None:
1863 gui_util.app = SparrowApp()
1865 # try:
1866 # from qt_material import apply_stylesheet
1867 #
1868 # apply_stylesheet(app, theme='dark_teal.xml')
1869 #
1870 #
1871 # import qdarkgraystyle
1872 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1873 # import qdarkstyle
1874 #
1875 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1876 #
1877 #
1878 # except ImportError:
1879 # logger.info(
1880 # 'Module qdarkgraystyle not available.\n'
1881 # 'If wanted, install qdarkstyle with "pip install '
1882 # 'qdarkgraystyle".')
1883 #
1884 win = SparrowViewer(*args, **kwargs)
1886 gui_util.app.install_sigint_handler()
1887 gui_util.app.exec_()
1888 gui_util.app.uninstall_sigint_handler()
1890 del win
1892 gc.collect()
1894 del gui_util.app