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(), visible=True,
477 where=qc.Qt.LeftDockWidgetArea)
479 self.add_panel(
480 'Time',
481 self.controls_time(), visible=True,
482 where=qc.Qt.LeftDockWidgetArea)
484 self.add_panel(
485 'Appearance',
486 self.controls_appearance(), visible=True,
487 where=qc.Qt.LeftDockWidgetArea)
489 snapshots_panel = self.controls_snapshots()
490 self.snapshots_panel = snapshots_panel
491 self.add_panel(
492 'Snapshots',
493 snapshots_panel, visible=False,
494 where=qc.Qt.LeftDockWidgetArea)
496 snapshots_panel.setup_menu(snapshots_menu)
498 self.setCentralWidget(self.main_frame)
500 self.mesh = None
502 ren = vtk.vtkRenderer()
504 # ren.SetBackground(0.15, 0.15, 0.15)
505 # ren.SetBackground(0.0, 0.0, 0.0)
506 # ren.TwoSidedLightingOn()
507 # ren.SetUseShadows(1)
509 self._lighting = None
510 self._background = None
512 self.ren = ren
513 self.update_render_settings()
514 self.update_camera()
516 renwin = self.vtk_widget.GetRenderWindow()
518 if self._use_depth_peeling:
519 renwin.SetAlphaBitPlanes(1)
520 renwin.SetMultiSamples(0)
522 ren.SetUseDepthPeeling(1)
523 ren.SetMaximumNumberOfPeels(100)
524 ren.SetOcclusionRatio(0.1)
526 ren.SetUseFXAA(1)
527 # ren.SetUseHiddenLineRemoval(1)
528 # ren.SetBackingStore(1)
530 self.renwin = renwin
532 # renwin.LineSmoothingOn()
533 # renwin.PointSmoothingOn()
534 # renwin.PolygonSmoothingOn()
536 renwin.AddRenderer(ren)
538 iren = renwin.GetInteractor()
539 iren.LightFollowCameraOn()
540 iren.SetInteractorStyle(None)
542 iren.AddObserver('LeftButtonPressEvent', self.button_event)
543 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
544 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
545 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
546 iren.AddObserver('RightButtonPressEvent', self.button_event)
547 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
548 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
549 iren.AddObserver('KeyPressEvent', self.key_down_event)
550 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
552 renwin.Render()
554 iren.Initialize()
556 self.iren = iren
558 self.rotating = False
560 self._elements = {}
561 self._elements_active = {}
563 self.talkie_connect(
564 self.state, 'elements', self.update_elements)
566 self.state.elements.append(elements.IcosphereState(
567 element_id='icosphere',
568 level=4,
569 smooth=True,
570 opacity=0.5,
571 ambient=0.1))
573 self.state.elements.append(elements.GridState(
574 element_id='grid'))
575 self.state.elements.append(elements.CoastlinesState(
576 element_id='coastlines'))
577 self.state.elements.append(elements.CrosshairState(
578 element_id='crosshair'))
580 # self.state.elements.append(elements.StationsState())
581 # self.state.elements.append(elements.SourceState())
582 # self.state.elements.append(
583 # elements.CatalogState(
584 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
585 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
587 if events:
588 self.state.elements.append(
589 elements.CatalogState(
590 selection=elements.MemoryCatalogSelection(events=events)))
592 self.state.sort_elements()
594 if snapshots:
595 snapshots_ = []
596 for obj in snapshots:
597 if isinstance(obj, str):
598 snapshots_.extend(snapshots_mod.load_snapshots(obj))
599 else:
600 snapshots_.append(obj)
602 snapshots_panel.add_snapshots(snapshots_)
603 self.raise_panel(snapshots_panel)
604 snapshots_panel.goto_snapshot(1)
606 self.timer = qc.QTimer(self)
607 self.timer.timeout.connect(self.periodical)
608 self.timer.setInterval(1000)
609 self.timer.start()
611 self._animation_saver = None
613 self.closing = False
614 self.vtk_widget.setFocus()
616 self.update_detached()
618 self.status(
619 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
621 self.status(
622 'Let\'s fly.', 2.0)
624 self.show()
625 self.windowHandle().showMaximized()
627 self.talkie_connect(
628 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
630 self.update_vtk_widget_size()
632 hatch_path = config.expand(os.path.join(
633 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
635 self.talkie_connect(self.state, '', self.capture_state)
636 self.capture_state()
638 set_download_callback(self.update_download_progress)
640 if not os.path.exists(hatch_path):
641 with open(hatch_path, 'w') as f:
642 f.write('%s\n' % util.time_to_str(time.time()))
644 self.start_tour()
646 def update_download_progress(self, message, args):
647 self.download_progress_update.emit()
649 def status(self, message, duration=None):
650 self.statusBar().showMessage(
651 message, int((duration or 0) * 1000))
653 def disable_capture(self):
654 self._block_capture += 1
656 logger.debug('Undo capture block (+1): %i' % self._block_capture)
658 def enable_capture(self, drop=False, aggregate=None):
659 if self._block_capture > 0:
660 self._block_capture -= 1
662 logger.debug('Undo capture block (-1): %i' % self._block_capture)
664 if self._block_capture == 0 and not drop:
665 self.capture_state(aggregate=aggregate)
667 def capture_state(self, *args, aggregate=None):
668 if self._block_capture:
669 return
671 if len(self._undo_stack) == 0 or not state_equal(
672 self.state, self._undo_stack[-1]):
674 if aggregate is not None:
675 if aggregate == self._undo_aggregate:
676 self._undo_stack.pop()
678 self._undo_aggregate = aggregate
679 else:
680 self._undo_aggregate = None
682 logger.debug('Capture undo state (%i%s)\n%s' % (
683 len(self._undo_stack) + 1,
684 '' if aggregate is None else ', aggregate=%s' % aggregate,
685 '\n'.join(
686 ' - %s' % s
687 for s in self._undo_stack[-1].str_diff(
688 self.state).splitlines())
689 if len(self._undo_stack) > 0 else 'initial'))
691 self._undo_stack.append(guts.clone(self.state))
692 self._redo_stack.clear()
694 def undo(self):
695 self._undo_aggregate = None
697 if len(self._undo_stack) <= 1:
698 return
700 state = self._undo_stack.pop()
701 self._redo_stack.append(state)
702 state = self._undo_stack[-1]
704 logger.debug('Undo (%i)\n%s' % (
705 len(self._undo_stack),
706 '\n'.join(
707 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
709 self.disable_capture()
710 try:
711 self.set_state(state)
712 finally:
713 self.enable_capture(drop=True)
715 def redo(self):
716 self._undo_aggregate = None
718 if len(self._redo_stack) == 0:
719 return
721 state = self._redo_stack.pop()
722 self._undo_stack.append(state)
724 logger.debug('Redo (%i)\n%s' % (
725 len(self._redo_stack),
726 '\n'.join(
727 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
729 self.disable_capture()
730 try:
731 self.set_state(state)
732 finally:
733 self.enable_capture(drop=True)
735 def start_tour(self):
736 snapshots_ = snapshots_mod.load_snapshots(
737 'https://data.pyrocko.org/examples/'
738 'sparrow-tour-v0.1.snapshots.yaml')
739 self.snapshots_panel.add_snapshots(snapshots_)
740 self.raise_panel(self.snapshots_panel)
741 self.snapshots_panel.transition_to_next_snapshot()
743 def open_manual(self):
744 import webbrowser
745 webbrowser.open(
746 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
748 def _add_vtk_widget_size_menu_entries(self, menu):
750 group = qw.QActionGroup(menu)
751 group.setExclusive(True)
753 def set_variable_size():
754 self.gui_state.fixed_size = False
756 variable_size_action = menu.addAction('Fit Window Size')
757 variable_size_action.setCheckable(True)
758 variable_size_action.setActionGroup(group)
759 variable_size_action.triggered.connect(set_variable_size)
761 fixed_size_items = []
762 for nx, ny, label in [
763 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
764 (426, 240, ''),
765 (640, 360, ''),
766 (854, 480, '(FWVGA)'),
767 (1280, 720, '(HD)'),
768 (1920, 1080, '(Full HD)'),
769 (2560, 1440, '(Quad HD)'),
770 (3840, 2160, '(4K UHD)'),
771 (3840*2, 2160*2, '',),
772 (None, None, 'Aspect 4:3'),
773 (640, 480, '(VGA)'),
774 (800, 600, '(SVGA)'),
775 (None, None, 'Other'),
776 (512, 512, ''),
777 (1024, 1024, '')]:
779 if None in (nx, ny):
780 menu.addSection(label)
781 else:
782 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
783 action = menu.addAction(name)
784 action.setCheckable(True)
785 action.setActionGroup(group)
786 fixed_size_items.append((action, (nx, ny)))
788 def make_set_fixed_size(nx, ny):
789 def set_fixed_size():
790 self.gui_state.fixed_size = (float(nx), float(ny))
792 return set_fixed_size
794 action.triggered.connect(make_set_fixed_size(nx, ny))
796 def update_widget(*args):
797 for action, (nx, ny) in fixed_size_items:
798 action.blockSignals(True)
799 action.setChecked(
800 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
801 int(z) for z in self.gui_state.fixed_size)))
802 action.blockSignals(False)
804 variable_size_action.blockSignals(True)
805 variable_size_action.setChecked(not self.gui_state.fixed_size)
806 variable_size_action.blockSignals(False)
808 update_widget()
809 self.talkie_connect(
810 self.gui_state, 'fixed_size', update_widget)
812 def update_vtk_widget_size(self, *args):
813 if self.gui_state.fixed_size:
814 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
815 wanted_size = qc.QSize(nx, ny)
816 else:
817 wanted_size = qc.QSize(
818 self.vtk_frame.window().width(), self.vtk_frame.height())
820 current_size = self.vtk_widget.size()
822 if current_size.width() != wanted_size.width() \
823 or current_size.height() != wanted_size.height():
825 self.vtk_widget.setFixedSize(wanted_size)
827 self.vtk_frame.recenter()
828 self.check_vtk_resize()
830 def update_focal_point(self, *args):
831 if self.gui_state.focal_point == 'center':
832 self.vtk_widget.setStatusTip(
833 'Click and drag: change location. %s-click and drag: '
834 'change view plane orientation.' % g_modifier_key)
835 else:
836 self.vtk_widget.setStatusTip(
837 '%s-click and drag: change location. Click and drag: '
838 'change view plane orientation. Uncheck "Navigation: Fix" to '
839 'reverse sense.' % g_modifier_key)
841 def update_detached(self, *args):
843 if self.gui_state.detached and not self.detached_window: # detach
844 logger.debug('Detaching VTK view.')
846 self.main_layout.removeWidget(self.vtk_frame)
847 self.detached_window = DetachedViewer(self, self.vtk_frame)
848 self.detached_window.show()
849 self.vtk_widget.setFocus()
851 screens = common.get_app().screens()
852 if len(screens) > 1:
853 for screen in screens:
854 if screen is not self.screen():
855 self.detached_window.windowHandle().setScreen(screen)
856 # .setScreen() does not work reliably,
857 # therefore trying also with .move()...
858 p = screen.geometry().topLeft()
859 self.detached_window.move(p.x() + 50, p.y() + 50)
860 # ... but also does not work in notion window manager.
862 self.detached_window.windowHandle().showMaximized()
864 frame = qw.QFrame()
865 # frame.setFrameShape(qw.QFrame.NoFrame)
866 # frame.setBackgroundRole(qg.QPalette.Mid)
867 # frame.setAutoFillBackground(True)
868 frame.setSizePolicy(
869 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
871 layout = qw.QGridLayout()
872 frame.setLayout(layout)
873 self.main_layout.insertWidget(0, frame)
875 self.state_editor = StateEditor(self)
877 layout.addWidget(self.state_editor, 0, 0)
879 # attach_button = qw.QPushButton('Attach View')
880 # attach_button.clicked.connect(self.attach)
881 # layout.addWidget(
882 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
884 self.vtk_frame_substitute = frame
886 if not self.gui_state.detached and self.detached_window: # attach
887 logger.debug('Attaching VTK view.')
888 self.detached_window.hide()
889 self.vtk_frame.setParent(self)
890 if self.vtk_frame_substitute:
891 self.main_layout.removeWidget(self.vtk_frame_substitute)
892 self.state_editor.unbind_state()
893 self.vtk_frame_substitute = None
895 self.main_layout.insertWidget(0, self.vtk_frame)
896 self.detached_window = None
897 self.vtk_widget.setFocus()
899 def attach(self):
900 self.gui_state.detached = False
902 def export_image(self):
904 caption = 'Export Image'
905 fn_out, _ = qw.QFileDialog.getSaveFileName(
906 self, caption, 'image.png',
907 options=common.qfiledialog_options)
909 if fn_out:
910 self.save_image(fn_out)
912 def save_image(self, path):
914 original_fixed_size = self.gui_state.fixed_size
915 if original_fixed_size is None:
916 self.gui_state.fixed_size = (1920., 1080.)
918 wif = vtk.vtkWindowToImageFilter()
919 wif.SetInput(self.renwin)
920 wif.SetInputBufferTypeToRGBA()
921 wif.SetScale(1, 1)
922 wif.ReadFrontBufferOff()
923 writer = vtk.vtkPNGWriter()
924 writer.SetInputConnection(wif.GetOutputPort())
926 self.renwin.Render()
927 wif.Modified()
928 writer.SetFileName(path)
929 writer.Write()
931 self.gui_state.fixed_size = original_fixed_size
933 def update_render_settings(self, *args):
934 if self._lighting is None or self._lighting != self.state.lighting:
935 self.ren.RemoveAllLights()
936 for li in light.get_lights(self.state.lighting):
937 self.ren.AddLight(li)
939 self._lighting = self.state.lighting
941 if self._background is None \
942 or self._background != self.state.background:
944 self.state.background.vtk_apply(self.ren)
945 self._background = self.state.background
947 self.update_view()
949 def start_animation(self, interpolator, output_path=None):
950 if self._animation:
951 logger.debug('Aborting animation in progress to start a new one.')
952 self.stop_animation()
954 self.disable_capture()
955 self._animation = interpolator
956 if output_path is None:
957 self._animation_tstart = time.time()
958 self._animation_iframe = None
959 else:
960 self._animation_iframe = 0
961 self.showFullScreen()
962 self.update_view()
963 self.gui_state.panels_visible = False
964 self.update_view()
966 self._animation_timer = qc.QTimer(self)
967 self._animation_timer.timeout.connect(self.next_animation_frame)
968 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
969 self._animation_timer.start()
970 if output_path is not None:
971 original_fixed_size = self.gui_state.fixed_size
972 if original_fixed_size is None:
973 self.gui_state.fixed_size = (1920., 1080.)
975 wif = vtk.vtkWindowToImageFilter()
976 wif.SetInput(self.renwin)
977 wif.SetInputBufferTypeToRGBA()
978 wif.SetScale(1, 1)
979 wif.ReadFrontBufferOff()
980 writer = vtk.vtkPNGWriter()
981 temp_path = tempfile.mkdtemp()
982 self._animation_saver = (
983 wif, writer, temp_path, output_path, original_fixed_size)
984 writer.SetInputConnection(wif.GetOutputPort())
986 def next_animation_frame(self):
988 ani = self._animation
989 if not ani:
990 return
992 if self._animation_iframe is not None:
993 state = ani(
994 ani.tmin
995 + self._animation_iframe * ani.dt)
997 self._animation_iframe += 1
998 else:
999 tnow = time.time()
1000 state = ani(min(
1001 ani.tmax,
1002 ani.tmin + (tnow - self._animation_tstart)))
1004 self.set_state(state)
1005 self.renwin.Render()
1006 if self._animation_saver:
1007 wif, writer, temp_path, _, _ = self._animation_saver
1008 wif.Modified()
1009 fn = os.path.join(temp_path, 'f%09i.png')
1010 writer.SetFileName(fn % self._animation_iframe)
1011 writer.Write()
1013 if self._animation_iframe is not None:
1014 t = self._animation_iframe * ani.dt
1015 else:
1016 t = tnow - self._animation_tstart
1018 if t > ani.tmax - ani.tmin:
1019 self.stop_animation()
1021 def stop_animation(self):
1022 if self._animation_timer:
1023 self._animation_timer.stop()
1025 if self._animation_saver:
1027 wif, writer, temp_path, output_path, original_fixed_size \
1028 = self._animation_saver
1029 self.gui_state.fixed_size = original_fixed_size
1031 fn_path = os.path.join(temp_path, 'f%09d.png')
1032 check_call([
1033 'ffmpeg', '-y',
1034 '-i', fn_path,
1035 '-c:v', 'libx264',
1036 '-preset', 'slow',
1037 '-crf', '17',
1038 '-vf', 'format=yuv420p,fps=%i' % (
1039 int(round(1.0/self._animation.dt))),
1040 output_path])
1041 shutil.rmtree(temp_path)
1043 self._animation_saver = None
1044 self._animation_saver
1046 self.showNormal()
1047 self.gui_state.panels_visible = True
1049 self._animation_tstart = None
1050 self._animation_iframe = None
1051 self._animation = None
1052 self.enable_capture()
1054 def set_state(self, state):
1055 self.disable_capture()
1056 try:
1057 self._update_elements_enabled = False
1058 self.setUpdatesEnabled(False)
1059 self.state.diff_update(state)
1060 self.state.sort_elements()
1061 self.setUpdatesEnabled(True)
1062 self._update_elements_enabled = True
1063 self.update_elements()
1064 finally:
1065 self.enable_capture()
1067 def periodical(self):
1068 pass
1070 def check_vtk_resize(self, *args):
1071 render_window_size = self.renwin.GetSize()
1072 if self._render_window_size != render_window_size:
1073 self._render_window_size = render_window_size
1074 self.resize_event(*render_window_size)
1076 def update_elements(self, *_):
1077 if not self._update_elements_enabled:
1078 return
1080 if self._in_update_elements:
1081 return
1083 self._in_update_elements = True
1084 for estate in self.state.elements:
1085 if estate.element_id not in self._elements:
1086 new_element = estate.create()
1087 logger.debug('Creating "%s" ("%s").' % (
1088 type(new_element).__name__,
1089 estate.element_id))
1090 self._elements[estate.element_id] = new_element
1092 element = self._elements[estate.element_id]
1094 if estate.element_id not in self._elements_active:
1095 logger.debug('Adding "%s" ("%s")' % (
1096 type(element).__name__,
1097 estate.element_id))
1098 element.bind_state(estate)
1099 element.set_parent(self)
1100 self._elements_active[estate.element_id] = element
1102 state_element_ids = [el.element_id for el in self.state.elements]
1103 deactivate = []
1104 for element_id, element in self._elements_active.items():
1105 if element_id not in state_element_ids:
1106 logger.debug('Removing "%s" ("%s").' % (
1107 type(element).__name__,
1108 element_id))
1109 element.unset_parent()
1110 deactivate.append(element_id)
1112 for element_id in deactivate:
1113 del self._elements_active[element_id]
1115 self._update_crosshair_bindings()
1117 self._in_update_elements = False
1119 def _update_crosshair_bindings(self):
1121 def get_crosshair_element():
1122 for element in self.state.elements:
1123 if element.element_id == 'crosshair':
1124 return element
1126 return None
1128 crosshair = get_crosshair_element()
1129 if crosshair is None or crosshair.is_connected:
1130 return
1132 def to_checkbox(state, widget):
1133 widget.blockSignals(True)
1134 widget.setChecked(state.visible)
1135 widget.blockSignals(False)
1137 def to_state(widget, state):
1138 state.visible = widget.isChecked()
1140 cb = self._crosshair_checkbox
1141 vstate.state_bind(
1142 self, crosshair, ['visible'], to_state,
1143 cb, [cb.toggled], to_checkbox)
1145 crosshair.is_connected = True
1147 def add_actor_2d(self, actor):
1148 if actor not in self._actors_2d:
1149 self.ren.AddActor2D(actor)
1150 self._actors_2d.add(actor)
1152 def remove_actor_2d(self, actor):
1153 if actor in self._actors_2d:
1154 self.ren.RemoveActor2D(actor)
1155 self._actors_2d.remove(actor)
1157 def add_actor(self, actor):
1158 if actor not in self._actors:
1159 self.ren.AddActor(actor)
1160 self._actors.add(actor)
1162 def add_actor_list(self, actorlist):
1163 for actor in actorlist:
1164 self.add_actor(actor)
1166 def remove_actor(self, actor):
1167 if actor in self._actors:
1168 self.ren.RemoveActor(actor)
1169 self._actors.remove(actor)
1171 def update_view(self):
1172 self.vtk_widget.update()
1174 def resize_event(self, size_x, size_y):
1175 self.gui_state.size = (size_x, size_y)
1177 def button_event(self, obj, event):
1178 if event == "LeftButtonPressEvent":
1179 self.rotating = True
1180 elif event == "LeftButtonReleaseEvent":
1181 self.rotating = False
1183 def mouse_move_event(self, obj, event):
1184 x0, y0 = self.iren.GetLastEventPosition()
1185 x, y = self.iren.GetEventPosition()
1187 size_x, size_y = self.renwin.GetSize()
1188 center_x = size_x / 2.0
1189 center_y = size_y / 2.0
1191 if self.rotating:
1192 self.do_rotate(x, y, x0, y0, center_x, center_y)
1194 def myWheelEvent(self, event):
1196 angle = event.angleDelta().y()
1198 if angle > 200:
1199 angle = 200
1201 if angle < -200:
1202 angle = -200
1204 self.disable_capture()
1205 try:
1206 self.do_dolly(-angle/100.)
1207 finally:
1208 self.enable_capture(aggregate='distance')
1210 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1212 dx = x0 - x
1213 dy = y0 - y
1215 phi = d2r*(self.state.strike - 90.)
1216 focp = self.gui_state.focal_point
1218 if focp == 'center':
1219 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1220 - math.sin(phi) * dx + math.cos(phi) * dy
1222 lat = self.state.lat
1223 lon = self.state.lon
1224 factor = self.state.distance / 10.0
1225 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1226 else:
1227 lat = 90. - self.state.dip
1228 lon = -self.state.strike - 90.
1229 factor = 0.5
1230 factor_lat = 1.0
1232 dlat = dy * factor
1233 dlon = dx * factor * factor_lat
1235 lat = max(min(lat + dlat, 90.), -90.)
1236 lon += dlon
1237 lon = (lon + 180.) % 360. - 180.
1239 if focp == 'center':
1240 self.state.lat = float(lat)
1241 self.state.lon = float(lon)
1242 else:
1243 self.state.dip = float(90. - lat)
1244 self.state.strike = float(-(lon + 90.))
1246 def do_dolly(self, v):
1247 self.state.distance *= float(1.0 + 0.1*v)
1249 def key_down_event(self, obj, event):
1250 k = obj.GetKeyCode()
1251 if k == 'f':
1252 self.gui_state.next_focal_point()
1254 elif k == 'r':
1255 self.reset_strike_dip()
1257 elif k == 'p':
1258 print(self.state)
1260 elif k == 'i':
1261 for elem in self.state.elements:
1262 if isinstance(elem, elements.IcosphereState):
1263 elem.visible = not elem.visible
1265 elif k == 'c':
1266 for elem in self.state.elements:
1267 if isinstance(elem, elements.CoastlinesState):
1268 elem.visible = not elem.visible
1270 elif k == 't':
1271 if not any(
1272 isinstance(elem, elements.TopoState)
1273 for elem in self.state.elements):
1275 self.state.elements.append(elements.TopoState())
1276 else:
1277 for elem in self.state.elements:
1278 if isinstance(elem, elements.TopoState):
1279 elem.visible = not elem.visible
1281 # elif k == ' ':
1282 # self.toggle_panel_visibility()
1284 def _state_bind(self, *args, **kwargs):
1285 vstate.state_bind(self, self.state, *args, **kwargs)
1287 def _gui_state_bind(self, *args, **kwargs):
1288 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1290 def controls_navigation(self):
1291 frame = qw.QFrame(self)
1292 frame.setSizePolicy(
1293 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1294 layout = qw.QGridLayout()
1295 frame.setLayout(layout)
1297 # lat, lon, depth
1299 layout.addWidget(
1300 qw.QLabel('Location'), 0, 0, 1, 2)
1302 le = qw.QLineEdit()
1303 le.setStatusTip(
1304 'Latitude, Longitude, Depth [km] or city name: '
1305 'Focal point location.')
1306 layout.addWidget(le, 1, 0, 1, 1)
1308 def lat_lon_depth_to_lineedit(state, widget):
1309 widget.setText('%g, %g, %g' % (
1310 state.lat, state.lon, state.depth / km))
1312 def lineedit_to_lat_lon_depth(widget, state):
1313 self.disable_capture()
1314 try:
1315 s = str(widget.text())
1316 choices = location_to_choices(s)
1317 if len(choices) > 0:
1318 self.state.lat, self.state.lon, self.state.depth = \
1319 choices[0].get_lat_lon_depth()
1320 else:
1321 raise NoLocationChoices(s)
1323 finally:
1324 self.enable_capture()
1326 self._state_bind(
1327 ['lat', 'lon', 'depth'],
1328 lineedit_to_lat_lon_depth,
1329 le, [le.editingFinished, le.returnPressed],
1330 lat_lon_depth_to_lineedit)
1332 self.lat_lon_lineedit = le
1334 # focal point
1336 cb = qw.QCheckBox('Fix')
1337 cb.setStatusTip(
1338 'Fix location. Orbit focal point without pressing %s.'
1339 % g_modifier_key)
1340 layout.addWidget(cb, 1, 1, 1, 1)
1342 def focal_point_to_checkbox(state, widget):
1343 widget.blockSignals(True)
1344 widget.setChecked(self.gui_state.focal_point != 'center')
1345 widget.blockSignals(False)
1347 def checkbox_to_focal_point(widget, state):
1348 self.gui_state.focal_point = \
1349 'target' if widget.isChecked() else 'center'
1351 self._gui_state_bind(
1352 ['focal_point'], checkbox_to_focal_point,
1353 cb, [cb.toggled], focal_point_to_checkbox)
1355 self.focal_point_checkbox = cb
1357 self.talkie_connect(
1358 self.gui_state, 'focal_point', self.update_focal_point)
1360 self.update_focal_point()
1362 # strike, dip
1364 layout.addWidget(
1365 qw.QLabel('View Plane'), 2, 0, 1, 2)
1367 le = qw.QLineEdit()
1368 le.setStatusTip(
1369 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1370 'direction.')
1371 layout.addWidget(le, 3, 0, 1, 1)
1373 def strike_dip_to_lineedit(state, widget):
1374 widget.setText('%g, %g' % (state.strike, state.dip))
1376 def lineedit_to_strike_dip(widget, state):
1377 s = str(widget.text())
1378 string_to_strike_dip = {
1379 'east': (0., 90.),
1380 'west': (180., 90.),
1381 'south': (90., 90.),
1382 'north': (270., 90.),
1383 'top': (90., 0.),
1384 'bottom': (90., 180.)}
1386 self.disable_capture()
1387 if s in string_to_strike_dip:
1388 state.strike, state.dip = string_to_strike_dip[s]
1390 s = s.replace(',', ' ')
1391 try:
1392 state.strike, state.dip = map(float, s.split())
1393 except Exception:
1394 raise ValueError('need two numerical values: <strike>, <dip>')
1395 finally:
1396 self.enable_capture()
1398 self._state_bind(
1399 ['strike', 'dip'], lineedit_to_strike_dip,
1400 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1402 self.strike_dip_lineedit = le
1404 but = qw.QPushButton('Reset')
1405 but.setStatusTip('Reset to north-up map view.')
1406 but.clicked.connect(self.reset_strike_dip)
1407 layout.addWidget(but, 3, 1, 1, 1)
1409 # crosshair
1411 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1412 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1414 # camera bindings
1415 self.talkie_connect(
1416 self.state,
1417 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1418 self.update_camera)
1420 self.talkie_connect(
1421 self.gui_state, 'panels_visible', self.update_panel_visibility)
1423 return frame
1425 def controls_time(self):
1426 frame = qw.QFrame(self)
1427 frame.setSizePolicy(
1428 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1430 layout = qw.QGridLayout()
1431 frame.setLayout(layout)
1433 layout.addWidget(qw.QLabel('Min'), 0, 0)
1434 le_tmin = qw.QLineEdit()
1435 layout.addWidget(le_tmin, 0, 1)
1437 layout.addWidget(qw.QLabel('Max'), 1, 0)
1438 le_tmax = qw.QLineEdit()
1439 layout.addWidget(le_tmax, 1, 1)
1441 label_tcursor = qw.QLabel()
1443 label_tcursor.setSizePolicy(
1444 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1446 layout.addWidget(label_tcursor, 2, 1)
1447 self._label_tcursor = label_tcursor
1449 self._state_bind(
1450 ['tmin'], common.lineedit_to_time, le_tmin,
1451 [le_tmin.editingFinished, le_tmin.returnPressed],
1452 common.time_to_lineedit,
1453 attribute='tmin')
1454 self._state_bind(
1455 ['tmax'], common.lineedit_to_time, le_tmax,
1456 [le_tmax.editingFinished, le_tmax.returnPressed],
1457 common.time_to_lineedit,
1458 attribute='tmax')
1460 self.tmin_lineedit = le_tmin
1461 self.tmax_lineedit = le_tmax
1463 range_edit = RangeEdit()
1464 range_edit.rangeEditPressed.connect(self.disable_capture)
1465 range_edit.rangeEditReleased.connect(self.enable_capture)
1466 range_edit.set_data_provider(self)
1467 range_edit.set_data_name('time')
1469 xblock = [False]
1471 def range_to_range_edit(state, widget):
1472 if not xblock[0]:
1473 widget.blockSignals(True)
1474 widget.set_focus(state.tduration, state.tposition)
1475 widget.set_range(state.tmin, state.tmax)
1476 widget.blockSignals(False)
1478 def range_edit_to_range(widget, state):
1479 xblock[0] = True
1480 self.state.tduration, self.state.tposition = widget.get_focus()
1481 self.state.tmin, self.state.tmax = widget.get_range()
1482 xblock[0] = False
1484 self._state_bind(
1485 ['tmin', 'tmax', 'tduration', 'tposition'],
1486 range_edit_to_range,
1487 range_edit,
1488 [range_edit.rangeChanged, range_edit.focusChanged],
1489 range_to_range_edit)
1491 def handle_tcursor_changed():
1492 self.gui_state.tcursor = range_edit.get_tcursor()
1494 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1496 layout.addWidget(range_edit, 3, 0, 1, 2)
1498 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1499 le_focus = qw.QLineEdit()
1500 layout.addWidget(le_focus, 4, 1)
1502 def focus_to_lineedit(state, widget):
1503 if state.tduration is None:
1504 widget.setText('')
1505 else:
1506 widget.setText('%s, %g' % (
1507 guts.str_duration(state.tduration),
1508 state.tposition))
1510 def lineedit_to_focus(widget, state):
1511 s = str(widget.text())
1512 w = [x.strip() for x in s.split(',')]
1513 try:
1514 if len(w) == 0 or not w[0]:
1515 state.tduration = None
1516 state.tposition = 0.0
1517 else:
1518 state.tduration = guts.parse_duration(w[0])
1519 if len(w) > 1:
1520 state.tposition = float(w[1])
1521 else:
1522 state.tposition = 0.0
1524 except Exception:
1525 raise ValueError('need two values: <duration>, <position>')
1527 self._state_bind(
1528 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1529 [le_focus.editingFinished, le_focus.returnPressed],
1530 focus_to_lineedit)
1532 label_effective_tmin = qw.QLabel()
1533 label_effective_tmax = qw.QLabel()
1535 label_effective_tmin.setSizePolicy(
1536 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1537 label_effective_tmax.setSizePolicy(
1538 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1539 label_effective_tmin.setMinimumSize(
1540 qg.QFontMetrics(label_effective_tmin.font()).width(
1541 '0000-00-00 00:00:00.000 '), 0)
1543 layout.addWidget(label_effective_tmin, 5, 1)
1544 layout.addWidget(label_effective_tmax, 6, 1)
1546 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1547 self.talkie_connect(
1548 self.state, var, self.update_effective_time_labels)
1550 self._label_effective_tmin = label_effective_tmin
1551 self._label_effective_tmax = label_effective_tmax
1553 self.talkie_connect(
1554 self.gui_state, 'tcursor', self.update_tcursor)
1556 return frame
1558 def controls_appearance(self):
1559 frame = qw.QFrame(self)
1560 frame.setSizePolicy(
1561 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1562 layout = qw.QGridLayout()
1563 frame.setLayout(layout)
1565 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1567 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1568 layout.addWidget(cb, 0, 1)
1569 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1571 self.talkie_connect(
1572 self.state, 'lighting', self.update_render_settings)
1574 # background
1576 layout.addWidget(qw.QLabel('Background'), 1, 0)
1578 cb = common.strings_to_combobox(
1579 ['black', 'white', 'skyblue1 - white'])
1581 layout.addWidget(cb, 1, 1)
1582 vstate.state_bind_combobox_background(
1583 self, self.state, 'background', cb)
1585 self.talkie_connect(
1586 self.state, 'background', self.update_render_settings)
1588 return frame
1590 def controls_snapshots(self):
1591 return snapshots_mod.SnapshotsPanel(self)
1593 def update_effective_time_labels(self, *args):
1594 tmin = self.state.tmin_effective
1595 tmax = self.state.tmax_effective
1597 stmin = common.time_or_none_to_str(tmin)
1598 stmax = common.time_or_none_to_str(tmax)
1600 self._label_effective_tmin.setText(stmin)
1601 self._label_effective_tmax.setText(stmax)
1603 def update_tcursor(self, *args):
1604 tcursor = self.gui_state.tcursor
1605 stcursor = common.time_or_none_to_str(tcursor)
1606 self._label_tcursor.setText(stcursor)
1608 def reset_strike_dip(self, *args):
1609 self.state.strike = 90.
1610 self.state.dip = 0
1611 self.gui_state.focal_point = 'center'
1613 def get_camera_geometry(self):
1615 def rtp2xyz(rtp):
1616 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1618 radius = 1.0 - self.state.depth / self.planet_radius
1620 cam_rtp = num.array([
1621 radius+self.state.distance,
1622 self.state.lat * d2r + 0.5*num.pi,
1623 self.state.lon * d2r])
1624 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1625 cam, up, foc = \
1626 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1628 foc_rtp = num.array([
1629 radius,
1630 self.state.lat * d2r + 0.5*num.pi,
1631 self.state.lon * d2r])
1633 foc = rtp2xyz(foc_rtp)
1635 rot_world = pmt.euler_to_matrix(
1636 -(self.state.lat-90.)*d2r,
1637 (self.state.lon+90.)*d2r,
1638 0.0*d2r).T
1640 rot_cam = pmt.euler_to_matrix(
1641 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1643 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1645 cam = foc + num.dot(rot, cam - foc)
1646 up = num.dot(rot, up)
1647 return cam, up, foc
1649 def update_camera(self, *args):
1650 cam, up, foc = self.get_camera_geometry()
1651 camera = self.ren.GetActiveCamera()
1652 camera.SetPosition(*cam)
1653 camera.SetFocalPoint(*foc)
1654 camera.SetViewUp(*up)
1656 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1658 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1659 self.feature_radius_min / self.planet_radius)**2))
1661 # if horizon == 0.0:
1662 # horizon = 2.0 + self.state.distance
1664 # clip_dist = max(min(self.state.distance*5., max(
1665 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1666 # , math.sqrt(num.sum(cam**2)))
1667 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1668 # clip_dist = feature_horizon
1670 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1672 self.camera_params = (
1673 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1675 self.update_view()
1677 def add_panel(
1678 self, title_label, panel,
1679 visible=False,
1680 # volatile=False,
1681 tabify=True,
1682 where=qc.Qt.RightDockWidgetArea,
1683 remove=None,
1684 title_controls=[]):
1686 dockwidget = common.MyDockWidget(
1687 self, title_label, title_controls=title_controls)
1689 if not visible:
1690 dockwidget.hide()
1692 if not self.gui_state.panels_visible:
1693 dockwidget.block()
1695 dockwidget.setWidget(panel)
1697 panel.setParent(dockwidget)
1699 dockwidgets = self.findChildren(common.MyDockWidget)
1700 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1702 self.addDockWidget(where, dockwidget)
1704 nwrap = 4
1705 if dws and len(dws) >= nwrap and tabify:
1706 self.tabifyDockWidget(
1707 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1709 mitem = dockwidget.toggleViewAction()
1711 def update_label(*args):
1712 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1713 self.update_slug_abbreviated_lengths()
1715 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1716 dockwidget.titlebar._title_label.title_changed.connect(
1717 self.update_slug_abbreviated_lengths)
1719 update_label()
1721 self._panel_togglers[dockwidget] = mitem
1722 self.panels_menu.addAction(mitem)
1723 if visible:
1724 dockwidget.setVisible(True)
1725 dockwidget.setFocus()
1726 dockwidget.raise_()
1728 def stack_panels(self):
1729 dockwidgets = self.findChildren(common.MyDockWidget)
1730 by_area = defaultdict(list)
1731 for dw in dockwidgets:
1732 area = self.dockWidgetArea(dw)
1733 by_area[area].append(dw)
1735 for dockwidgets in by_area.values():
1736 dw_last = None
1737 for dw in dockwidgets:
1738 if dw_last is not None:
1739 self.tabifyDockWidget(dw_last, dw)
1741 dw_last = dw
1743 def update_slug_abbreviated_lengths(self):
1744 dockwidgets = self.findChildren(common.MyDockWidget)
1745 title_labels = []
1746 for dw in dockwidgets:
1747 title_labels.append(dw.titlebar._title_label)
1749 by_title = defaultdict(list)
1750 for tl in title_labels:
1751 by_title[tl.get_title()].append(tl)
1753 for group in by_title.values():
1754 slugs = [tl.get_slug() for tl in group]
1756 n = max(len(slug) for slug in slugs)
1757 nunique = len(set(slugs))
1759 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1760 n -= 1
1762 if n > 0:
1763 n = max(3, n)
1765 for tl in group:
1766 tl.set_slug_abbreviated_length(n)
1768 def raise_panel(self, panel):
1769 dockwidget = panel.parent()
1770 dockwidget.setVisible(True)
1771 dockwidget.setFocus()
1772 dockwidget.raise_()
1774 def toggle_panel_visibility(self):
1775 self.gui_state.panels_visible = not self.gui_state.panels_visible
1777 def update_panel_visibility(self, *args):
1778 self.setUpdatesEnabled(False)
1779 mbar = self.menuBar()
1780 sbar = self.statusBar()
1781 dockwidgets = self.findChildren(common.MyDockWidget)
1783 # Set height to zero instead of hiding so that shortcuts still work
1784 # otherwise one would have to mess around with separate QShortcut
1785 # objects.
1786 mbar.setFixedHeight(
1787 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1789 sbar.setVisible(self.gui_state.panels_visible)
1790 for dockwidget in dockwidgets:
1791 dockwidget.setBlocked(not self.gui_state.panels_visible)
1793 self.setUpdatesEnabled(True)
1795 def remove_panel(self, panel):
1796 dockwidget = panel.parent()
1797 self.removeDockWidget(dockwidget)
1798 dockwidget.setParent(None)
1799 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1801 def register_data_provider(self, provider):
1802 if provider not in self.data_providers:
1803 self.data_providers.append(provider)
1805 def unregister_data_provider(self, provider):
1806 if provider in self.data_providers:
1807 self.data_providers.remove(provider)
1809 def iter_data(self, name):
1810 for provider in self.data_providers:
1811 for data in provider.iter_data(name):
1812 yield data
1814 def confirm_close(self):
1815 ret = qw.QMessageBox.question(
1816 self,
1817 'Sparrow',
1818 'Close Sparrow window?',
1819 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1820 qw.QMessageBox.Ok)
1822 return ret == qw.QMessageBox.Ok
1824 def closeEvent(self, event):
1825 if self.instant_close or self.confirm_close():
1826 self.attach()
1827 self.closing = True
1828 event.accept()
1829 else:
1830 event.ignore()
1832 def is_closing(self):
1833 return self.closing
1836def main(*args, **kwargs):
1838 from pyrocko import util
1839 from pyrocko.gui import util as gui_util
1840 from . import common
1841 util.setup_logging('sparrow', 'info')
1843 global win
1845 app = gui_util.get_app()
1846 win = SparrowViewer(*args, **kwargs)
1847 app.set_main_window(win)
1849 gui_util.app.install_sigint_handler()
1851 try:
1852 gui_util.app.exec_()
1853 finally:
1854 gui_util.app.uninstall_sigint_handler()
1855 app.unset_main_window()
1856 common.set_viewer(None)
1857 del win
1858 gc.collect()