Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/main.py: 72%
1081 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
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
25from pyrocko.dataset.util import set_download_callback
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):
288 download_progress_update = qc.pyqtSignal()
290 def __init__(
291 self,
292 use_depth_peeling=True,
293 events=None,
294 snapshots=None,
295 instant_close=False):
297 common.set_viewer(self)
299 qw.QMainWindow.__init__(self)
300 TalkieConnectionOwner.__init__(self)
302 self.instant_close = instant_close
304 self.state = vstate.ViewerState()
305 self.gui_state = vstate.ViewerGuiState()
307 self.setWindowTitle('Sparrow')
309 self.setTabPosition(
310 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
312 self.planet_radius = cake.earthradius
313 self.feature_radius_min = cake.earthradius - 1000. * km
315 self._block_capture = 0
316 self._undo_stack = []
317 self._redo_stack = []
318 self._undo_aggregate = None
320 self._panel_togglers = {}
321 self._actors = set()
322 self._actors_2d = set()
323 self._render_window_size = (0, 0)
324 self._use_depth_peeling = use_depth_peeling
325 self._in_update_elements = False
326 self._update_elements_enabled = True
328 self._animation_tstart = None
329 self._animation_iframe = None
330 self._animation = None
332 mbar = qw.QMenuBar()
333 self.setMenuBar(mbar)
335 menu = mbar.addMenu('File')
337 menu.addAction(
338 'Export Image...',
339 self.export_image,
340 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
341 qc.Qt.ApplicationShortcut)
343 menu.addAction(
344 'Quit',
345 self.close,
346 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
347 qc.Qt.ApplicationShortcut)
349 menu = mbar.addMenu('Edit')
351 menu.addAction(
352 'Undo',
353 self.undo,
354 qg.QKeySequence(
355 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext(
356 qc.Qt.ApplicationShortcut)
358 menu.addAction(
359 'Redo',
360 self.redo,
361 qg.QKeySequence(
362 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext(
363 qc.Qt.ApplicationShortcut)
365 menu = mbar.addMenu('View')
366 menu_sizes = menu.addMenu('Size')
367 self._add_vtk_widget_size_menu_entries(menu_sizes)
369 # detached/attached
370 self.talkie_connect(
371 self.gui_state, 'detached', self.update_detached)
373 action = qw.QAction('Detach')
374 action.setCheckable(True)
375 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
376 action.setShortcutContext(qc.Qt.ApplicationShortcut)
378 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
379 menu.addAction(action)
381 # hide controls
382 action = qw.QAction('Hide Controls', self)
383 action.setCheckable(True)
384 action.setShortcut(qc.Qt.Key_Space)
385 action.setShortcutContext(qc.Qt.ApplicationShortcut)
386 action.triggered.connect(self.toggle_panel_visibility)
387 menu.addAction(action)
389 self.panels_menu = mbar.addMenu('Panels')
390 self.panels_menu.addAction(
391 'Stack Panels',
392 self.stack_panels)
393 self.panels_menu.addSeparator()
395 snapshots_menu = mbar.addMenu('Snapshots')
397 menu = mbar.addMenu('Elements')
398 for name, estate in sorted([
399 ('Icosphere', elements.IcosphereState(
400 level=4,
401 smooth=True,
402 opacity=0.5,
403 ambient=0.1)),
404 ('Grid', elements.GridState()),
405 ('Stations', elements.StationsState()),
406 ('Topography', elements.TopoState()),
407 ('Custom Topography', elements.CustomTopoState()),
408 ('Catalog', elements.CatalogState()),
409 ('Coastlines', elements.CoastlinesState()),
410 ('Rectangular Source', elements.SourceState()),
411 ('HUD Subtitle', elements.HudState(
412 template='Subtitle')),
413 ('HUD (tmax_effective)', elements.HudState(
414 template='tmax: {tmax_effective|date}',
415 position='top-left')),
416 ('AxesBox', elements.AxesBoxState()),
417 ('Volcanoes', elements.VolcanoesState()),
418 ('Faults', elements.ActiveFaultsState()),
419 ('Plate bounds', elements.PlatesBoundsState()),
420 ('InSAR Surface Displacements', elements.KiteState()),
421 ('Geometry', elements.GeometryState()),
422 ('Spheroid', elements.SpheroidState())]):
424 def wrap_add_element(estate):
425 def add_element(*args):
426 new_element = guts.clone(estate)
427 new_element.element_id = elements.random_id()
428 self.state.elements.append(new_element)
429 self.state.sort_elements()
431 return add_element
433 mitem = qw.QAction(name, self)
435 mitem.triggered.connect(wrap_add_element(estate))
437 menu.addAction(mitem)
439 menu = mbar.addMenu('Help')
441 menu.addAction(
442 'Interactive Tour',
443 self.start_tour)
445 menu.addAction(
446 'Online Manual',
447 self.open_manual)
449 self.data_providers = []
450 self.elements = {}
452 self.detached_window = None
454 self.main_frame = qw.QFrame()
455 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
457 self.vtk_frame = CenteringScrollArea()
459 self.vtk_widget = QVTKWidget(self, self)
460 self.vtk_frame.setWidget(self.vtk_widget)
462 self.main_layout = qw.QVBoxLayout()
463 self.main_layout.setContentsMargins(0, 0, 0, 0)
464 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
466 pb = Progressbars(self)
467 self.progressbars = pb
468 self.main_layout.addWidget(pb)
470 self.main_frame.setLayout(self.main_layout)
472 self.vtk_frame_substitute = None
474 self.add_panel(
475 'Navigation',
476 self.controls_navigation(),
477 visible=True,
478 scrollable=False,
479 where=qc.Qt.LeftDockWidgetArea)
481 self.add_panel(
482 'Time',
483 self.controls_time(),
484 visible=True,
485 scrollable=False,
486 where=qc.Qt.LeftDockWidgetArea)
488 self.add_panel(
489 'Appearance',
490 self.controls_appearance(),
491 visible=True,
492 scrollable=False,
493 where=qc.Qt.LeftDockWidgetArea)
495 snapshots_panel = self.controls_snapshots()
496 self.snapshots_panel = snapshots_panel
497 self.add_panel(
498 'Snapshots',
499 snapshots_panel,
500 visible=False,
501 scrollable=False,
502 where=qc.Qt.LeftDockWidgetArea)
504 snapshots_panel.setup_menu(snapshots_menu)
506 self.setCentralWidget(self.main_frame)
508 self.mesh = None
510 ren = vtk.vtkRenderer()
512 # ren.SetBackground(0.15, 0.15, 0.15)
513 # ren.SetBackground(0.0, 0.0, 0.0)
514 # ren.TwoSidedLightingOn()
515 # ren.SetUseShadows(1)
517 self._lighting = None
518 self._background = None
520 self.ren = ren
521 self.update_render_settings()
522 self.update_camera()
524 renwin = self.vtk_widget.GetRenderWindow()
526 if self._use_depth_peeling:
527 renwin.SetAlphaBitPlanes(1)
528 renwin.SetMultiSamples(0)
530 ren.SetUseDepthPeeling(1)
531 ren.SetMaximumNumberOfPeels(100)
532 ren.SetOcclusionRatio(0.1)
534 ren.SetUseFXAA(1)
535 # ren.SetUseHiddenLineRemoval(1)
536 # ren.SetBackingStore(1)
538 self.renwin = renwin
540 # renwin.LineSmoothingOn()
541 # renwin.PointSmoothingOn()
542 # renwin.PolygonSmoothingOn()
544 renwin.AddRenderer(ren)
546 iren = renwin.GetInteractor()
547 iren.LightFollowCameraOn()
548 iren.SetInteractorStyle(None)
550 iren.AddObserver('LeftButtonPressEvent', self.button_event)
551 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
552 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
553 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
554 iren.AddObserver('RightButtonPressEvent', self.button_event)
555 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
556 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
557 iren.AddObserver('KeyPressEvent', self.key_down_event)
558 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
560 renwin.Render()
562 iren.Initialize()
564 self.iren = iren
566 self.rotating = False
568 self._elements = {}
569 self._elements_active = {}
571 self.talkie_connect(
572 self.state, 'elements', self.update_elements)
574 self.state.elements.append(elements.IcosphereState(
575 element_id='icosphere',
576 level=4,
577 smooth=True,
578 opacity=0.5,
579 ambient=0.1))
581 self.state.elements.append(elements.GridState(
582 element_id='grid'))
583 self.state.elements.append(elements.CoastlinesState(
584 element_id='coastlines'))
585 self.state.elements.append(elements.CrosshairState(
586 element_id='crosshair'))
588 # self.state.elements.append(elements.StationsState())
589 # self.state.elements.append(elements.SourceState())
590 # self.state.elements.append(
591 # elements.CatalogState(
592 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
593 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
595 if events:
596 self.state.elements.append(
597 elements.CatalogState(
598 selection=elements.MemoryCatalogSelection(events=events)))
600 self.state.sort_elements()
602 if snapshots:
603 snapshots_ = []
604 for obj in snapshots:
605 if isinstance(obj, str):
606 snapshots_.extend(snapshots_mod.load_snapshots(obj))
607 else:
608 snapshots_.append(obj)
610 snapshots_panel.add_snapshots(snapshots_)
611 self.raise_panel(snapshots_panel)
612 snapshots_panel.goto_snapshot(1)
614 self.timer = qc.QTimer(self)
615 self.timer.timeout.connect(self.periodical)
616 self.timer.setInterval(1000)
617 self.timer.start()
619 self._animation_saver = None
621 self.closing = False
622 self.vtk_widget.setFocus()
624 self.update_detached()
626 self.status(
627 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
629 self.status(
630 'Let\'s fly.', 2.0)
632 self.show()
633 self.windowHandle().showMaximized()
635 self.talkie_connect(
636 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
638 self.update_vtk_widget_size()
640 hatch_path = config.expand(os.path.join(
641 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
643 self.talkie_connect(self.state, '', self.capture_state)
644 self.capture_state()
646 set_download_callback(self.update_download_progress)
648 if not os.path.exists(hatch_path):
649 with open(hatch_path, 'w') as f:
650 f.write('%s\n' % util.time_to_str(time.time()))
652 self.start_tour()
654 def update_download_progress(self, message, args):
655 self.download_progress_update.emit()
657 def status(self, message, duration=None):
658 self.statusBar().showMessage(
659 message, int((duration or 0) * 1000))
661 def disable_capture(self):
662 self._block_capture += 1
664 logger.debug('Undo capture block (+1): %i' % self._block_capture)
666 def enable_capture(self, drop=False, aggregate=None):
667 if self._block_capture > 0:
668 self._block_capture -= 1
670 logger.debug('Undo capture block (-1): %i' % self._block_capture)
672 if self._block_capture == 0 and not drop:
673 self.capture_state(aggregate=aggregate)
675 def capture_state(self, *args, aggregate=None):
676 if self._block_capture:
677 return
679 if len(self._undo_stack) == 0 or not state_equal(
680 self.state, self._undo_stack[-1]):
682 if aggregate is not None:
683 if aggregate == self._undo_aggregate:
684 self._undo_stack.pop()
686 self._undo_aggregate = aggregate
687 else:
688 self._undo_aggregate = None
690 logger.debug('Capture undo state (%i%s)\n%s' % (
691 len(self._undo_stack) + 1,
692 '' if aggregate is None else ', aggregate=%s' % aggregate,
693 '\n'.join(
694 ' - %s' % s
695 for s in self._undo_stack[-1].str_diff(
696 self.state).splitlines())
697 if len(self._undo_stack) > 0 else 'initial'))
699 self._undo_stack.append(guts.clone(self.state))
700 self._redo_stack.clear()
702 def undo(self):
703 self._undo_aggregate = None
705 if len(self._undo_stack) <= 1:
706 return
708 state = self._undo_stack.pop()
709 self._redo_stack.append(state)
710 state = self._undo_stack[-1]
712 logger.debug('Undo (%i)\n%s' % (
713 len(self._undo_stack),
714 '\n'.join(
715 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
717 self.disable_capture()
718 try:
719 self.set_state(state)
720 finally:
721 self.enable_capture(drop=True)
723 def redo(self):
724 self._undo_aggregate = None
726 if len(self._redo_stack) == 0:
727 return
729 state = self._redo_stack.pop()
730 self._undo_stack.append(state)
732 logger.debug('Redo (%i)\n%s' % (
733 len(self._redo_stack),
734 '\n'.join(
735 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
737 self.disable_capture()
738 try:
739 self.set_state(state)
740 finally:
741 self.enable_capture(drop=True)
743 def start_tour(self):
744 snapshots_ = snapshots_mod.load_snapshots(
745 'https://data.pyrocko.org/examples/'
746 'sparrow-tour-v0.1.snapshots.yaml')
747 self.snapshots_panel.add_snapshots(snapshots_)
748 self.raise_panel(self.snapshots_panel)
749 self.snapshots_panel.transition_to_next_snapshot()
751 def open_manual(self):
752 import webbrowser
753 webbrowser.open(
754 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
756 def _add_vtk_widget_size_menu_entries(self, menu):
758 group = qw.QActionGroup(menu)
759 group.setExclusive(True)
761 def set_variable_size():
762 self.gui_state.fixed_size = False
764 variable_size_action = menu.addAction('Fit Window Size')
765 variable_size_action.setCheckable(True)
766 variable_size_action.setActionGroup(group)
767 variable_size_action.triggered.connect(set_variable_size)
769 fixed_size_items = []
770 for nx, ny, label in [
771 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
772 (426, 240, ''),
773 (640, 360, ''),
774 (854, 480, '(FWVGA)'),
775 (1280, 720, '(HD)'),
776 (1920, 1080, '(Full HD)'),
777 (2560, 1440, '(Quad HD)'),
778 (3840, 2160, '(4K UHD)'),
779 (3840*2, 2160*2, '',),
780 (None, None, 'Aspect 4:3'),
781 (640, 480, '(VGA)'),
782 (800, 600, '(SVGA)'),
783 (None, None, 'Other'),
784 (512, 512, ''),
785 (1024, 1024, '')]:
787 if None in (nx, ny):
788 menu.addSection(label)
789 else:
790 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
791 action = menu.addAction(name)
792 action.setCheckable(True)
793 action.setActionGroup(group)
794 fixed_size_items.append((action, (nx, ny)))
796 def make_set_fixed_size(nx, ny):
797 def set_fixed_size():
798 self.gui_state.fixed_size = (float(nx), float(ny))
800 return set_fixed_size
802 action.triggered.connect(make_set_fixed_size(nx, ny))
804 def update_widget(*args):
805 for action, (nx, ny) in fixed_size_items:
806 action.blockSignals(True)
807 action.setChecked(
808 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
809 int(z) for z in self.gui_state.fixed_size)))
810 action.blockSignals(False)
812 variable_size_action.blockSignals(True)
813 variable_size_action.setChecked(not self.gui_state.fixed_size)
814 variable_size_action.blockSignals(False)
816 update_widget()
817 self.talkie_connect(
818 self.gui_state, 'fixed_size', update_widget)
820 def update_vtk_widget_size(self, *args):
821 if self.gui_state.fixed_size:
822 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
823 wanted_size = qc.QSize(nx, ny)
824 else:
825 wanted_size = qc.QSize(
826 self.vtk_frame.window().width(), self.vtk_frame.height())
828 current_size = self.vtk_widget.size()
830 if current_size.width() != wanted_size.width() \
831 or current_size.height() != wanted_size.height():
833 self.vtk_widget.setFixedSize(wanted_size)
835 self.vtk_frame.recenter()
836 self.check_vtk_resize()
838 def update_focal_point(self, *args):
839 if self.gui_state.focal_point == 'center':
840 self.vtk_widget.setStatusTip(
841 'Click and drag: change location. %s-click and drag: '
842 'change view plane orientation.' % g_modifier_key)
843 else:
844 self.vtk_widget.setStatusTip(
845 '%s-click and drag: change location. Click and drag: '
846 'change view plane orientation. Uncheck "Navigation: Fix" to '
847 'reverse sense.' % g_modifier_key)
849 def update_detached(self, *args):
851 if self.gui_state.detached and not self.detached_window: # detach
852 logger.debug('Detaching VTK view.')
854 self.main_layout.removeWidget(self.vtk_frame)
855 self.detached_window = DetachedViewer(self, self.vtk_frame)
856 self.detached_window.show()
857 self.vtk_widget.setFocus()
859 screens = common.get_app().screens()
860 if len(screens) > 1:
861 for screen in screens:
862 if screen is not self.screen():
863 self.detached_window.windowHandle().setScreen(screen)
864 # .setScreen() does not work reliably,
865 # therefore trying also with .move()...
866 p = screen.geometry().topLeft()
867 self.detached_window.move(p.x() + 50, p.y() + 50)
868 # ... but also does not work in notion window manager.
870 self.detached_window.windowHandle().showMaximized()
872 frame = qw.QFrame()
873 # frame.setFrameShape(qw.QFrame.NoFrame)
874 # frame.setBackgroundRole(qg.QPalette.Mid)
875 # frame.setAutoFillBackground(True)
876 frame.setSizePolicy(
877 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
879 layout = qw.QGridLayout()
880 frame.setLayout(layout)
881 self.main_layout.insertWidget(0, frame)
883 self.state_editor = StateEditor(self)
885 layout.addWidget(self.state_editor, 0, 0)
887 # attach_button = qw.QPushButton('Attach View')
888 # attach_button.clicked.connect(self.attach)
889 # layout.addWidget(
890 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
892 self.vtk_frame_substitute = frame
894 if not self.gui_state.detached and self.detached_window: # attach
895 logger.debug('Attaching VTK view.')
896 self.detached_window.hide()
897 self.vtk_frame.setParent(self)
898 if self.vtk_frame_substitute:
899 self.main_layout.removeWidget(self.vtk_frame_substitute)
900 self.state_editor.unbind_state()
901 self.vtk_frame_substitute = None
903 self.main_layout.insertWidget(0, self.vtk_frame)
904 self.detached_window = None
905 self.vtk_widget.setFocus()
907 def attach(self):
908 self.gui_state.detached = False
910 def export_image(self):
912 caption = 'Export Image'
913 fn_out, _ = qw.QFileDialog.getSaveFileName(
914 self, caption, 'image.png',
915 options=common.qfiledialog_options)
917 if fn_out:
918 self.save_image(fn_out)
920 def save_image(self, path):
922 original_fixed_size = self.gui_state.fixed_size
923 if original_fixed_size is None:
924 self.gui_state.fixed_size = (1920., 1080.)
926 wif = vtk.vtkWindowToImageFilter()
927 wif.SetInput(self.renwin)
928 wif.SetInputBufferTypeToRGBA()
929 wif.SetScale(1, 1)
930 wif.ReadFrontBufferOff()
931 writer = vtk.vtkPNGWriter()
932 writer.SetInputConnection(wif.GetOutputPort())
934 self.renwin.Render()
935 wif.Modified()
936 writer.SetFileName(path)
937 writer.Write()
939 self.gui_state.fixed_size = original_fixed_size
941 def update_render_settings(self, *args):
942 if self._lighting is None or self._lighting != self.state.lighting:
943 self.ren.RemoveAllLights()
944 for li in light.get_lights(self.state.lighting):
945 self.ren.AddLight(li)
947 self._lighting = self.state.lighting
949 if self._background is None \
950 or self._background != self.state.background:
952 self.state.background.vtk_apply(self.ren)
953 self._background = self.state.background
955 self.update_view()
957 def start_animation(self, interpolator, output_path=None):
958 if self._animation:
959 logger.debug('Aborting animation in progress to start a new one.')
960 self.stop_animation()
962 self.disable_capture()
963 self._animation = interpolator
964 if output_path is None:
965 self._animation_tstart = time.time()
966 self._animation_iframe = None
967 else:
968 self._animation_iframe = 0
969 mess = 'Rendering movie'
970 self.progressbars.set_status(mess, 0, can_abort=True)
972 self._animation_timer = qc.QTimer(self)
973 self._animation_timer.timeout.connect(self.next_animation_frame)
974 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
975 self._animation_timer.start()
976 if output_path is not None:
977 original_fixed_size = self.gui_state.fixed_size
978 if original_fixed_size is None:
979 self.gui_state.fixed_size = (1920., 1080.)
981 wif = vtk.vtkWindowToImageFilter()
982 wif.SetInput(self.renwin)
983 wif.SetInputBufferTypeToRGBA()
984 wif.SetScale(1, 1)
985 wif.ReadFrontBufferOff()
986 writer = vtk.vtkPNGWriter()
987 temp_path = tempfile.mkdtemp()
988 self._animation_saver = (
989 wif, writer, temp_path, output_path, original_fixed_size)
990 writer.SetInputConnection(wif.GetOutputPort())
992 def next_animation_frame(self):
994 ani = self._animation
995 if not ani:
996 return
998 if self._animation_iframe is not None:
999 state = ani(
1000 ani.tmin
1001 + self._animation_iframe * ani.dt)
1003 self._animation_iframe += 1
1004 else:
1005 tnow = time.time()
1006 state = ani(min(
1007 ani.tmax,
1008 ani.tmin + (tnow - self._animation_tstart)))
1010 self.set_state(state)
1011 self.renwin.Render()
1012 abort = False
1013 if self._animation_saver:
1014 abort = self.progressbars.set_status(
1015 'Rendering movie',
1016 100*self._animation_iframe*ani.dt / (ani.tmax - ani.tmin),
1017 can_abort=True)
1019 wif, writer, temp_path, _, _ = self._animation_saver
1020 wif.Modified()
1021 fn = os.path.join(temp_path, 'f%09i.png')
1022 writer.SetFileName(fn % self._animation_iframe)
1023 writer.Write()
1025 if self._animation_iframe is not None:
1026 t = self._animation_iframe * ani.dt
1027 else:
1028 t = tnow - self._animation_tstart
1030 if t > ani.tmax - ani.tmin or abort:
1031 self.stop_animation()
1033 def stop_animation(self):
1034 if self._animation_timer:
1035 self._animation_timer.stop()
1037 if self._animation_saver:
1039 wif, writer, temp_path, output_path, original_fixed_size \
1040 = self._animation_saver
1041 self.gui_state.fixed_size = original_fixed_size
1043 fn_path = os.path.join(temp_path, 'f%09d.png')
1044 check_call([
1045 'ffmpeg', '-y',
1046 '-i', fn_path,
1047 '-c:v', 'libx264',
1048 '-preset', 'slow',
1049 '-crf', '17',
1050 '-vf', 'format=yuv420p,fps=%i' % (
1051 int(round(1.0/self._animation.dt))),
1052 output_path])
1053 shutil.rmtree(temp_path)
1055 self._animation_saver = None
1056 self._animation_saver
1058 self.progressbars.set_status(
1059 'Rendering movie', 100, can_abort=True)
1061 self._animation_tstart = None
1062 self._animation_iframe = None
1063 self._animation = None
1064 self.enable_capture()
1066 def set_state(self, state):
1067 self.disable_capture()
1068 try:
1069 self._update_elements_enabled = False
1070 self.setUpdatesEnabled(False)
1071 self.state.diff_update(state)
1072 self.state.sort_elements()
1073 self.setUpdatesEnabled(True)
1074 self._update_elements_enabled = True
1075 self.update_elements()
1076 finally:
1077 self.enable_capture()
1079 def periodical(self):
1080 pass
1082 def check_vtk_resize(self, *args):
1083 render_window_size = self.renwin.GetSize()
1084 if self._render_window_size != render_window_size:
1085 self._render_window_size = render_window_size
1086 self.resize_event(*render_window_size)
1088 def update_elements(self, *_):
1089 if not self._update_elements_enabled:
1090 return
1092 if self._in_update_elements:
1093 return
1095 self._in_update_elements = True
1096 for estate in self.state.elements:
1097 if estate.element_id not in self._elements:
1098 new_element = estate.create()
1099 logger.debug('Creating "%s" ("%s").' % (
1100 type(new_element).__name__,
1101 estate.element_id))
1102 self._elements[estate.element_id] = new_element
1104 element = self._elements[estate.element_id]
1106 if estate.element_id not in self._elements_active:
1107 logger.debug('Adding "%s" ("%s")' % (
1108 type(element).__name__,
1109 estate.element_id))
1110 element.bind_state(estate)
1111 element.set_parent(self)
1112 self._elements_active[estate.element_id] = element
1114 state_element_ids = [el.element_id for el in self.state.elements]
1115 deactivate = []
1116 for element_id, element in self._elements_active.items():
1117 if element_id not in state_element_ids:
1118 logger.debug('Removing "%s" ("%s").' % (
1119 type(element).__name__,
1120 element_id))
1121 element.unset_parent()
1122 deactivate.append(element_id)
1124 for element_id in deactivate:
1125 del self._elements_active[element_id]
1127 self._update_crosshair_bindings()
1129 self._in_update_elements = False
1131 def _update_crosshair_bindings(self):
1133 def get_crosshair_element():
1134 for element in self.state.elements:
1135 if element.element_id == 'crosshair':
1136 return element
1138 return None
1140 crosshair = get_crosshair_element()
1141 if crosshair is None or crosshair.is_connected:
1142 return
1144 def to_checkbox(state, widget):
1145 widget.blockSignals(True)
1146 widget.setChecked(state.visible)
1147 widget.blockSignals(False)
1149 def to_state(widget, state):
1150 state.visible = widget.isChecked()
1152 cb = self._crosshair_checkbox
1153 vstate.state_bind(
1154 self, crosshair, ['visible'], to_state,
1155 cb, [cb.toggled], to_checkbox)
1157 crosshair.is_connected = True
1159 def add_actor_2d(self, actor):
1160 if actor not in self._actors_2d:
1161 self.ren.AddActor2D(actor)
1162 self._actors_2d.add(actor)
1164 def remove_actor_2d(self, actor):
1165 if actor in self._actors_2d:
1166 self.ren.RemoveActor2D(actor)
1167 self._actors_2d.remove(actor)
1169 def add_actor(self, actor):
1170 if actor not in self._actors:
1171 self.ren.AddActor(actor)
1172 self._actors.add(actor)
1174 def add_actor_list(self, actorlist):
1175 for actor in actorlist:
1176 self.add_actor(actor)
1178 def remove_actor(self, actor):
1179 if actor in self._actors:
1180 self.ren.RemoveActor(actor)
1181 self._actors.remove(actor)
1183 def update_view(self):
1184 self.vtk_widget.update()
1186 def resize_event(self, size_x, size_y):
1187 self.gui_state.size = (size_x, size_y)
1189 def button_event(self, obj, event):
1190 if event == "LeftButtonPressEvent":
1191 self.rotating = True
1192 elif event == "LeftButtonReleaseEvent":
1193 self.rotating = False
1195 def mouse_move_event(self, obj, event):
1196 x0, y0 = self.iren.GetLastEventPosition()
1197 x, y = self.iren.GetEventPosition()
1199 size_x, size_y = self.renwin.GetSize()
1200 center_x = size_x / 2.0
1201 center_y = size_y / 2.0
1203 if self.rotating:
1204 self.do_rotate(x, y, x0, y0, center_x, center_y)
1206 def myWheelEvent(self, event):
1208 angle = event.angleDelta().y()
1210 if angle > 200:
1211 angle = 200
1213 if angle < -200:
1214 angle = -200
1216 self.disable_capture()
1217 try:
1218 self.do_dolly(-angle/100.)
1219 finally:
1220 self.enable_capture(aggregate='distance')
1222 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1224 dx = x0 - x
1225 dy = y0 - y
1227 phi = d2r*(self.state.strike - 90.)
1228 focp = self.gui_state.focal_point
1230 if focp == 'center':
1231 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1232 - math.sin(phi) * dx + math.cos(phi) * dy
1234 lat = self.state.lat
1235 lon = self.state.lon
1236 factor = self.state.distance / 10.0
1237 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1238 else:
1239 lat = 90. - self.state.dip
1240 lon = -self.state.strike - 90.
1241 factor = 0.5
1242 factor_lat = 1.0
1244 dlat = dy * factor
1245 dlon = dx * factor * factor_lat
1247 lat = max(min(lat + dlat, 90.), -90.)
1248 lon += dlon
1249 lon = (lon + 180.) % 360. - 180.
1251 if focp == 'center':
1252 self.state.lat = float(lat)
1253 self.state.lon = float(lon)
1254 else:
1255 self.state.dip = float(90. - lat)
1256 self.state.strike = float(((-(lon + 90.))+180.) % 360. - 180.)
1258 def do_dolly(self, v):
1259 self.state.distance *= float(1.0 + 0.1*v)
1261 def key_down_event(self, obj, event):
1262 k = obj.GetKeyCode()
1263 if k == 'f':
1264 self.gui_state.next_focal_point()
1266 elif k == 'r':
1267 self.reset_strike_dip()
1269 elif k == 'p':
1270 print(self.state)
1272 elif k == 'i':
1273 for elem in self.state.elements:
1274 if isinstance(elem, elements.IcosphereState):
1275 elem.visible = not elem.visible
1277 elif k == 'c':
1278 for elem in self.state.elements:
1279 if isinstance(elem, elements.CoastlinesState):
1280 elem.visible = not elem.visible
1282 elif k == 't':
1283 if not any(
1284 isinstance(elem, elements.TopoState)
1285 for elem in self.state.elements):
1287 self.state.elements.append(elements.TopoState())
1288 else:
1289 for elem in self.state.elements:
1290 if isinstance(elem, elements.TopoState):
1291 elem.visible = not elem.visible
1293 # elif k == ' ':
1294 # self.toggle_panel_visibility()
1296 def _state_bind(self, *args, **kwargs):
1297 vstate.state_bind(self, self.state, *args, **kwargs)
1299 def _gui_state_bind(self, *args, **kwargs):
1300 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1302 def controls_navigation(self):
1303 frame = qw.QFrame(self)
1304 frame.setSizePolicy(
1305 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1306 layout = qw.QGridLayout()
1307 frame.setLayout(layout)
1309 # lat, lon, depth
1311 layout.addWidget(
1312 qw.QLabel('Location'), 0, 0, 1, 2)
1314 le = qw.QLineEdit()
1315 le.setStatusTip(
1316 'Latitude, Longitude, Depth [km] or city name: '
1317 'Focal point location.')
1318 layout.addWidget(le, 1, 0, 1, 1)
1320 def lat_lon_depth_to_lineedit(state, widget):
1321 widget.setText('%g, %g, %g' % (
1322 state.lat, state.lon, state.depth / km))
1324 def lineedit_to_lat_lon_depth(widget, state):
1325 self.disable_capture()
1326 try:
1327 s = str(widget.text())
1328 choices = location_to_choices(s)
1329 if len(choices) > 0:
1330 self.state.lat, self.state.lon, self.state.depth = \
1331 choices[0].get_lat_lon_depth()
1332 else:
1333 raise NoLocationChoices(s)
1335 finally:
1336 self.enable_capture()
1338 self._state_bind(
1339 ['lat', 'lon', 'depth'],
1340 lineedit_to_lat_lon_depth,
1341 le, [le.editingFinished, le.returnPressed],
1342 lat_lon_depth_to_lineedit)
1344 self.lat_lon_lineedit = le
1346 # focal point
1348 cb = qw.QCheckBox('Fix')
1349 cb.setStatusTip(
1350 'Fix location. Orbit focal point without pressing %s.'
1351 % g_modifier_key)
1352 layout.addWidget(cb, 1, 1, 1, 1)
1354 def focal_point_to_checkbox(state, widget):
1355 widget.blockSignals(True)
1356 widget.setChecked(self.gui_state.focal_point != 'center')
1357 widget.blockSignals(False)
1359 def checkbox_to_focal_point(widget, state):
1360 self.gui_state.focal_point = \
1361 'target' if widget.isChecked() else 'center'
1363 self._gui_state_bind(
1364 ['focal_point'], checkbox_to_focal_point,
1365 cb, [cb.toggled], focal_point_to_checkbox)
1367 self.focal_point_checkbox = cb
1369 self.talkie_connect(
1370 self.gui_state, 'focal_point', self.update_focal_point)
1372 self.update_focal_point()
1374 # strike, dip
1376 layout.addWidget(
1377 qw.QLabel('View Plane'), 2, 0, 1, 2)
1379 le = qw.QLineEdit()
1380 le.setStatusTip(
1381 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1382 'direction.')
1383 layout.addWidget(le, 3, 0, 1, 1)
1385 def strike_dip_to_lineedit(state, widget):
1386 widget.setText('%g, %g' % (state.strike, state.dip))
1388 def lineedit_to_strike_dip(widget, state):
1389 s = str(widget.text())
1390 string_to_strike_dip = {
1391 'east': (0., 90.),
1392 'west': (180., 90.),
1393 'south': (90., 90.),
1394 'north': (270., 90.),
1395 'top': (90., 0.),
1396 'bottom': (90., 180.)}
1398 self.disable_capture()
1399 if s in string_to_strike_dip:
1400 state.strike, state.dip = string_to_strike_dip[s]
1402 s = s.replace(',', ' ')
1403 try:
1404 state.strike, state.dip = map(float, s.split())
1405 except Exception:
1406 raise ValueError('need two numerical values: <strike>, <dip>')
1407 finally:
1408 self.enable_capture()
1410 self._state_bind(
1411 ['strike', 'dip'], lineedit_to_strike_dip,
1412 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1414 self.strike_dip_lineedit = le
1416 but = qw.QPushButton('Reset')
1417 but.setStatusTip('Reset to north-up map view.')
1418 but.clicked.connect(self.reset_strike_dip)
1419 layout.addWidget(but, 3, 1, 1, 1)
1421 # crosshair
1423 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1424 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1426 # camera bindings
1427 self.talkie_connect(
1428 self.state,
1429 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1430 self.update_camera)
1432 self.talkie_connect(
1433 self.gui_state, 'panels_visible', self.update_panel_visibility)
1435 return frame
1437 def controls_time(self):
1438 frame = qw.QFrame(self)
1439 frame.setSizePolicy(
1440 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1442 layout = qw.QGridLayout()
1443 frame.setLayout(layout)
1445 layout.addWidget(qw.QLabel('Min'), 0, 0)
1446 le_tmin = qw.QLineEdit()
1447 layout.addWidget(le_tmin, 0, 1)
1449 layout.addWidget(qw.QLabel('Max'), 1, 0)
1450 le_tmax = qw.QLineEdit()
1451 layout.addWidget(le_tmax, 1, 1)
1453 label_tcursor = qw.QLabel()
1455 label_tcursor.setSizePolicy(
1456 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1458 layout.addWidget(label_tcursor, 2, 1)
1459 self._label_tcursor = label_tcursor
1461 self._state_bind(
1462 ['tmin'], common.lineedit_to_time, le_tmin,
1463 [le_tmin.editingFinished, le_tmin.returnPressed],
1464 common.time_to_lineedit,
1465 attribute='tmin')
1466 self._state_bind(
1467 ['tmax'], common.lineedit_to_time, le_tmax,
1468 [le_tmax.editingFinished, le_tmax.returnPressed],
1469 common.time_to_lineedit,
1470 attribute='tmax')
1472 self.tmin_lineedit = le_tmin
1473 self.tmax_lineedit = le_tmax
1475 range_edit = RangeEdit()
1476 range_edit.rangeEditPressed.connect(self.disable_capture)
1477 range_edit.rangeEditReleased.connect(self.enable_capture)
1478 range_edit.set_data_provider(self)
1479 range_edit.set_data_name('time')
1481 xblock = [False]
1483 def range_to_range_edit(state, widget):
1484 if not xblock[0]:
1485 widget.blockSignals(True)
1486 widget.set_focus(state.tduration, state.tposition)
1487 widget.set_range(state.tmin, state.tmax)
1488 widget.blockSignals(False)
1490 def range_edit_to_range(widget, state):
1491 xblock[0] = True
1492 self.state.tduration, self.state.tposition = widget.get_focus()
1493 self.state.tmin, self.state.tmax = widget.get_range()
1494 xblock[0] = False
1496 self._state_bind(
1497 ['tmin', 'tmax', 'tduration', 'tposition'],
1498 range_edit_to_range,
1499 range_edit,
1500 [range_edit.rangeChanged, range_edit.focusChanged],
1501 range_to_range_edit)
1503 def handle_tcursor_changed():
1504 self.gui_state.tcursor = range_edit.get_tcursor()
1506 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1508 layout.addWidget(range_edit, 3, 0, 1, 2)
1510 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1511 le_focus = qw.QLineEdit()
1512 layout.addWidget(le_focus, 4, 1)
1514 def focus_to_lineedit(state, widget):
1515 if state.tduration is None:
1516 widget.setText('')
1517 else:
1518 widget.setText('%s, %g' % (
1519 guts.str_duration(state.tduration),
1520 state.tposition))
1522 def lineedit_to_focus(widget, state):
1523 s = str(widget.text())
1524 w = [x.strip() for x in s.split(',')]
1525 try:
1526 if len(w) == 0 or not w[0]:
1527 state.tduration = None
1528 state.tposition = 0.0
1529 else:
1530 state.tduration = guts.parse_duration(w[0])
1531 if len(w) > 1:
1532 state.tposition = float(w[1])
1533 else:
1534 state.tposition = 0.0
1536 except Exception:
1537 raise ValueError('need two values: <duration>, <position>')
1539 self._state_bind(
1540 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1541 [le_focus.editingFinished, le_focus.returnPressed],
1542 focus_to_lineedit)
1544 label_effective_tmin = qw.QLabel()
1545 label_effective_tmax = qw.QLabel()
1547 label_effective_tmin.setSizePolicy(
1548 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1549 label_effective_tmax.setSizePolicy(
1550 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1551 label_effective_tmin.setMinimumSize(
1552 qg.QFontMetrics(label_effective_tmin.font()).width(
1553 '0000-00-00 00:00:00.000 '), 0)
1555 layout.addWidget(label_effective_tmin, 5, 1)
1556 layout.addWidget(label_effective_tmax, 6, 1)
1558 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1559 self.talkie_connect(
1560 self.state, var, self.update_effective_time_labels)
1562 self._label_effective_tmin = label_effective_tmin
1563 self._label_effective_tmax = label_effective_tmax
1565 self.talkie_connect(
1566 self.gui_state, 'tcursor', self.update_tcursor)
1568 return frame
1570 def controls_appearance(self):
1571 frame = qw.QFrame(self)
1572 frame.setSizePolicy(
1573 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1574 layout = qw.QGridLayout()
1575 frame.setLayout(layout)
1577 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1579 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1580 layout.addWidget(cb, 0, 1)
1581 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1583 self.talkie_connect(
1584 self.state, 'lighting', self.update_render_settings)
1586 # background
1588 layout.addWidget(qw.QLabel('Background'), 1, 0)
1590 cb = common.strings_to_combobox(
1591 ['black', 'white', 'skyblue1 - white'])
1593 layout.addWidget(cb, 1, 1)
1594 vstate.state_bind_combobox_background(
1595 self, self.state, 'background', cb)
1597 self.talkie_connect(
1598 self.state, 'background', self.update_render_settings)
1600 return frame
1602 def controls_snapshots(self):
1603 return snapshots_mod.SnapshotsPanel(self)
1605 def update_effective_time_labels(self, *args):
1606 tmin = self.state.tmin_effective
1607 tmax = self.state.tmax_effective
1609 stmin = common.time_or_none_to_str(tmin)
1610 stmax = common.time_or_none_to_str(tmax)
1612 self._label_effective_tmin.setText(stmin)
1613 self._label_effective_tmax.setText(stmax)
1615 def update_tcursor(self, *args):
1616 tcursor = self.gui_state.tcursor
1617 stcursor = common.time_or_none_to_str(tcursor)
1618 self._label_tcursor.setText(stcursor)
1620 def reset_strike_dip(self, *args):
1621 self.state.strike = 90.
1622 self.state.dip = 0
1623 self.gui_state.focal_point = 'center'
1625 def get_camera_geometry(self):
1627 def rtp2xyz(rtp):
1628 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1630 radius = 1.0 - self.state.depth / self.planet_radius
1632 cam_rtp = num.array([
1633 radius+self.state.distance,
1634 self.state.lat * d2r + 0.5*num.pi,
1635 self.state.lon * d2r])
1636 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1637 cam, up, foc = \
1638 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1640 foc_rtp = num.array([
1641 radius,
1642 self.state.lat * d2r + 0.5*num.pi,
1643 self.state.lon * d2r])
1645 foc = rtp2xyz(foc_rtp)
1647 rot_world = pmt.euler_to_matrix(
1648 -(self.state.lat-90.)*d2r,
1649 (self.state.lon+90.)*d2r,
1650 0.0*d2r).T
1652 rot_cam = pmt.euler_to_matrix(
1653 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1655 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1657 cam = foc + num.dot(rot, cam - foc)
1658 up = num.dot(rot, up)
1659 return cam, up, foc
1661 def update_camera(self, *args):
1662 cam, up, foc = self.get_camera_geometry()
1663 camera = self.ren.GetActiveCamera()
1664 camera.SetPosition(*cam)
1665 camera.SetFocalPoint(*foc)
1666 camera.SetViewUp(*up)
1668 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1670 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1671 self.feature_radius_min / self.planet_radius)**2))
1673 # if horizon == 0.0:
1674 # horizon = 2.0 + self.state.distance
1676 # clip_dist = max(min(self.state.distance*5., max(
1677 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1678 # , math.sqrt(num.sum(cam**2)))
1679 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1680 # clip_dist = feature_horizon
1682 camera.SetClippingRange(
1683 max(clip_dist*0.00001, clip_dist-3.0), clip_dist)
1685 self.camera_params = (
1686 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1688 self.update_view()
1690 def add_panel(
1691 self, title_label, panel,
1692 visible=False,
1693 # volatile=False,
1694 tabify=True,
1695 where=qc.Qt.RightDockWidgetArea,
1696 remove=None,
1697 title_controls=[],
1698 scrollable=True):
1700 dockwidget = common.MyDockWidget(
1701 self, title_label, title_controls=title_controls)
1703 if not visible:
1704 dockwidget.hide()
1706 if not self.gui_state.panels_visible:
1707 dockwidget.block()
1709 if scrollable:
1710 scrollarea = common.MyScrollArea()
1711 scrollarea.setWidget(panel)
1712 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
1713 scrollarea.setSizeAdjustPolicy(
1714 qw.QAbstractScrollArea.AdjustToContents)
1715 scrollarea.setFrameShape(qw.QFrame.NoFrame)
1717 dockwidget.setWidget(scrollarea)
1718 else:
1719 dockwidget.setWidget(panel)
1721 dockwidgets = self.findChildren(common.MyDockWidget)
1722 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1724 self.addDockWidget(where, dockwidget)
1726 nwrap = 4
1727 if dws and len(dws) >= nwrap and tabify:
1728 self.tabifyDockWidget(
1729 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1731 mitem = dockwidget.toggleViewAction()
1733 def update_label(*args):
1734 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1735 self.update_slug_abbreviated_lengths()
1737 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1738 dockwidget.titlebar._title_label.title_changed.connect(
1739 self.update_slug_abbreviated_lengths)
1741 update_label()
1743 self._panel_togglers[dockwidget] = mitem
1744 self.panels_menu.addAction(mitem)
1745 if visible:
1746 dockwidget.setVisible(True)
1747 dockwidget.setFocus()
1748 dockwidget.raise_()
1750 def stack_panels(self):
1751 dockwidgets = self.findChildren(common.MyDockWidget)
1752 by_area = defaultdict(list)
1753 for dw in dockwidgets:
1754 area = self.dockWidgetArea(dw)
1755 by_area[area].append(dw)
1757 for dockwidgets in by_area.values():
1758 dw_last = None
1759 for dw in dockwidgets:
1760 if dw_last is not None:
1761 self.tabifyDockWidget(dw_last, dw)
1763 dw_last = dw
1765 def update_slug_abbreviated_lengths(self):
1766 dockwidgets = self.findChildren(common.MyDockWidget)
1767 title_labels = []
1768 for dw in dockwidgets:
1769 title_labels.append(dw.titlebar._title_label)
1771 by_title = defaultdict(list)
1772 for tl in title_labels:
1773 by_title[tl.get_title()].append(tl)
1775 for group in by_title.values():
1776 slugs = [tl.get_slug() for tl in group]
1778 n = max(len(slug) for slug in slugs)
1779 nunique = len(set(slugs))
1781 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1782 n -= 1
1784 if n > 0:
1785 n = max(3, n)
1787 for tl in group:
1788 tl.set_slug_abbreviated_length(n)
1790 def get_dockwidget(self, panel):
1791 dockwidget = panel
1792 while not isinstance(dockwidget, qw.QDockWidget):
1793 dockwidget = dockwidget.parent()
1795 return dockwidget
1797 def raise_panel(self, panel):
1798 dockwidget = self.get_dockwidget(panel)
1799 dockwidget.setVisible(True)
1800 dockwidget.setFocus()
1801 dockwidget.raise_()
1803 def toggle_panel_visibility(self):
1804 self.gui_state.panels_visible = not self.gui_state.panels_visible
1806 def update_panel_visibility(self, *args):
1807 self.setUpdatesEnabled(False)
1808 mbar = self.menuBar()
1809 sbar = self.statusBar()
1810 dockwidgets = self.findChildren(common.MyDockWidget)
1812 # Set height to zero instead of hiding so that shortcuts still work
1813 # otherwise one would have to mess around with separate QShortcut
1814 # objects.
1815 mbar.setFixedHeight(
1816 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1818 sbar.setVisible(self.gui_state.panels_visible)
1819 for dockwidget in dockwidgets:
1820 dockwidget.setBlocked(not self.gui_state.panels_visible)
1822 self.setUpdatesEnabled(True)
1824 def remove_panel(self, panel):
1825 dockwidget = self.get_dockwidget(panel)
1826 self.removeDockWidget(dockwidget)
1827 dockwidget.setParent(None)
1828 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1830 def register_data_provider(self, provider):
1831 if provider not in self.data_providers:
1832 self.data_providers.append(provider)
1834 def unregister_data_provider(self, provider):
1835 if provider in self.data_providers:
1836 self.data_providers.remove(provider)
1838 def iter_data(self, name):
1839 for provider in self.data_providers:
1840 for data in provider.iter_data(name):
1841 yield data
1843 def confirm_close(self):
1844 ret = qw.QMessageBox.question(
1845 self,
1846 'Sparrow',
1847 'Close Sparrow window?',
1848 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1849 qw.QMessageBox.Ok)
1851 return ret == qw.QMessageBox.Ok
1853 def closeEvent(self, event):
1854 if self.instant_close or self.confirm_close():
1855 self.attach()
1856 self.closing = True
1857 event.accept()
1858 else:
1859 event.ignore()
1861 def is_closing(self):
1862 return self.closing
1865def main(*args, **kwargs):
1867 from pyrocko import util
1868 from pyrocko.gui import util as gui_util
1869 from . import common
1870 util.setup_logging('sparrow', 'info')
1872 global win
1874 app = gui_util.get_app()
1875 win = SparrowViewer(*args, **kwargs)
1876 app.set_main_window(win)
1878 gui_util.app.install_sigint_handler()
1880 try:
1881 gui_util.app.exec_()
1882 finally:
1883 gui_util.app.uninstall_sigint_handler()
1884 app.unset_main_window()
1885 common.set_viewer(None)
1886 del win
1887 gc.collect()