Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/gui/sparrow/main.py: 73%
1085 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +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
36import vtkmodules.qt
38vtkmodules.qt.PyQtImpl = 'PyQt5'
39vtk.qt.QVTKRWIBase = 'QGLWidget'
41from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
43from pyrocko import geometry # noqa
44from . import state as vstate, elements # noqa
46logger = logging.getLogger('pyrocko.gui.sparrow.main')
49d2r = num.pi/180.
50km = 1000.
52if platform.uname()[0] == 'Darwin':
53 g_modifier_key = '\u2318'
54else:
55 g_modifier_key = 'Ctrl'
58class ZeroFrame(qw.QFrame):
60 def sizeHint(self):
61 return qc.QSize(0, 0)
64class LocationChoice(object):
65 def __init__(self, name, lat, lon, depth=0):
66 self._name = name
67 self._lat = lat
68 self._lon = lon
69 self._depth = depth
71 def get_lat_lon_depth(self):
72 return self._lat, self._lon, self._depth
75def location_to_choices(s):
76 choices = []
77 s_vals = s.replace(',', ' ')
78 try:
79 vals = [float(x) for x in s_vals.split()]
80 if len(vals) == 3:
81 vals[2] *= km
83 choices.append(LocationChoice('', *vals))
85 except ValueError:
86 cities = geonames.get_cities_by_name(s.strip())
87 for c in cities:
88 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
90 return choices
93class NoLocationChoices(Exception):
95 def __init__(self, s):
96 self._string = s
98 def __str__(self):
99 return 'No location choices for string "%s"' % self._string
102class QVTKWidget(QVTKRenderWindowInteractor):
103 def __init__(self, viewer):
104 QVTKRenderWindowInteractor.__init__(self, parent=viewer)
105 self._viewer = viewer
106 self._ctrl_state = False
108 def wheelEvent(self, event):
109 return self._viewer.myWheelEvent(event)
111 def keyPressEvent(self, event):
112 if event.key() == qc.Qt.Key_Control:
113 self._update_ctrl_state(True)
114 QVTKRenderWindowInteractor.keyPressEvent(self, event)
116 def keyReleaseEvent(self, event):
117 if event.key() == qc.Qt.Key_Control:
118 self._update_ctrl_state(False)
119 QVTKRenderWindowInteractor.keyReleaseEvent(self, event)
121 def focusInEvent(self, event):
122 self._update_ctrl_state()
123 QVTKRenderWindowInteractor.focusInEvent(self, event)
125 def focusOutEvent(self, event):
126 self._update_ctrl_state(False)
127 QVTKRenderWindowInteractor.focusOutEvent(self, event)
129 def mousePressEvent(self, event):
130 self._viewer.disable_capture()
131 QVTKRenderWindowInteractor.mousePressEvent(self, event)
133 def mouseReleaseEvent(self, event):
134 self._viewer.enable_capture()
135 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event)
137 def _update_ctrl_state(self, state=None):
138 if state is None:
139 app = common.get_app()
140 if not app:
141 return
142 state = app.keyboardModifiers() == qc.Qt.ControlModifier
143 if self._ctrl_state != state:
144 self._viewer.gui_state.next_focal_point()
145 self._ctrl_state = state
147 def container_resized(self, ev):
148 self._viewer.update_vtk_widget_size()
151class DetachedViewer(qw.QMainWindow):
153 def __init__(self, main_window, vtk_frame):
154 qw.QMainWindow.__init__(self, main_window)
155 self.main_window = main_window
156 self.setWindowTitle('Sparrow View')
157 vtk_frame.setParent(self)
158 self.setCentralWidget(vtk_frame)
160 def closeEvent(self, ev):
161 ev.ignore()
162 self.main_window.attach()
165class CenteringScrollArea(qw.QScrollArea):
166 def __init__(self):
167 qw.QScrollArea.__init__(self)
168 self.setAlignment(qc.Qt.AlignCenter)
169 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
170 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
171 self.setFrameShape(qw.QFrame.NoFrame)
173 def resizeEvent(self, ev):
174 retval = qw.QScrollArea.resizeEvent(self, ev)
175 self.widget().container_resized(ev)
176 return retval
178 def recenter(self):
179 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
180 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
182 def wheelEvent(self, *args, **kwargs):
183 return self.widget().wheelEvent(*args, **kwargs)
186class YAMLEditor(qw.QTextEdit):
188 def __init__(self, parent):
189 qw.QTextEdit.__init__(self)
190 self._parent = parent
192 def event(self, ev):
193 if isinstance(ev, qg.QKeyEvent) \
194 and ev.key() == qc.Qt.Key_Return \
195 and ev.modifiers() & qc.Qt.ShiftModifier:
196 self._parent.state_changed()
197 return True
199 return qw.QTextEdit.event(self, ev)
202class StateEditor(qw.QFrame, TalkieConnectionOwner):
203 def __init__(self, viewer, *args, **kwargs):
204 qw.QFrame.__init__(self, *args, **kwargs)
205 TalkieConnectionOwner.__init__(self)
207 layout = qw.QGridLayout()
209 self.setLayout(layout)
211 self.source_editor = YAMLEditor(self)
212 self.source_editor.setAcceptRichText(False)
213 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
214 font = qg.QFont("Monospace")
215 self.source_editor.setCurrentFont(font)
216 layout.addWidget(self.source_editor, 0, 0, 1, 2)
218 self.error_display_label = qw.QLabel('Error')
219 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
221 self.error_display = qw.QTextEdit()
222 self.error_display.setCurrentFont(font)
223 self.error_display.setReadOnly(True)
225 self.error_display.setSizePolicy(
226 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
228 self.error_display_label.hide()
229 self.error_display.hide()
231 layout.addWidget(self.error_display, 2, 0, 1, 2)
233 self.instant_updates = qw.QCheckBox('Instant Updates')
234 self.instant_updates.toggled.connect(self.state_changed)
235 layout.addWidget(self.instant_updates, 3, 0)
237 button = qw.QPushButton('Apply')
238 button.clicked.connect(self.state_changed)
239 layout.addWidget(button, 3, 1)
241 self.viewer = viewer
242 # recommended way, but resulted in a variable-width font being used:
243 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
244 self.bind_state()
245 self.source_editor.textChanged.connect(self.text_changed_handler)
246 self.destroyed.connect(self.unbind_state)
247 self.bind_state()
249 def bind_state(self, *args):
250 self.talkie_connect(self.viewer.state, '', self.update_state)
251 self.update_state()
253 def unbind_state(self):
254 self.talkie_disconnect_all()
256 def update_state(self, *args):
257 cursor = self.source_editor.textCursor()
259 cursor_position = cursor.position()
260 vsb_position = self.source_editor.verticalScrollBar().value()
261 hsb_position = self.source_editor.horizontalScrollBar().value()
263 self.source_editor.setPlainText(str(self.viewer.state))
265 cursor.setPosition(cursor_position)
266 self.source_editor.setTextCursor(cursor)
267 self.source_editor.verticalScrollBar().setValue(vsb_position)
268 self.source_editor.horizontalScrollBar().setValue(hsb_position)
270 def text_changed_handler(self, *args):
271 if self.instant_updates.isChecked():
272 self.state_changed()
274 def state_changed(self):
275 try:
276 s = self.source_editor.toPlainText()
277 state = guts.load(string=s)
278 self.viewer.set_state(state)
279 self.error_display.setPlainText('')
280 self.error_display_label.hide()
281 self.error_display.hide()
283 except Exception as e:
284 self.error_display.show()
285 self.error_display_label.show()
286 self.error_display.setPlainText(str(e))
289class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
291 download_progress_update = qc.pyqtSignal()
293 def __init__(
294 self,
295 use_depth_peeling=True,
296 events=None,
297 snapshots=None,
298 instant_close=False):
300 common.set_viewer(self)
302 qw.QMainWindow.__init__(self)
303 TalkieConnectionOwner.__init__(self)
305 app = common.get_app()
306 app.set_main_window(self)
308 self.instant_close = instant_close
310 self.state = vstate.ViewerState()
311 self.gui_state = vstate.ViewerGuiState()
313 self.setWindowTitle('Sparrow')
315 self.setTabPosition(
316 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
318 self.planet_radius = cake.earthradius
319 self.feature_radius_min = cake.earthradius - 1000. * km
321 self._block_capture = 0
322 self._undo_stack = []
323 self._redo_stack = []
324 self._undo_aggregate = None
326 self._panel_togglers = {}
327 self._actors = set()
328 self._actors_2d = set()
329 self._render_window_size = (0, 0)
330 self._use_depth_peeling = use_depth_peeling
331 self._in_update_elements = False
332 self._update_elements_enabled = True
334 self._animation_tstart = None
335 self._animation_iframe = None
336 self._animation = None
338 mbar = qw.QMenuBar()
339 self.setMenuBar(mbar)
341 menu = mbar.addMenu('File')
343 menu.addAction(
344 'Export Image...',
345 self.export_image,
346 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
347 qc.Qt.ApplicationShortcut)
349 menu.addAction(
350 'Quit',
351 self.close,
352 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
353 qc.Qt.ApplicationShortcut)
355 menu = mbar.addMenu('Edit')
357 menu.addAction(
358 'Undo',
359 self.undo,
360 qg.QKeySequence(
361 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext(
362 qc.Qt.ApplicationShortcut)
364 menu.addAction(
365 'Redo',
366 self.redo,
367 qg.QKeySequence(
368 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext(
369 qc.Qt.ApplicationShortcut)
371 menu = mbar.addMenu('View')
372 menu_sizes = menu.addMenu('Size')
373 self._add_vtk_widget_size_menu_entries(menu_sizes)
375 # detached/attached
376 self.talkie_connect(
377 self.gui_state, 'detached', self.update_detached)
379 action = qw.QAction('Detach')
380 action.setCheckable(True)
381 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
382 action.setShortcutContext(qc.Qt.ApplicationShortcut)
384 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
385 menu.addAction(action)
387 # hide controls
388 action = qw.QAction('Hide Controls', self)
389 action.setCheckable(True)
390 action.setShortcut(qc.Qt.Key_Space)
391 action.setShortcutContext(qc.Qt.ApplicationShortcut)
392 action.triggered.connect(self.toggle_panel_visibility)
393 menu.addAction(action)
395 self.panels_menu = mbar.addMenu('Panels')
396 self.panels_menu.addAction(
397 'Stack Panels',
398 self.stack_panels)
399 self.panels_menu.addSeparator()
401 snapshots_menu = mbar.addMenu('Snapshots')
403 menu = mbar.addMenu('Elements')
404 for name, estate in sorted([
405 ('Icosphere', elements.IcosphereState(
406 level=4,
407 smooth=True,
408 opacity=0.5,
409 ambient=0.1)),
410 ('Grid', elements.GridState()),
411 ('Stations', elements.StationsState()),
412 ('Topography', elements.TopoState()),
413 ('Custom Topography', elements.CustomTopoState()),
414 ('Catalog', elements.CatalogState()),
415 ('Coastlines', elements.CoastlinesState()),
416 ('Rectangular Source', elements.SourceState()),
417 ('HUD Subtitle', elements.HudState(
418 template='Subtitle')),
419 ('HUD (tmax_effective)', elements.HudState(
420 template='tmax: {tmax_effective|date}',
421 position='top-left')),
422 ('AxesBox', elements.AxesBoxState()),
423 ('Volcanoes', elements.VolcanoesState()),
424 ('Faults', elements.ActiveFaultsState()),
425 ('Plate bounds', elements.PlatesBoundsState()),
426 ('InSAR Surface Displacements', elements.KiteState()),
427 ('Geometry', elements.GeometryState()),
428 ('Spheroid', elements.SpheroidState())]):
430 def wrap_add_element(estate):
431 def add_element(*args):
432 new_element = guts.clone(estate)
433 new_element.element_id = elements.random_id()
434 self.state.elements.append(new_element)
435 self.state.sort_elements()
437 return add_element
439 mitem = qw.QAction(name, self)
441 mitem.triggered.connect(wrap_add_element(estate))
443 menu.addAction(mitem)
445 menu = mbar.addMenu('Help')
447 menu.addAction(
448 'Interactive Tour',
449 self.start_tour)
451 menu.addAction(
452 'Online Manual',
453 self.open_manual)
455 self.data_providers = []
456 self.elements = {}
458 self.detached_window = None
460 self.main_frame = qw.QFrame()
461 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
463 self.vtk_frame = CenteringScrollArea()
465 self.vtk_widget = QVTKWidget(self)
466 self.vtk_frame.setWidget(self.vtk_widget)
468 self.main_layout = qw.QVBoxLayout()
469 self.main_layout.setContentsMargins(0, 0, 0, 0)
470 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
472 pb = Progressbars(self)
473 self.progressbars = pb
474 self.main_layout.addWidget(pb)
476 self.main_frame.setLayout(self.main_layout)
478 self.vtk_frame_substitute = None
480 self.add_panel(
481 'Navigation',
482 self.controls_navigation(),
483 visible=True,
484 scrollable=False,
485 where=qc.Qt.LeftDockWidgetArea)
487 self.add_panel(
488 'Time',
489 self.controls_time(),
490 visible=True,
491 scrollable=False,
492 where=qc.Qt.LeftDockWidgetArea)
494 self.add_panel(
495 'Appearance',
496 self.controls_appearance(),
497 visible=True,
498 scrollable=False,
499 where=qc.Qt.LeftDockWidgetArea)
501 snapshots_panel = self.controls_snapshots()
502 self.snapshots_panel = snapshots_panel
503 self.add_panel(
504 'Snapshots',
505 snapshots_panel,
506 visible=False,
507 scrollable=False,
508 where=qc.Qt.LeftDockWidgetArea)
510 snapshots_panel.setup_menu(snapshots_menu)
512 self.setCentralWidget(self.main_frame)
514 self.mesh = None
516 ren = vtk.vtkRenderer()
518 # ren.SetBackground(0.15, 0.15, 0.15)
519 # ren.SetBackground(0.0, 0.0, 0.0)
520 # ren.TwoSidedLightingOn()
521 # ren.SetUseShadows(1)
523 self._lighting = None
524 self._background = None
526 self.ren = ren
527 self.update_render_settings()
528 self.update_camera()
530 renwin = self.vtk_widget.GetRenderWindow()
532 if self._use_depth_peeling:
533 renwin.SetAlphaBitPlanes(1)
534 renwin.SetMultiSamples(0)
536 ren.SetUseDepthPeeling(1)
537 ren.SetMaximumNumberOfPeels(100)
538 ren.SetOcclusionRatio(0.1)
540 ren.SetUseFXAA(1)
541 # ren.SetUseHiddenLineRemoval(1)
542 # ren.SetBackingStore(1)
544 self.renwin = renwin
546 # renwin.LineSmoothingOn()
547 # renwin.PointSmoothingOn()
548 # renwin.PolygonSmoothingOn()
550 renwin.AddRenderer(ren)
552 iren = renwin.GetInteractor()
553 iren.LightFollowCameraOn()
554 iren.SetInteractorStyle(None)
556 iren.AddObserver('LeftButtonPressEvent', self.button_event)
557 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
558 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
559 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
560 iren.AddObserver('RightButtonPressEvent', self.button_event)
561 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
562 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
563 iren.AddObserver('KeyPressEvent', self.key_down_event)
564 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
566 renwin.Render()
568 iren.Initialize()
570 self.iren = iren
572 self.rotating = False
574 self._elements = {}
575 self._elements_active = {}
577 self.talkie_connect(
578 self.state, 'elements', self.update_elements)
580 self.state.elements.append(elements.IcosphereState(
581 element_id='icosphere',
582 level=4,
583 smooth=True,
584 opacity=0.5,
585 ambient=0.1))
587 self.state.elements.append(elements.GridState(
588 element_id='grid'))
589 self.state.elements.append(elements.CoastlinesState(
590 element_id='coastlines'))
591 self.state.elements.append(elements.CrosshairState(
592 element_id='crosshair'))
594 # self.state.elements.append(elements.StationsState())
595 # self.state.elements.append(elements.SourceState())
596 # self.state.elements.append(
597 # elements.CatalogState(
598 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
599 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
601 if events:
602 self.state.elements.append(
603 elements.CatalogState(
604 selection=elements.MemoryCatalogSelection(events=events)))
606 self.state.sort_elements()
608 if snapshots:
609 snapshots_ = []
610 for obj in snapshots:
611 if isinstance(obj, str):
612 snapshots_.extend(snapshots_mod.load_snapshots(obj))
613 else:
614 snapshots_.append(obj)
616 snapshots_panel.add_snapshots(snapshots_)
617 self.raise_panel(snapshots_panel)
618 snapshots_panel.goto_snapshot(1)
620 self.timer = qc.QTimer(self)
621 self.timer.timeout.connect(self.periodical)
622 self.timer.setInterval(1000)
623 self.timer.start()
625 self._animation_saver = None
627 self.closing = False
628 self.vtk_widget.setFocus()
630 self.update_detached()
632 self.status(
633 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
635 self.status(
636 'Let\'s fly.', 2.0)
638 self.show()
639 self.windowHandle().showMaximized()
641 self.talkie_connect(
642 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
644 self.update_vtk_widget_size()
646 hatch_path = config.expand(os.path.join(
647 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
649 self.talkie_connect(self.state, '', self.capture_state)
650 self.capture_state()
652 set_download_callback(self.update_download_progress)
654 if not os.path.exists(hatch_path):
655 with open(hatch_path, 'w') as f:
656 f.write('%s\n' % util.time_to_str(time.time()))
658 self.start_tour()
660 def update_download_progress(self, message, args):
661 self.download_progress_update.emit()
663 def status(self, message, duration=None):
664 self.statusBar().showMessage(
665 message, int((duration or 0) * 1000))
667 def disable_capture(self):
668 self._block_capture += 1
670 logger.debug('Undo capture block (+1): %i' % self._block_capture)
672 def enable_capture(self, drop=False, aggregate=None):
673 if self._block_capture > 0:
674 self._block_capture -= 1
676 logger.debug('Undo capture block (-1): %i' % self._block_capture)
678 if self._block_capture == 0 and not drop:
679 self.capture_state(aggregate=aggregate)
681 def capture_state(self, *args, aggregate=None):
682 if self._block_capture:
683 return
685 if len(self._undo_stack) == 0 or not state_equal(
686 self.state, self._undo_stack[-1]):
688 if aggregate is not None:
689 if aggregate == self._undo_aggregate:
690 self._undo_stack.pop()
692 self._undo_aggregate = aggregate
693 else:
694 self._undo_aggregate = None
696 logger.debug('Capture undo state (%i%s)\n%s' % (
697 len(self._undo_stack) + 1,
698 '' if aggregate is None else ', aggregate=%s' % aggregate,
699 '\n'.join(
700 ' - %s' % s
701 for s in self._undo_stack[-1].str_diff(
702 self.state).splitlines())
703 if len(self._undo_stack) > 0 else 'initial'))
705 self._undo_stack.append(guts.clone(self.state))
706 self._redo_stack.clear()
708 def undo(self):
709 self._undo_aggregate = None
711 if len(self._undo_stack) <= 1:
712 return
714 state = self._undo_stack.pop()
715 self._redo_stack.append(state)
716 state = self._undo_stack[-1]
718 logger.debug('Undo (%i)\n%s' % (
719 len(self._undo_stack),
720 '\n'.join(
721 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
723 self.disable_capture()
724 try:
725 self.set_state(state)
726 finally:
727 self.enable_capture(drop=True)
729 def redo(self):
730 self._undo_aggregate = None
732 if len(self._redo_stack) == 0:
733 return
735 state = self._redo_stack.pop()
736 self._undo_stack.append(state)
738 logger.debug('Redo (%i)\n%s' % (
739 len(self._redo_stack),
740 '\n'.join(
741 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
743 self.disable_capture()
744 try:
745 self.set_state(state)
746 finally:
747 self.enable_capture(drop=True)
749 def start_tour(self):
750 snapshots_ = snapshots_mod.load_snapshots(
751 'https://data.pyrocko.org/examples/'
752 'sparrow-tour-v0.1.snapshots.yaml')
753 self.snapshots_panel.add_snapshots(snapshots_)
754 self.raise_panel(self.snapshots_panel)
755 self.snapshots_panel.transition_to_next_snapshot()
757 def open_manual(self):
758 import webbrowser
759 webbrowser.open(
760 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
762 def _add_vtk_widget_size_menu_entries(self, menu):
764 group = qw.QActionGroup(menu)
765 group.setExclusive(True)
767 def set_variable_size():
768 self.gui_state.fixed_size = False
770 variable_size_action = menu.addAction('Fit Window Size')
771 variable_size_action.setCheckable(True)
772 variable_size_action.setActionGroup(group)
773 variable_size_action.triggered.connect(set_variable_size)
775 fixed_size_items = []
776 for nx, ny, label in [
777 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
778 (426, 240, ''),
779 (640, 360, ''),
780 (854, 480, '(FWVGA)'),
781 (1280, 720, '(HD)'),
782 (1920, 1080, '(Full HD)'),
783 (2560, 1440, '(Quad HD)'),
784 (3840, 2160, '(4K UHD)'),
785 (3840*2, 2160*2, '',),
786 (None, None, 'Aspect 4:3'),
787 (640, 480, '(VGA)'),
788 (800, 600, '(SVGA)'),
789 (None, None, 'Other'),
790 (512, 512, ''),
791 (1024, 1024, '')]:
793 if None in (nx, ny):
794 menu.addSection(label)
795 else:
796 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
797 action = menu.addAction(name)
798 action.setCheckable(True)
799 action.setActionGroup(group)
800 fixed_size_items.append((action, (nx, ny)))
802 def make_set_fixed_size(nx, ny):
803 def set_fixed_size():
804 self.gui_state.fixed_size = (float(nx), float(ny))
806 return set_fixed_size
808 action.triggered.connect(make_set_fixed_size(nx, ny))
810 def update_widget(*args):
811 for action, (nx, ny) in fixed_size_items:
812 action.blockSignals(True)
813 action.setChecked(
814 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
815 int(z) for z in self.gui_state.fixed_size)))
816 action.blockSignals(False)
818 variable_size_action.blockSignals(True)
819 variable_size_action.setChecked(not self.gui_state.fixed_size)
820 variable_size_action.blockSignals(False)
822 update_widget()
823 self.talkie_connect(
824 self.gui_state, 'fixed_size', update_widget)
826 def update_vtk_widget_size(self, *args):
827 if self.gui_state.fixed_size:
828 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
829 wanted_size = qc.QSize(nx, ny)
830 else:
831 wanted_size = qc.QSize(
832 self.vtk_frame.window().width(), self.vtk_frame.height())
834 current_size = self.vtk_widget.size()
836 if current_size.width() != wanted_size.width() \
837 or current_size.height() != wanted_size.height():
839 self.vtk_widget.setFixedSize(wanted_size)
841 self.vtk_frame.recenter()
842 self.check_vtk_resize()
844 def update_focal_point(self, *args):
845 if self.gui_state.focal_point == 'center':
846 self.vtk_widget.setStatusTip(
847 'Click and drag: change location. %s-click and drag: '
848 'change view plane orientation.' % g_modifier_key)
849 else:
850 self.vtk_widget.setStatusTip(
851 '%s-click and drag: change location. Click and drag: '
852 'change view plane orientation. Uncheck "Navigation: Fix" to '
853 'reverse sense.' % g_modifier_key)
855 def update_detached(self, *args):
857 if self.gui_state.detached and not self.detached_window: # detach
858 logger.debug('Detaching VTK view.')
860 self.main_layout.removeWidget(self.vtk_frame)
861 self.detached_window = DetachedViewer(self, self.vtk_frame)
862 self.detached_window.show()
863 self.vtk_widget.setFocus()
865 screens = common.get_app().screens()
866 if len(screens) > 1:
867 for screen in screens:
868 if screen is not self.screen():
869 self.detached_window.windowHandle().setScreen(screen)
870 # .setScreen() does not work reliably,
871 # therefore trying also with .move()...
872 p = screen.geometry().topLeft()
873 self.detached_window.move(p.x() + 50, p.y() + 50)
874 # ... but also does not work in notion window manager.
876 self.detached_window.windowHandle().showMaximized()
878 frame = qw.QFrame()
879 # frame.setFrameShape(qw.QFrame.NoFrame)
880 # frame.setBackgroundRole(qg.QPalette.Mid)
881 # frame.setAutoFillBackground(True)
882 frame.setSizePolicy(
883 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
885 layout = qw.QGridLayout()
886 frame.setLayout(layout)
887 self.main_layout.insertWidget(0, frame)
889 self.state_editor = StateEditor(self)
891 layout.addWidget(self.state_editor, 0, 0)
893 # attach_button = qw.QPushButton('Attach View')
894 # attach_button.clicked.connect(self.attach)
895 # layout.addWidget(
896 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
898 self.vtk_frame_substitute = frame
900 if not self.gui_state.detached and self.detached_window: # attach
901 logger.debug('Attaching VTK view.')
902 self.detached_window.hide()
903 self.vtk_frame.setParent(self)
904 if self.vtk_frame_substitute:
905 self.main_layout.removeWidget(self.vtk_frame_substitute)
906 self.state_editor.unbind_state()
907 self.vtk_frame_substitute = None
909 self.main_layout.insertWidget(0, self.vtk_frame)
910 self.detached_window = None
911 self.vtk_widget.setFocus()
913 def attach(self):
914 self.gui_state.detached = False
916 def export_image(self):
918 caption = 'Export Image'
919 fn_out, _ = qw.QFileDialog.getSaveFileName(
920 self, caption, 'image.png',
921 options=common.qfiledialog_options)
923 if fn_out:
924 self.save_image(fn_out)
926 def save_image(self, path):
928 original_fixed_size = self.gui_state.fixed_size
929 if original_fixed_size is None:
930 self.gui_state.fixed_size = (1920., 1080.)
932 wif = vtk.vtkWindowToImageFilter()
933 wif.SetInput(self.renwin)
934 wif.SetInputBufferTypeToRGBA()
935 wif.SetScale(1, 1)
936 wif.ReadFrontBufferOff()
937 writer = vtk.vtkPNGWriter()
938 writer.SetInputConnection(wif.GetOutputPort())
940 self.renwin.Render()
941 wif.Modified()
942 writer.SetFileName(path)
943 writer.Write()
945 self.gui_state.fixed_size = original_fixed_size
947 def update_render_settings(self, *args):
948 if self._lighting is None or self._lighting != self.state.lighting:
949 self.ren.RemoveAllLights()
950 for li in light.get_lights(self.state.lighting):
951 self.ren.AddLight(li)
953 self._lighting = self.state.lighting
955 if self._background is None \
956 or self._background != self.state.background:
958 self.state.background.vtk_apply(self.ren)
959 self._background = self.state.background
961 self.update_view()
963 def start_animation(self, interpolator, output_path=None):
964 if self._animation:
965 logger.debug('Aborting animation in progress to start a new one.')
966 self.stop_animation()
968 self.disable_capture()
969 self._animation = interpolator
970 if output_path is None:
971 self._animation_tstart = time.time()
972 self._animation_iframe = None
973 else:
974 self._animation_iframe = 0
975 mess = 'Rendering movie'
976 self.progressbars.set_status(mess, 0, can_abort=True)
978 self._animation_timer = qc.QTimer(self)
979 self._animation_timer.timeout.connect(self.next_animation_frame)
980 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
981 self._animation_timer.start()
982 if output_path is not None:
983 original_fixed_size = self.gui_state.fixed_size
984 if original_fixed_size is None:
985 self.gui_state.fixed_size = (1920., 1080.)
987 wif = vtk.vtkWindowToImageFilter()
988 wif.SetInput(self.renwin)
989 wif.SetInputBufferTypeToRGBA()
990 wif.SetScale(1, 1)
991 wif.ReadFrontBufferOff()
992 writer = vtk.vtkPNGWriter()
993 temp_path = tempfile.mkdtemp()
994 self._animation_saver = (
995 wif, writer, temp_path, output_path, original_fixed_size)
996 writer.SetInputConnection(wif.GetOutputPort())
998 def next_animation_frame(self):
1000 ani = self._animation
1001 if not ani:
1002 return
1004 if self._animation_iframe is not None:
1005 state = ani(
1006 ani.tmin
1007 + self._animation_iframe * ani.dt)
1009 self._animation_iframe += 1
1010 else:
1011 tnow = time.time()
1012 state = ani(min(
1013 ani.tmax,
1014 ani.tmin + (tnow - self._animation_tstart)))
1016 self.set_state(state)
1017 self.renwin.Render()
1018 abort = False
1019 if self._animation_saver:
1020 abort = self.progressbars.set_status(
1021 'Rendering movie',
1022 100*self._animation_iframe*ani.dt / (ani.tmax - ani.tmin),
1023 can_abort=True)
1025 wif, writer, temp_path, _, _ = self._animation_saver
1026 wif.Modified()
1027 fn = os.path.join(temp_path, 'f%09i.png')
1028 writer.SetFileName(fn % self._animation_iframe)
1029 writer.Write()
1031 if self._animation_iframe is not None:
1032 t = self._animation_iframe * ani.dt
1033 else:
1034 t = tnow - self._animation_tstart
1036 if t > ani.tmax - ani.tmin or abort:
1037 self.stop_animation()
1039 def stop_animation(self):
1040 if self._animation_timer:
1041 self._animation_timer.stop()
1043 if self._animation_saver:
1045 wif, writer, temp_path, output_path, original_fixed_size \
1046 = self._animation_saver
1047 self.gui_state.fixed_size = original_fixed_size
1049 fn_path = os.path.join(temp_path, 'f%09d.png')
1050 check_call([
1051 'ffmpeg', '-y',
1052 '-i', fn_path,
1053 '-c:v', 'libx264',
1054 '-preset', 'slow',
1055 '-crf', '17',
1056 '-vf', 'format=yuv420p,fps=%i' % (
1057 int(round(1.0/self._animation.dt))),
1058 output_path])
1059 shutil.rmtree(temp_path)
1061 self._animation_saver = None
1062 self._animation_saver
1064 self.progressbars.set_status(
1065 'Rendering movie', 100, can_abort=True)
1067 self._animation_tstart = None
1068 self._animation_iframe = None
1069 self._animation = None
1070 self.enable_capture()
1072 def set_state(self, state):
1073 self.disable_capture()
1074 try:
1075 self._update_elements_enabled = False
1076 self.setUpdatesEnabled(False)
1077 self.state.diff_update(state)
1078 self.state.sort_elements()
1079 self.setUpdatesEnabled(True)
1080 self._update_elements_enabled = True
1081 self.update_elements()
1082 finally:
1083 self.enable_capture()
1085 def periodical(self):
1086 pass
1088 def check_vtk_resize(self, *args):
1089 render_window_size = self.renwin.GetSize()
1090 if self._render_window_size != render_window_size:
1091 self._render_window_size = render_window_size
1092 self.resize_event(*render_window_size)
1094 def update_elements(self, *_):
1095 if not self._update_elements_enabled:
1096 return
1098 if self._in_update_elements:
1099 return
1101 self._in_update_elements = True
1102 for estate in self.state.elements:
1103 if estate.element_id not in self._elements:
1104 new_element = estate.create()
1105 logger.debug('Creating "%s" ("%s").' % (
1106 type(new_element).__name__,
1107 estate.element_id))
1108 self._elements[estate.element_id] = new_element
1110 element = self._elements[estate.element_id]
1112 if estate.element_id not in self._elements_active:
1113 logger.debug('Adding "%s" ("%s")' % (
1114 type(element).__name__,
1115 estate.element_id))
1116 element.bind_state(estate)
1117 element.set_parent(self)
1118 self._elements_active[estate.element_id] = element
1120 state_element_ids = [el.element_id for el in self.state.elements]
1121 deactivate = []
1122 for element_id, element in self._elements_active.items():
1123 if element_id not in state_element_ids:
1124 logger.debug('Removing "%s" ("%s").' % (
1125 type(element).__name__,
1126 element_id))
1127 element.unset_parent()
1128 deactivate.append(element_id)
1130 for element_id in deactivate:
1131 del self._elements_active[element_id]
1133 self._update_crosshair_bindings()
1135 self._in_update_elements = False
1137 def _update_crosshair_bindings(self):
1139 def get_crosshair_element():
1140 for element in self.state.elements:
1141 if element.element_id == 'crosshair':
1142 return element
1144 return None
1146 crosshair = get_crosshair_element()
1147 if crosshair is None or crosshair.is_connected:
1148 return
1150 def to_checkbox(state, widget):
1151 widget.blockSignals(True)
1152 widget.setChecked(state.visible)
1153 widget.blockSignals(False)
1155 def to_state(widget, state):
1156 state.visible = widget.isChecked()
1158 cb = self._crosshair_checkbox
1159 vstate.state_bind(
1160 self, crosshair, ['visible'], to_state,
1161 cb, [cb.toggled], to_checkbox)
1163 crosshair.is_connected = True
1165 def add_actor_2d(self, actor):
1166 if actor not in self._actors_2d:
1167 self.ren.AddActor2D(actor)
1168 self._actors_2d.add(actor)
1170 def remove_actor_2d(self, actor):
1171 if actor in self._actors_2d:
1172 self.ren.RemoveActor2D(actor)
1173 self._actors_2d.remove(actor)
1175 def add_actor(self, actor):
1176 if actor not in self._actors:
1177 self.ren.AddActor(actor)
1178 self._actors.add(actor)
1180 def add_actor_list(self, actorlist):
1181 for actor in actorlist:
1182 self.add_actor(actor)
1184 def remove_actor(self, actor):
1185 if actor in self._actors:
1186 self.ren.RemoveActor(actor)
1187 self._actors.remove(actor)
1189 def update_view(self):
1190 self.vtk_widget.update()
1192 def resize_event(self, size_x, size_y):
1193 self.gui_state.size = (size_x, size_y)
1195 def button_event(self, obj, event):
1196 if event == "LeftButtonPressEvent":
1197 self.rotating = True
1198 elif event == "LeftButtonReleaseEvent":
1199 self.rotating = False
1201 def mouse_move_event(self, obj, event):
1202 x0, y0 = self.iren.GetLastEventPosition()
1203 x, y = self.iren.GetEventPosition()
1205 size_x, size_y = self.renwin.GetSize()
1206 center_x = size_x / 2.0
1207 center_y = size_y / 2.0
1209 if self.rotating:
1210 self.do_rotate(x, y, x0, y0, center_x, center_y)
1212 def myWheelEvent(self, event):
1214 angle = event.angleDelta().y()
1216 if angle > 200:
1217 angle = 200
1219 if angle < -200:
1220 angle = -200
1222 self.disable_capture()
1223 try:
1224 self.do_dolly(-angle/100.)
1225 finally:
1226 self.enable_capture(aggregate='distance')
1228 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1230 dx = x0 - x
1231 dy = y0 - y
1233 phi = d2r*(self.state.strike - 90.)
1234 focp = self.gui_state.focal_point
1236 if focp == 'center':
1237 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1238 - math.sin(phi) * dx + math.cos(phi) * dy
1240 lat = self.state.lat
1241 lon = self.state.lon
1242 factor = self.state.distance / 10.0
1243 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1244 else:
1245 lat = 90. - self.state.dip
1246 lon = -self.state.strike - 90.
1247 factor = 0.5
1248 factor_lat = 1.0
1250 dlat = dy * factor
1251 dlon = dx * factor * factor_lat
1253 lat = max(min(lat + dlat, 90.), -90.)
1254 lon += dlon
1255 lon = (lon + 180.) % 360. - 180.
1257 if focp == 'center':
1258 self.state.lat = float(lat)
1259 self.state.lon = float(lon)
1260 else:
1261 self.state.dip = float(90. - lat)
1262 self.state.strike = float(((-(lon + 90.))+180.) % 360. - 180.)
1264 def do_dolly(self, v):
1265 self.state.distance *= float(1.0 + 0.1*v)
1267 def key_down_event(self, obj, event):
1268 k = obj.GetKeyCode()
1269 if k == 'f':
1270 self.gui_state.next_focal_point()
1272 elif k == 'r':
1273 self.reset_strike_dip()
1275 elif k == 'p':
1276 print(self.state)
1278 elif k == 'i':
1279 for elem in self.state.elements:
1280 if isinstance(elem, elements.IcosphereState):
1281 elem.visible = not elem.visible
1283 elif k == 'c':
1284 for elem in self.state.elements:
1285 if isinstance(elem, elements.CoastlinesState):
1286 elem.visible = not elem.visible
1288 elif k == 't':
1289 if not any(
1290 isinstance(elem, elements.TopoState)
1291 for elem in self.state.elements):
1293 self.state.elements.append(elements.TopoState())
1294 else:
1295 for elem in self.state.elements:
1296 if isinstance(elem, elements.TopoState):
1297 elem.visible = not elem.visible
1299 # elif k == ' ':
1300 # self.toggle_panel_visibility()
1302 def _state_bind(self, *args, **kwargs):
1303 vstate.state_bind(self, self.state, *args, **kwargs)
1305 def _gui_state_bind(self, *args, **kwargs):
1306 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1308 def controls_navigation(self):
1309 frame = qw.QFrame(self)
1310 frame.setSizePolicy(
1311 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1312 layout = qw.QGridLayout()
1313 frame.setLayout(layout)
1315 # lat, lon, depth
1317 layout.addWidget(
1318 qw.QLabel('Location'), 0, 0, 1, 2)
1320 le = qw.QLineEdit()
1321 le.setStatusTip(
1322 'Latitude, Longitude, Depth [km] or city name: '
1323 'Focal point location.')
1324 layout.addWidget(le, 1, 0, 1, 1)
1326 def lat_lon_depth_to_lineedit(state, widget):
1327 widget.setText('%g, %g, %g' % (
1328 state.lat, state.lon, state.depth / km))
1330 def lineedit_to_lat_lon_depth(widget, state):
1331 self.disable_capture()
1332 try:
1333 s = str(widget.text())
1334 choices = location_to_choices(s)
1335 if len(choices) > 0:
1336 self.state.lat, self.state.lon, self.state.depth = \
1337 choices[0].get_lat_lon_depth()
1338 else:
1339 raise NoLocationChoices(s)
1341 finally:
1342 self.enable_capture()
1344 self._state_bind(
1345 ['lat', 'lon', 'depth'],
1346 lineedit_to_lat_lon_depth,
1347 le, [le.editingFinished, le.returnPressed],
1348 lat_lon_depth_to_lineedit)
1350 self.lat_lon_lineedit = le
1352 # focal point
1354 cb = qw.QCheckBox('Fix')
1355 cb.setStatusTip(
1356 'Fix location. Orbit focal point without pressing %s.'
1357 % g_modifier_key)
1358 layout.addWidget(cb, 1, 1, 1, 1)
1360 def focal_point_to_checkbox(state, widget):
1361 widget.blockSignals(True)
1362 widget.setChecked(self.gui_state.focal_point != 'center')
1363 widget.blockSignals(False)
1365 def checkbox_to_focal_point(widget, state):
1366 self.gui_state.focal_point = \
1367 'target' if widget.isChecked() else 'center'
1369 self._gui_state_bind(
1370 ['focal_point'], checkbox_to_focal_point,
1371 cb, [cb.toggled], focal_point_to_checkbox)
1373 self.focal_point_checkbox = cb
1375 self.talkie_connect(
1376 self.gui_state, 'focal_point', self.update_focal_point)
1378 self.update_focal_point()
1380 # strike, dip
1382 layout.addWidget(
1383 qw.QLabel('View Plane'), 2, 0, 1, 2)
1385 le = qw.QLineEdit()
1386 le.setStatusTip(
1387 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1388 'direction.')
1389 layout.addWidget(le, 3, 0, 1, 1)
1391 def strike_dip_to_lineedit(state, widget):
1392 widget.setText('%g, %g' % (state.strike, state.dip))
1394 def lineedit_to_strike_dip(widget, state):
1395 s = str(widget.text())
1396 string_to_strike_dip = {
1397 'east': (0., 90.),
1398 'west': (180., 90.),
1399 'south': (90., 90.),
1400 'north': (270., 90.),
1401 'top': (90., 0.),
1402 'bottom': (90., 180.)}
1404 self.disable_capture()
1405 if s in string_to_strike_dip:
1406 state.strike, state.dip = string_to_strike_dip[s]
1408 s = s.replace(',', ' ')
1409 try:
1410 state.strike, state.dip = map(float, s.split())
1411 except Exception:
1412 raise ValueError('need two numerical values: <strike>, <dip>')
1413 finally:
1414 self.enable_capture()
1416 self._state_bind(
1417 ['strike', 'dip'], lineedit_to_strike_dip,
1418 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1420 self.strike_dip_lineedit = le
1422 but = qw.QPushButton('Reset')
1423 but.setStatusTip('Reset to north-up map view.')
1424 but.clicked.connect(self.reset_strike_dip)
1425 layout.addWidget(but, 3, 1, 1, 1)
1427 # crosshair
1429 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1430 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1432 # camera bindings
1433 self.talkie_connect(
1434 self.state,
1435 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1436 self.update_camera)
1438 self.talkie_connect(
1439 self.gui_state, 'panels_visible', self.update_panel_visibility)
1441 return frame
1443 def controls_time(self):
1444 frame = qw.QFrame(self)
1445 frame.setSizePolicy(
1446 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1448 layout = qw.QGridLayout()
1449 frame.setLayout(layout)
1451 layout.addWidget(qw.QLabel('Min'), 0, 0)
1452 le_tmin = qw.QLineEdit()
1453 layout.addWidget(le_tmin, 0, 1)
1455 layout.addWidget(qw.QLabel('Max'), 1, 0)
1456 le_tmax = qw.QLineEdit()
1457 layout.addWidget(le_tmax, 1, 1)
1459 label_tcursor = qw.QLabel()
1461 label_tcursor.setSizePolicy(
1462 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1464 layout.addWidget(label_tcursor, 2, 1)
1465 self._label_tcursor = label_tcursor
1467 self._state_bind(
1468 ['tmin'], common.lineedit_to_time, le_tmin,
1469 [le_tmin.editingFinished, le_tmin.returnPressed],
1470 common.time_to_lineedit,
1471 attribute='tmin')
1472 self._state_bind(
1473 ['tmax'], common.lineedit_to_time, le_tmax,
1474 [le_tmax.editingFinished, le_tmax.returnPressed],
1475 common.time_to_lineedit,
1476 attribute='tmax')
1478 self.tmin_lineedit = le_tmin
1479 self.tmax_lineedit = le_tmax
1481 range_edit = RangeEdit()
1482 range_edit.rangeEditPressed.connect(self.disable_capture)
1483 range_edit.rangeEditReleased.connect(self.enable_capture)
1484 range_edit.set_data_provider(self)
1485 range_edit.set_data_name('time')
1487 xblock = [False]
1489 def range_to_range_edit(state, widget):
1490 if not xblock[0]:
1491 widget.blockSignals(True)
1492 widget.set_focus(state.tduration, state.tposition)
1493 widget.set_range(state.tmin, state.tmax)
1494 widget.blockSignals(False)
1496 def range_edit_to_range(widget, state):
1497 xblock[0] = True
1498 self.state.tduration, self.state.tposition = widget.get_focus()
1499 self.state.tmin, self.state.tmax = widget.get_range()
1500 xblock[0] = False
1502 self._state_bind(
1503 ['tmin', 'tmax', 'tduration', 'tposition'],
1504 range_edit_to_range,
1505 range_edit,
1506 [range_edit.rangeChanged, range_edit.focusChanged],
1507 range_to_range_edit)
1509 def handle_tcursor_changed():
1510 self.gui_state.tcursor = range_edit.get_tcursor()
1512 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1514 layout.addWidget(range_edit, 3, 0, 1, 2)
1516 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1517 le_focus = qw.QLineEdit()
1518 layout.addWidget(le_focus, 4, 1)
1520 def focus_to_lineedit(state, widget):
1521 if state.tduration is None:
1522 widget.setText('')
1523 else:
1524 widget.setText('%s, %g' % (
1525 guts.str_duration(state.tduration),
1526 state.tposition))
1528 def lineedit_to_focus(widget, state):
1529 s = str(widget.text())
1530 w = [x.strip() for x in s.split(',')]
1531 try:
1532 if len(w) == 0 or not w[0]:
1533 state.tduration = None
1534 state.tposition = 0.0
1535 else:
1536 state.tduration = guts.parse_duration(w[0])
1537 if len(w) > 1:
1538 state.tposition = float(w[1])
1539 else:
1540 state.tposition = 0.0
1542 except Exception:
1543 raise ValueError('need two values: <duration>, <position>')
1545 self._state_bind(
1546 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1547 [le_focus.editingFinished, le_focus.returnPressed],
1548 focus_to_lineedit)
1550 label_effective_tmin = qw.QLabel()
1551 label_effective_tmax = qw.QLabel()
1553 label_effective_tmin.setSizePolicy(
1554 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1555 label_effective_tmax.setSizePolicy(
1556 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1557 label_effective_tmin.setMinimumSize(
1558 qg.QFontMetrics(label_effective_tmin.font()).width(
1559 '0000-00-00 00:00:00.000 '), 0)
1561 layout.addWidget(label_effective_tmin, 5, 1)
1562 layout.addWidget(label_effective_tmax, 6, 1)
1564 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1565 self.talkie_connect(
1566 self.state, var, self.update_effective_time_labels)
1568 self._label_effective_tmin = label_effective_tmin
1569 self._label_effective_tmax = label_effective_tmax
1571 self.talkie_connect(
1572 self.gui_state, 'tcursor', self.update_tcursor)
1574 return frame
1576 def controls_appearance(self):
1577 frame = qw.QFrame(self)
1578 frame.setSizePolicy(
1579 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1580 layout = qw.QGridLayout()
1581 frame.setLayout(layout)
1583 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1585 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1586 layout.addWidget(cb, 0, 1)
1587 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1589 self.talkie_connect(
1590 self.state, 'lighting', self.update_render_settings)
1592 # background
1594 layout.addWidget(qw.QLabel('Background'), 1, 0)
1596 cb = common.strings_to_combobox(
1597 ['black', 'white', 'skyblue1 - white'])
1599 layout.addWidget(cb, 1, 1)
1600 vstate.state_bind_combobox_background(
1601 self, self.state, 'background', cb)
1603 self.talkie_connect(
1604 self.state, 'background', self.update_render_settings)
1606 return frame
1608 def controls_snapshots(self):
1609 return snapshots_mod.SnapshotsPanel(self)
1611 def update_effective_time_labels(self, *args):
1612 tmin = self.state.tmin_effective
1613 tmax = self.state.tmax_effective
1615 stmin = common.time_or_none_to_str(tmin)
1616 stmax = common.time_or_none_to_str(tmax)
1618 self._label_effective_tmin.setText(stmin)
1619 self._label_effective_tmax.setText(stmax)
1621 def update_tcursor(self, *args):
1622 tcursor = self.gui_state.tcursor
1623 stcursor = common.time_or_none_to_str(tcursor)
1624 self._label_tcursor.setText(stcursor)
1626 def reset_strike_dip(self, *args):
1627 self.state.strike = 90.
1628 self.state.dip = 0
1629 self.gui_state.focal_point = 'center'
1631 def get_camera_geometry(self):
1633 def rtp2xyz(rtp):
1634 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1636 radius = 1.0 - self.state.depth / self.planet_radius
1638 cam_rtp = num.array([
1639 radius+self.state.distance,
1640 self.state.lat * d2r + 0.5*num.pi,
1641 self.state.lon * d2r])
1642 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1643 cam, up, foc = \
1644 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1646 foc_rtp = num.array([
1647 radius,
1648 self.state.lat * d2r + 0.5*num.pi,
1649 self.state.lon * d2r])
1651 foc = rtp2xyz(foc_rtp)
1653 rot_world = pmt.euler_to_matrix(
1654 -(self.state.lat-90.)*d2r,
1655 (self.state.lon+90.)*d2r,
1656 0.0*d2r).T
1658 rot_cam = pmt.euler_to_matrix(
1659 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1661 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1663 cam = foc + num.dot(rot, cam - foc)
1664 up = num.dot(rot, up)
1665 return cam, up, foc
1667 def update_camera(self, *args):
1668 cam, up, foc = self.get_camera_geometry()
1669 camera = self.ren.GetActiveCamera()
1670 camera.SetPosition(*cam)
1671 camera.SetFocalPoint(*foc)
1672 camera.SetViewUp(*up)
1674 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1676 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1677 self.feature_radius_min / self.planet_radius)**2))
1679 # if horizon == 0.0:
1680 # horizon = 2.0 + self.state.distance
1682 # clip_dist = max(min(self.state.distance*5., max(
1683 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1684 # , math.sqrt(num.sum(cam**2)))
1685 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1686 # clip_dist = feature_horizon
1688 camera.SetClippingRange(
1689 max(clip_dist*0.00001, clip_dist-3.0), clip_dist)
1691 self.camera_params = (
1692 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1694 self.update_view()
1696 def add_panel(
1697 self, title_label, panel,
1698 visible=False,
1699 # volatile=False,
1700 tabify=True,
1701 where=qc.Qt.RightDockWidgetArea,
1702 remove=None,
1703 title_controls=[],
1704 scrollable=True):
1706 dockwidget = common.MyDockWidget(
1707 self, title_label, title_controls=title_controls)
1709 if not visible:
1710 dockwidget.hide()
1712 if not self.gui_state.panels_visible:
1713 dockwidget.block()
1715 if scrollable:
1716 scrollarea = common.MyScrollArea()
1717 scrollarea.setWidget(panel)
1718 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
1719 scrollarea.setSizeAdjustPolicy(
1720 qw.QAbstractScrollArea.AdjustToContents)
1721 scrollarea.setFrameShape(qw.QFrame.NoFrame)
1723 dockwidget.setWidget(scrollarea)
1724 else:
1725 dockwidget.setWidget(panel)
1727 dockwidgets = self.findChildren(common.MyDockWidget)
1728 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1730 self.addDockWidget(where, dockwidget)
1732 nwrap = 4
1733 if dws and len(dws) >= nwrap and tabify:
1734 self.tabifyDockWidget(
1735 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1737 mitem = dockwidget.toggleViewAction()
1739 def update_label(*args):
1740 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1741 self.update_slug_abbreviated_lengths()
1743 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1744 dockwidget.titlebar._title_label.title_changed.connect(
1745 self.update_slug_abbreviated_lengths)
1747 update_label()
1749 self._panel_togglers[dockwidget] = mitem
1750 self.panels_menu.addAction(mitem)
1751 if visible:
1752 dockwidget.setVisible(True)
1753 dockwidget.setFocus()
1754 dockwidget.raise_()
1756 def stack_panels(self):
1757 dockwidgets = self.findChildren(common.MyDockWidget)
1758 by_area = defaultdict(list)
1759 for dw in dockwidgets:
1760 area = self.dockWidgetArea(dw)
1761 by_area[area].append(dw)
1763 for dockwidgets in by_area.values():
1764 dw_last = None
1765 for dw in dockwidgets:
1766 if dw_last is not None:
1767 self.tabifyDockWidget(dw_last, dw)
1769 dw_last = dw
1771 def update_slug_abbreviated_lengths(self):
1772 dockwidgets = self.findChildren(common.MyDockWidget)
1773 title_labels = []
1774 for dw in dockwidgets:
1775 title_labels.append(dw.titlebar._title_label)
1777 by_title = defaultdict(list)
1778 for tl in title_labels:
1779 by_title[tl.get_title()].append(tl)
1781 for group in by_title.values():
1782 slugs = [tl.get_slug() for tl in group]
1784 n = max(len(slug) for slug in slugs)
1785 nunique = len(set(slugs))
1787 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1788 n -= 1
1790 if n > 0:
1791 n = max(3, n)
1793 for tl in group:
1794 tl.set_slug_abbreviated_length(n)
1796 def get_dockwidget(self, panel):
1797 dockwidget = panel
1798 while not isinstance(dockwidget, qw.QDockWidget):
1799 dockwidget = dockwidget.parent()
1801 return dockwidget
1803 def raise_panel(self, panel):
1804 dockwidget = self.get_dockwidget(panel)
1805 dockwidget.setVisible(True)
1806 dockwidget.setFocus()
1807 dockwidget.raise_()
1809 def toggle_panel_visibility(self):
1810 self.gui_state.panels_visible = not self.gui_state.panels_visible
1812 def update_panel_visibility(self, *args):
1813 self.setUpdatesEnabled(False)
1814 mbar = self.menuBar()
1815 sbar = self.statusBar()
1816 dockwidgets = self.findChildren(common.MyDockWidget)
1818 # Set height to zero instead of hiding so that shortcuts still work
1819 # otherwise one would have to mess around with separate QShortcut
1820 # objects.
1821 mbar.setFixedHeight(
1822 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1824 sbar.setVisible(self.gui_state.panels_visible)
1825 for dockwidget in dockwidgets:
1826 dockwidget.setBlocked(not self.gui_state.panels_visible)
1828 self.setUpdatesEnabled(True)
1830 def remove_panel(self, panel):
1831 dockwidget = self.get_dockwidget(panel)
1832 self.removeDockWidget(dockwidget)
1833 dockwidget.setParent(None)
1834 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1836 def register_data_provider(self, provider):
1837 if provider not in self.data_providers:
1838 self.data_providers.append(provider)
1840 def unregister_data_provider(self, provider):
1841 if provider in self.data_providers:
1842 self.data_providers.remove(provider)
1844 def iter_data(self, name):
1845 for provider in self.data_providers:
1846 for data in provider.iter_data(name):
1847 yield data
1849 def confirm_close(self):
1850 ret = qw.QMessageBox.question(
1851 self,
1852 'Sparrow',
1853 'Close Sparrow window?',
1854 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1855 qw.QMessageBox.Ok)
1857 return ret == qw.QMessageBox.Ok
1859 def closeEvent(self, event):
1860 if self.instant_close or self.confirm_close():
1861 self.attach()
1862 self.closing = True
1863 event.accept()
1864 else:
1865 event.ignore()
1867 def is_closing(self):
1868 return self.closing
1871def main(*args, **kwargs):
1873 from pyrocko import util
1874 from pyrocko.gui import util as gui_util
1875 from . import common
1876 util.setup_logging('sparrow', 'info')
1878 global win
1880 app = gui_util.get_app()
1881 win = SparrowViewer(*args, **kwargs)
1882 app.set_main_window(win)
1884 gui_util.app.install_sigint_handler()
1886 try:
1887 gui_util.app.exec_()
1888 finally:
1889 gui_util.app.uninstall_sigint_handler()
1890 app.unset_main_window()
1891 common.set_viewer(None)
1892 del win
1893 gc.collect()