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 self.panels_menu = mbar.addMenu('Panels')
382 self.panels_menu.addAction(
383 'Stack Panels',
384 self.stack_panels)
385 self.panels_menu.addSeparator()
387 snapshots_menu = mbar.addMenu('Snapshots')
389 menu = mbar.addMenu('Elements')
390 for name, estate in sorted([
391 ('Icosphere', elements.IcosphereState(
392 level=4,
393 smooth=True,
394 opacity=0.5,
395 ambient=0.1)),
396 ('Grid', elements.GridState()),
397 ('Stations', elements.StationsState()),
398 ('Topography', elements.TopoState()),
399 ('Custom Topography', elements.CustomTopoState()),
400 ('Catalog', elements.CatalogState()),
401 ('Coastlines', elements.CoastlinesState()),
402 ('Source', elements.SourceState()),
403 ('HUD Subtitle', elements.HudState(
404 template='Subtitle')),
405 ('HUD (tmax_effective)', elements.HudState(
406 template='tmax: {tmax_effective|date}',
407 position='top-left')),
408 ('AxesBox', elements.AxesBoxState()),
409 ('Volcanoes', elements.VolcanoesState()),
410 ('Faults', elements.ActiveFaultsState()),
411 ('Plate bounds', elements.PlatesBoundsState()),
412 ('InSAR Surface Displacements', elements.KiteState()),
413 ('Geometry', elements.GeometryState()),
414 ('Spheroid', elements.SpheroidState())]):
416 def wrap_add_element(estate):
417 def add_element(*args):
418 new_element = guts.clone(estate)
419 new_element.element_id = elements.random_id()
420 self.state.elements.append(new_element)
421 self.state.sort_elements()
423 return add_element
425 mitem = qw.QAction(name, self)
427 mitem.triggered.connect(wrap_add_element(estate))
429 menu.addAction(mitem)
431 menu = mbar.addMenu('Help')
433 menu.addAction(
434 'Interactive Tour',
435 self.start_tour)
437 menu.addAction(
438 'Online Manual',
439 self.open_manual)
441 self.data_providers = []
442 self.elements = {}
444 self.detached_window = None
446 self.main_frame = qw.QFrame()
447 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
449 self.vtk_frame = CenteringScrollArea()
451 self.vtk_widget = QVTKWidget(self, self)
452 self.vtk_frame.setWidget(self.vtk_widget)
454 self.main_layout = qw.QVBoxLayout()
455 self.main_layout.setContentsMargins(0, 0, 0, 0)
456 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
458 pb = Progressbars(self)
459 self.progressbars = pb
460 self.main_layout.addWidget(pb)
462 self.main_frame.setLayout(self.main_layout)
464 self.vtk_frame_substitute = None
466 self.add_panel(
467 'Navigation',
468 self.controls_navigation(), visible=True,
469 where=qc.Qt.LeftDockWidgetArea)
471 self.add_panel(
472 'Time',
473 self.controls_time(), visible=True,
474 where=qc.Qt.LeftDockWidgetArea)
476 self.add_panel(
477 'Appearance',
478 self.controls_appearance(), visible=True,
479 where=qc.Qt.LeftDockWidgetArea)
481 snapshots_panel = self.controls_snapshots()
482 self.snapshots_panel = snapshots_panel
483 self.add_panel(
484 'Snapshots',
485 snapshots_panel, visible=False,
486 where=qc.Qt.LeftDockWidgetArea)
488 snapshots_panel.setup_menu(snapshots_menu)
490 self.setCentralWidget(self.main_frame)
492 self.mesh = None
494 ren = vtk.vtkRenderer()
496 # ren.SetBackground(0.15, 0.15, 0.15)
497 # ren.SetBackground(0.0, 0.0, 0.0)
498 # ren.TwoSidedLightingOn()
499 # ren.SetUseShadows(1)
501 self._lighting = None
502 self._background = None
504 self.ren = ren
505 self.update_render_settings()
506 self.update_camera()
508 renwin = self.vtk_widget.GetRenderWindow()
510 if self._use_depth_peeling:
511 renwin.SetAlphaBitPlanes(1)
512 renwin.SetMultiSamples(0)
514 ren.SetUseDepthPeeling(1)
515 ren.SetMaximumNumberOfPeels(100)
516 ren.SetOcclusionRatio(0.1)
518 ren.SetUseFXAA(1)
519 # ren.SetUseHiddenLineRemoval(1)
520 # ren.SetBackingStore(1)
522 self.renwin = renwin
524 # renwin.LineSmoothingOn()
525 # renwin.PointSmoothingOn()
526 # renwin.PolygonSmoothingOn()
528 renwin.AddRenderer(ren)
530 iren = renwin.GetInteractor()
531 iren.LightFollowCameraOn()
532 iren.SetInteractorStyle(None)
534 iren.AddObserver('LeftButtonPressEvent', self.button_event)
535 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
536 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
537 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
538 iren.AddObserver('RightButtonPressEvent', self.button_event)
539 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
540 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
541 iren.AddObserver('KeyPressEvent', self.key_down_event)
542 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
544 renwin.Render()
546 iren.Initialize()
548 self.iren = iren
550 self.rotating = False
552 self._elements = {}
553 self._elements_active = {}
555 self.talkie_connect(
556 self.state, 'elements', self.update_elements)
558 self.state.elements.append(elements.IcosphereState(
559 element_id='icosphere',
560 level=4,
561 smooth=True,
562 opacity=0.5,
563 ambient=0.1))
565 self.state.elements.append(elements.GridState(
566 element_id='grid'))
567 self.state.elements.append(elements.CoastlinesState(
568 element_id='coastlines'))
569 self.state.elements.append(elements.CrosshairState(
570 element_id='crosshair'))
572 # self.state.elements.append(elements.StationsState())
573 # self.state.elements.append(elements.SourceState())
574 # self.state.elements.append(
575 # elements.CatalogState(
576 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
577 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
579 if events:
580 self.state.elements.append(
581 elements.CatalogState(
582 selection=elements.MemoryCatalogSelection(events=events)))
584 self.state.sort_elements()
586 if snapshots:
587 snapshots_ = []
588 for obj in snapshots:
589 if isinstance(obj, str):
590 snapshots_.extend(snapshots_mod.load_snapshots(obj))
591 else:
592 snapshots_.append(obj)
594 snapshots_panel.add_snapshots(snapshots_)
595 self.raise_panel(snapshots_panel)
596 snapshots_panel.goto_snapshot(1)
598 self.timer = qc.QTimer(self)
599 self.timer.timeout.connect(self.periodical)
600 self.timer.setInterval(1000)
601 self.timer.start()
603 self._animation_saver = None
605 self.closing = False
606 self.vtk_widget.setFocus()
608 self.update_detached()
610 self.status(
611 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
613 self.status(
614 'Let\'s fly.', 2.0)
616 self.show()
617 self.windowHandle().showMaximized()
619 self.talkie_connect(
620 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
622 self.update_vtk_widget_size()
624 hatch_path = config.expand(os.path.join(
625 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
627 self.talkie_connect(self.state, '', self.capture_state)
628 self.capture_state()
630 set_download_callback(self.update_download_progress)
632 if not os.path.exists(hatch_path):
633 with open(hatch_path, 'w') as f:
634 f.write('%s\n' % util.time_to_str(time.time()))
636 self.start_tour()
638 def update_download_progress(self, message, args):
639 self.download_progress_update.emit()
641 def status(self, message, duration=None):
642 self.statusBar().showMessage(
643 message, int((duration or 0) * 1000))
645 def disable_capture(self):
646 self._block_capture += 1
648 logger.debug('Undo capture block (+1): %i' % self._block_capture)
650 def enable_capture(self, drop=False, aggregate=None):
651 if self._block_capture > 0:
652 self._block_capture -= 1
654 logger.debug('Undo capture block (-1): %i' % self._block_capture)
656 if self._block_capture == 0 and not drop:
657 self.capture_state(aggregate=aggregate)
659 def capture_state(self, *args, aggregate=None):
660 if self._block_capture:
661 return
663 if len(self._undo_stack) == 0 or not state_equal(
664 self.state, self._undo_stack[-1]):
666 if aggregate is not None:
667 if aggregate == self._undo_aggregate:
668 self._undo_stack.pop()
670 self._undo_aggregate = aggregate
671 else:
672 self._undo_aggregate = None
674 logger.debug('Capture undo state (%i%s)\n%s' % (
675 len(self._undo_stack) + 1,
676 '' if aggregate is None else ', aggregate=%s' % aggregate,
677 '\n'.join(
678 ' - %s' % s
679 for s in self._undo_stack[-1].str_diff(
680 self.state).splitlines())
681 if len(self._undo_stack) > 0 else 'initial'))
683 self._undo_stack.append(guts.clone(self.state))
684 self._redo_stack.clear()
686 def undo(self):
687 self._undo_aggregate = None
689 if len(self._undo_stack) <= 1:
690 return
692 state = self._undo_stack.pop()
693 self._redo_stack.append(state)
694 state = self._undo_stack[-1]
696 logger.debug('Undo (%i)\n%s' % (
697 len(self._undo_stack),
698 '\n'.join(
699 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
701 self.disable_capture()
702 try:
703 self.set_state(state)
704 finally:
705 self.enable_capture(drop=True)
707 def redo(self):
708 self._undo_aggregate = None
710 if len(self._redo_stack) == 0:
711 return
713 state = self._redo_stack.pop()
714 self._undo_stack.append(state)
716 logger.debug('Redo (%i)\n%s' % (
717 len(self._redo_stack),
718 '\n'.join(
719 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
721 self.disable_capture()
722 try:
723 self.set_state(state)
724 finally:
725 self.enable_capture(drop=True)
727 def start_tour(self):
728 snapshots_ = snapshots_mod.load_snapshots(
729 'https://data.pyrocko.org/examples/'
730 'sparrow-tour-v0.1.snapshots.yaml')
731 self.snapshots_panel.add_snapshots(snapshots_)
732 self.raise_panel(self.snapshots_panel)
733 self.snapshots_panel.transition_to_next_snapshot()
735 def open_manual(self):
736 import webbrowser
737 webbrowser.open(
738 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
740 def _add_vtk_widget_size_menu_entries(self, menu):
742 group = qw.QActionGroup(menu)
743 group.setExclusive(True)
745 def set_variable_size():
746 self.gui_state.fixed_size = False
748 variable_size_action = menu.addAction('Fit Window Size')
749 variable_size_action.setCheckable(True)
750 variable_size_action.setActionGroup(group)
751 variable_size_action.triggered.connect(set_variable_size)
753 fixed_size_items = []
754 for nx, ny, label in [
755 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
756 (426, 240, ''),
757 (640, 360, ''),
758 (854, 480, '(FWVGA)'),
759 (1280, 720, '(HD)'),
760 (1920, 1080, '(Full HD)'),
761 (2560, 1440, '(Quad HD)'),
762 (3840, 2160, '(4K UHD)'),
763 (3840*2, 2160*2, '',),
764 (None, None, 'Aspect 4:3'),
765 (640, 480, '(VGA)'),
766 (800, 600, '(SVGA)'),
767 (None, None, 'Other'),
768 (512, 512, ''),
769 (1024, 1024, '')]:
771 if None in (nx, ny):
772 menu.addSection(label)
773 else:
774 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
775 action = menu.addAction(name)
776 action.setCheckable(True)
777 action.setActionGroup(group)
778 fixed_size_items.append((action, (nx, ny)))
780 def make_set_fixed_size(nx, ny):
781 def set_fixed_size():
782 self.gui_state.fixed_size = (float(nx), float(ny))
784 return set_fixed_size
786 action.triggered.connect(make_set_fixed_size(nx, ny))
788 def update_widget(*args):
789 for action, (nx, ny) in fixed_size_items:
790 action.blockSignals(True)
791 action.setChecked(
792 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
793 int(z) for z in self.gui_state.fixed_size)))
794 action.blockSignals(False)
796 variable_size_action.blockSignals(True)
797 variable_size_action.setChecked(not self.gui_state.fixed_size)
798 variable_size_action.blockSignals(False)
800 update_widget()
801 self.talkie_connect(
802 self.gui_state, 'fixed_size', update_widget)
804 def update_vtk_widget_size(self, *args):
805 if self.gui_state.fixed_size:
806 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
807 wanted_size = qc.QSize(nx, ny)
808 else:
809 wanted_size = qc.QSize(
810 self.vtk_frame.window().width(), self.vtk_frame.height())
812 current_size = self.vtk_widget.size()
814 if current_size.width() != wanted_size.width() \
815 or current_size.height() != wanted_size.height():
817 self.vtk_widget.setFixedSize(wanted_size)
819 self.vtk_frame.recenter()
820 self.check_vtk_resize()
822 def update_focal_point(self, *args):
823 if self.gui_state.focal_point == 'center':
824 self.vtk_widget.setStatusTip(
825 'Click and drag: change location. %s-click and drag: '
826 'change view plane orientation.' % g_modifier_key)
827 else:
828 self.vtk_widget.setStatusTip(
829 '%s-click and drag: change location. Click and drag: '
830 'change view plane orientation. Uncheck "Navigation: Fix" to '
831 'reverse sense.' % g_modifier_key)
833 def update_detached(self, *args):
835 if self.gui_state.detached and not self.detached_window: # detach
836 logger.debug('Detaching VTK view.')
838 self.main_layout.removeWidget(self.vtk_frame)
839 self.detached_window = DetachedViewer(self, self.vtk_frame)
840 self.detached_window.show()
841 self.vtk_widget.setFocus()
843 screens = common.get_app().screens()
844 if len(screens) > 1:
845 for screen in screens:
846 if screen is not self.screen():
847 self.detached_window.windowHandle().setScreen(screen)
848 # .setScreen() does not work reliably,
849 # therefore trying also with .move()...
850 p = screen.geometry().topLeft()
851 self.detached_window.move(p.x() + 50, p.y() + 50)
852 # ... but also does not work in notion window manager.
854 self.detached_window.windowHandle().showMaximized()
856 frame = qw.QFrame()
857 # frame.setFrameShape(qw.QFrame.NoFrame)
858 # frame.setBackgroundRole(qg.QPalette.Mid)
859 # frame.setAutoFillBackground(True)
860 frame.setSizePolicy(
861 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
863 layout = qw.QGridLayout()
864 frame.setLayout(layout)
865 self.main_layout.insertWidget(0, frame)
867 self.state_editor = StateEditor(self)
869 layout.addWidget(self.state_editor, 0, 0)
871 # attach_button = qw.QPushButton('Attach View')
872 # attach_button.clicked.connect(self.attach)
873 # layout.addWidget(
874 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
876 self.vtk_frame_substitute = frame
878 if not self.gui_state.detached and self.detached_window: # attach
879 logger.debug('Attaching VTK view.')
880 self.detached_window.hide()
881 self.vtk_frame.setParent(self)
882 if self.vtk_frame_substitute:
883 self.main_layout.removeWidget(self.vtk_frame_substitute)
884 self.state_editor.unbind_state()
885 self.vtk_frame_substitute = None
887 self.main_layout.insertWidget(0, self.vtk_frame)
888 self.detached_window = None
889 self.vtk_widget.setFocus()
891 def attach(self):
892 self.gui_state.detached = False
894 def export_image(self):
896 caption = 'Export Image'
897 fn_out, _ = qw.QFileDialog.getSaveFileName(
898 self, caption, 'image.png',
899 options=common.qfiledialog_options)
901 if fn_out:
902 self.save_image(fn_out)
904 def save_image(self, path):
906 original_fixed_size = self.gui_state.fixed_size
907 if original_fixed_size is None:
908 self.gui_state.fixed_size = (1920., 1080.)
910 wif = vtk.vtkWindowToImageFilter()
911 wif.SetInput(self.renwin)
912 wif.SetInputBufferTypeToRGBA()
913 wif.SetScale(1, 1)
914 wif.ReadFrontBufferOff()
915 writer = vtk.vtkPNGWriter()
916 writer.SetInputConnection(wif.GetOutputPort())
918 self.renwin.Render()
919 wif.Modified()
920 writer.SetFileName(path)
921 writer.Write()
923 self.gui_state.fixed_size = original_fixed_size
925 def update_render_settings(self, *args):
926 if self._lighting is None or self._lighting != self.state.lighting:
927 self.ren.RemoveAllLights()
928 for li in light.get_lights(self.state.lighting):
929 self.ren.AddLight(li)
931 self._lighting = self.state.lighting
933 if self._background is None \
934 or self._background != self.state.background:
936 self.state.background.vtk_apply(self.ren)
937 self._background = self.state.background
939 self.update_view()
941 def start_animation(self, interpolator, output_path=None):
942 if self._animation:
943 logger.debug('Aborting animation in progress to start a new one.')
944 self.stop_animation()
946 self.disable_capture()
947 self._animation = interpolator
948 if output_path is None:
949 self._animation_tstart = time.time()
950 self._animation_iframe = None
951 else:
952 self._animation_iframe = 0
953 self.showFullScreen()
954 self.update_view()
955 self.gui_state.panels_visible = False
956 self.update_view()
958 self._animation_timer = qc.QTimer(self)
959 self._animation_timer.timeout.connect(self.next_animation_frame)
960 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
961 self._animation_timer.start()
962 if output_path is not None:
963 original_fixed_size = self.gui_state.fixed_size
964 if original_fixed_size is None:
965 self.gui_state.fixed_size = (1920., 1080.)
967 wif = vtk.vtkWindowToImageFilter()
968 wif.SetInput(self.renwin)
969 wif.SetInputBufferTypeToRGBA()
970 wif.SetScale(1, 1)
971 wif.ReadFrontBufferOff()
972 writer = vtk.vtkPNGWriter()
973 temp_path = tempfile.mkdtemp()
974 self._animation_saver = (
975 wif, writer, temp_path, output_path, original_fixed_size)
976 writer.SetInputConnection(wif.GetOutputPort())
978 def next_animation_frame(self):
980 ani = self._animation
981 if not ani:
982 return
984 if self._animation_iframe is not None:
985 state = ani(
986 ani.tmin
987 + self._animation_iframe * ani.dt)
989 self._animation_iframe += 1
990 else:
991 tnow = time.time()
992 state = ani(min(
993 ani.tmax,
994 ani.tmin + (tnow - self._animation_tstart)))
996 self.set_state(state)
997 self.renwin.Render()
998 if self._animation_saver:
999 wif, writer, temp_path, _, _ = self._animation_saver
1000 wif.Modified()
1001 fn = os.path.join(temp_path, 'f%09i.png')
1002 writer.SetFileName(fn % self._animation_iframe)
1003 writer.Write()
1005 if self._animation_iframe is not None:
1006 t = self._animation_iframe * ani.dt
1007 else:
1008 t = tnow - self._animation_tstart
1010 if t > ani.tmax - ani.tmin:
1011 self.stop_animation()
1013 def stop_animation(self):
1014 if self._animation_timer:
1015 self._animation_timer.stop()
1017 if self._animation_saver:
1019 wif, writer, temp_path, output_path, original_fixed_size \
1020 = self._animation_saver
1021 self.gui_state.fixed_size = original_fixed_size
1023 fn_path = os.path.join(temp_path, 'f%09d.png')
1024 check_call([
1025 'ffmpeg', '-y',
1026 '-i', fn_path,
1027 '-c:v', 'libx264',
1028 '-preset', 'slow',
1029 '-crf', '17',
1030 '-vf', 'format=yuv420p,fps=%i' % (
1031 int(round(1.0/self._animation.dt))),
1032 output_path])
1033 shutil.rmtree(temp_path)
1035 self._animation_saver = None
1036 self._animation_saver
1038 self.showNormal()
1039 self.gui_state.panels_visible = True
1041 self._animation_tstart = None
1042 self._animation_iframe = None
1043 self._animation = None
1044 self.enable_capture()
1046 def set_state(self, state):
1047 self.disable_capture()
1048 try:
1049 self._update_elements_enabled = False
1050 self.setUpdatesEnabled(False)
1051 self.state.diff_update(state)
1052 self.state.sort_elements()
1053 self.setUpdatesEnabled(True)
1054 self._update_elements_enabled = True
1055 self.update_elements()
1056 finally:
1057 self.enable_capture()
1059 def periodical(self):
1060 pass
1062 def check_vtk_resize(self, *args):
1063 render_window_size = self.renwin.GetSize()
1064 if self._render_window_size != render_window_size:
1065 self._render_window_size = render_window_size
1066 self.resize_event(*render_window_size)
1068 def update_elements(self, *_):
1069 if not self._update_elements_enabled:
1070 return
1072 if self._in_update_elements:
1073 return
1075 self._in_update_elements = True
1076 for estate in self.state.elements:
1077 if estate.element_id not in self._elements:
1078 new_element = estate.create()
1079 logger.debug('Creating "%s" ("%s").' % (
1080 type(new_element).__name__,
1081 estate.element_id))
1082 self._elements[estate.element_id] = new_element
1084 element = self._elements[estate.element_id]
1086 if estate.element_id not in self._elements_active:
1087 logger.debug('Adding "%s" ("%s")' % (
1088 type(element).__name__,
1089 estate.element_id))
1090 element.bind_state(estate)
1091 element.set_parent(self)
1092 self._elements_active[estate.element_id] = element
1094 state_element_ids = [el.element_id for el in self.state.elements]
1095 deactivate = []
1096 for element_id, element in self._elements_active.items():
1097 if element_id not in state_element_ids:
1098 logger.debug('Removing "%s" ("%s").' % (
1099 type(element).__name__,
1100 element_id))
1101 element.unset_parent()
1102 deactivate.append(element_id)
1104 for element_id in deactivate:
1105 del self._elements_active[element_id]
1107 self._update_crosshair_bindings()
1109 self._in_update_elements = False
1111 def _update_crosshair_bindings(self):
1113 def get_crosshair_element():
1114 for element in self.state.elements:
1115 if element.element_id == 'crosshair':
1116 return element
1118 return None
1120 crosshair = get_crosshair_element()
1121 if crosshair is None or crosshair.is_connected:
1122 return
1124 def to_checkbox(state, widget):
1125 widget.blockSignals(True)
1126 widget.setChecked(state.visible)
1127 widget.blockSignals(False)
1129 def to_state(widget, state):
1130 state.visible = widget.isChecked()
1132 cb = self._crosshair_checkbox
1133 vstate.state_bind(
1134 self, crosshair, ['visible'], to_state,
1135 cb, [cb.toggled], to_checkbox)
1137 crosshair.is_connected = True
1139 def add_actor_2d(self, actor):
1140 if actor not in self._actors_2d:
1141 self.ren.AddActor2D(actor)
1142 self._actors_2d.add(actor)
1144 def remove_actor_2d(self, actor):
1145 if actor in self._actors_2d:
1146 self.ren.RemoveActor2D(actor)
1147 self._actors_2d.remove(actor)
1149 def add_actor(self, actor):
1150 if actor not in self._actors:
1151 self.ren.AddActor(actor)
1152 self._actors.add(actor)
1154 def add_actor_list(self, actorlist):
1155 for actor in actorlist:
1156 self.add_actor(actor)
1158 def remove_actor(self, actor):
1159 if actor in self._actors:
1160 self.ren.RemoveActor(actor)
1161 self._actors.remove(actor)
1163 def update_view(self):
1164 self.vtk_widget.update()
1166 def resize_event(self, size_x, size_y):
1167 self.gui_state.size = (size_x, size_y)
1169 def button_event(self, obj, event):
1170 if event == "LeftButtonPressEvent":
1171 self.rotating = True
1172 elif event == "LeftButtonReleaseEvent":
1173 self.rotating = False
1175 def mouse_move_event(self, obj, event):
1176 x0, y0 = self.iren.GetLastEventPosition()
1177 x, y = self.iren.GetEventPosition()
1179 size_x, size_y = self.renwin.GetSize()
1180 center_x = size_x / 2.0
1181 center_y = size_y / 2.0
1183 if self.rotating:
1184 self.do_rotate(x, y, x0, y0, center_x, center_y)
1186 def myWheelEvent(self, event):
1188 angle = event.angleDelta().y()
1190 if angle > 200:
1191 angle = 200
1193 if angle < -200:
1194 angle = -200
1196 self.disable_capture()
1197 try:
1198 self.do_dolly(-angle/100.)
1199 finally:
1200 self.enable_capture(aggregate='distance')
1202 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1204 dx = x0 - x
1205 dy = y0 - y
1207 phi = d2r*(self.state.strike - 90.)
1208 focp = self.gui_state.focal_point
1210 if focp == 'center':
1211 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1212 - math.sin(phi) * dx + math.cos(phi) * dy
1214 lat = self.state.lat
1215 lon = self.state.lon
1216 factor = self.state.distance / 10.0
1217 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1218 else:
1219 lat = 90. - self.state.dip
1220 lon = -self.state.strike - 90.
1221 factor = 0.5
1222 factor_lat = 1.0
1224 dlat = dy * factor
1225 dlon = dx * factor * factor_lat
1227 lat = max(min(lat + dlat, 90.), -90.)
1228 lon += dlon
1229 lon = (lon + 180.) % 360. - 180.
1231 if focp == 'center':
1232 self.state.lat = float(lat)
1233 self.state.lon = float(lon)
1234 else:
1235 self.state.dip = float(90. - lat)
1236 self.state.strike = float(-(lon + 90.))
1238 def do_dolly(self, v):
1239 self.state.distance *= float(1.0 + 0.1*v)
1241 def key_down_event(self, obj, event):
1242 k = obj.GetKeyCode()
1243 if k == 'f':
1244 self.gui_state.next_focal_point()
1246 elif k == 'r':
1247 self.reset_strike_dip()
1249 elif k == 'p':
1250 print(self.state)
1252 elif k == 'i':
1253 for elem in self.state.elements:
1254 if isinstance(elem, elements.IcosphereState):
1255 elem.visible = not elem.visible
1257 elif k == 'c':
1258 for elem in self.state.elements:
1259 if isinstance(elem, elements.CoastlinesState):
1260 elem.visible = not elem.visible
1262 elif k == 't':
1263 if not any(
1264 isinstance(elem, elements.TopoState)
1265 for elem in self.state.elements):
1267 self.state.elements.append(elements.TopoState())
1268 else:
1269 for elem in self.state.elements:
1270 if isinstance(elem, elements.TopoState):
1271 elem.visible = not elem.visible
1273 elif k == ' ':
1274 self.toggle_panel_visibility()
1276 def _state_bind(self, *args, **kwargs):
1277 vstate.state_bind(self, self.state, *args, **kwargs)
1279 def _gui_state_bind(self, *args, **kwargs):
1280 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1282 def controls_navigation(self):
1283 frame = qw.QFrame(self)
1284 frame.setSizePolicy(
1285 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1286 layout = qw.QGridLayout()
1287 frame.setLayout(layout)
1289 # lat, lon, depth
1291 layout.addWidget(
1292 qw.QLabel('Location'), 0, 0, 1, 2)
1294 le = qw.QLineEdit()
1295 le.setStatusTip(
1296 'Latitude, Longitude, Depth [km] or city name: '
1297 'Focal point location.')
1298 layout.addWidget(le, 1, 0, 1, 1)
1300 def lat_lon_depth_to_lineedit(state, widget):
1301 widget.setText('%g, %g, %g' % (
1302 state.lat, state.lon, state.depth / km))
1304 def lineedit_to_lat_lon_depth(widget, state):
1305 self.disable_capture()
1306 try:
1307 s = str(widget.text())
1308 choices = location_to_choices(s)
1309 if len(choices) > 0:
1310 self.state.lat, self.state.lon, self.state.depth = \
1311 choices[0].get_lat_lon_depth()
1312 else:
1313 raise NoLocationChoices(s)
1315 finally:
1316 self.enable_capture()
1318 self._state_bind(
1319 ['lat', 'lon', 'depth'],
1320 lineedit_to_lat_lon_depth,
1321 le, [le.editingFinished, le.returnPressed],
1322 lat_lon_depth_to_lineedit)
1324 self.lat_lon_lineedit = le
1326 # focal point
1328 cb = qw.QCheckBox('Fix')
1329 cb.setStatusTip(
1330 'Fix location. Orbit focal point without pressing %s.'
1331 % g_modifier_key)
1332 layout.addWidget(cb, 1, 1, 1, 1)
1334 def focal_point_to_checkbox(state, widget):
1335 widget.blockSignals(True)
1336 widget.setChecked(self.gui_state.focal_point != 'center')
1337 widget.blockSignals(False)
1339 def checkbox_to_focal_point(widget, state):
1340 self.gui_state.focal_point = \
1341 'target' if widget.isChecked() else 'center'
1343 self._gui_state_bind(
1344 ['focal_point'], checkbox_to_focal_point,
1345 cb, [cb.toggled], focal_point_to_checkbox)
1347 self.focal_point_checkbox = cb
1349 self.talkie_connect(
1350 self.gui_state, 'focal_point', self.update_focal_point)
1352 self.update_focal_point()
1354 # strike, dip
1356 layout.addWidget(
1357 qw.QLabel('View Plane'), 2, 0, 1, 2)
1359 le = qw.QLineEdit()
1360 le.setStatusTip(
1361 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1362 'direction.')
1363 layout.addWidget(le, 3, 0, 1, 1)
1365 def strike_dip_to_lineedit(state, widget):
1366 widget.setText('%g, %g' % (state.strike, state.dip))
1368 def lineedit_to_strike_dip(widget, state):
1369 s = str(widget.text())
1370 string_to_strike_dip = {
1371 'east': (0., 90.),
1372 'west': (180., 90.),
1373 'south': (90., 90.),
1374 'north': (270., 90.),
1375 'top': (90., 0.),
1376 'bottom': (90., 180.)}
1378 self.disable_capture()
1379 if s in string_to_strike_dip:
1380 state.strike, state.dip = string_to_strike_dip[s]
1382 s = s.replace(',', ' ')
1383 try:
1384 state.strike, state.dip = map(float, s.split())
1385 except Exception:
1386 raise ValueError('need two numerical values: <strike>, <dip>')
1387 finally:
1388 self.enable_capture()
1390 self._state_bind(
1391 ['strike', 'dip'], lineedit_to_strike_dip,
1392 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1394 self.strike_dip_lineedit = le
1396 but = qw.QPushButton('Reset')
1397 but.setStatusTip('Reset to north-up map view.')
1398 but.clicked.connect(self.reset_strike_dip)
1399 layout.addWidget(but, 3, 1, 1, 1)
1401 # crosshair
1403 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1404 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1406 # camera bindings
1407 self.talkie_connect(
1408 self.state,
1409 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1410 self.update_camera)
1412 self.talkie_connect(
1413 self.gui_state, 'panels_visible', self.update_panel_visibility)
1415 return frame
1417 def controls_time(self):
1418 frame = qw.QFrame(self)
1419 frame.setSizePolicy(
1420 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1422 layout = qw.QGridLayout()
1423 frame.setLayout(layout)
1425 layout.addWidget(qw.QLabel('Min'), 0, 0)
1426 le_tmin = qw.QLineEdit()
1427 layout.addWidget(le_tmin, 0, 1)
1429 layout.addWidget(qw.QLabel('Max'), 1, 0)
1430 le_tmax = qw.QLineEdit()
1431 layout.addWidget(le_tmax, 1, 1)
1433 label_tcursor = qw.QLabel()
1435 label_tcursor.setSizePolicy(
1436 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1438 layout.addWidget(label_tcursor, 2, 1)
1439 self._label_tcursor = label_tcursor
1441 def time_to_lineedit(state, attribute, widget):
1442 widget.setText(
1443 common.time_or_none_to_str(getattr(state, attribute)))
1445 def lineedit_to_time(widget, state, attribute):
1446 from pyrocko.util import str_to_time_fillup
1448 s = str(widget.text())
1449 if not s.strip():
1450 setattr(state, attribute, None)
1451 else:
1452 try:
1453 setattr(state, attribute, str_to_time_fillup(s))
1454 except Exception:
1455 raise ValueError(
1456 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1458 self._state_bind(
1459 ['tmin'], lineedit_to_time, le_tmin,
1460 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1461 attribute='tmin')
1462 self._state_bind(
1463 ['tmax'], lineedit_to_time, le_tmax,
1464 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1465 attribute='tmax')
1467 self.tmin_lineedit = le_tmin
1468 self.tmax_lineedit = le_tmax
1470 range_edit = RangeEdit()
1471 range_edit.rangeEditPressed.connect(self.disable_capture)
1472 range_edit.rangeEditReleased.connect(self.enable_capture)
1473 range_edit.set_data_provider(self)
1474 range_edit.set_data_name('time')
1476 xblock = [False]
1478 def range_to_range_edit(state, widget):
1479 if not xblock[0]:
1480 widget.blockSignals(True)
1481 widget.set_focus(state.tduration, state.tposition)
1482 widget.set_range(state.tmin, state.tmax)
1483 widget.blockSignals(False)
1485 def range_edit_to_range(widget, state):
1486 xblock[0] = True
1487 self.state.tduration, self.state.tposition = widget.get_focus()
1488 self.state.tmin, self.state.tmax = widget.get_range()
1489 xblock[0] = False
1491 self._state_bind(
1492 ['tmin', 'tmax', 'tduration', 'tposition'],
1493 range_edit_to_range,
1494 range_edit,
1495 [range_edit.rangeChanged, range_edit.focusChanged],
1496 range_to_range_edit)
1498 def handle_tcursor_changed():
1499 self.gui_state.tcursor = range_edit.get_tcursor()
1501 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1503 layout.addWidget(range_edit, 3, 0, 1, 2)
1505 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1506 le_focus = qw.QLineEdit()
1507 layout.addWidget(le_focus, 4, 1)
1509 def focus_to_lineedit(state, widget):
1510 if state.tduration is None:
1511 widget.setText('')
1512 else:
1513 widget.setText('%s, %g' % (
1514 guts.str_duration(state.tduration),
1515 state.tposition))
1517 def lineedit_to_focus(widget, state):
1518 s = str(widget.text())
1519 w = [x.strip() for x in s.split(',')]
1520 try:
1521 if len(w) == 0 or not w[0]:
1522 state.tduration = None
1523 state.tposition = 0.0
1524 else:
1525 state.tduration = guts.parse_duration(w[0])
1526 if len(w) > 1:
1527 state.tposition = float(w[1])
1528 else:
1529 state.tposition = 0.0
1531 except Exception:
1532 raise ValueError('need two values: <duration>, <position>')
1534 self._state_bind(
1535 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1536 [le_focus.editingFinished, le_focus.returnPressed],
1537 focus_to_lineedit)
1539 label_effective_tmin = qw.QLabel()
1540 label_effective_tmax = qw.QLabel()
1542 label_effective_tmin.setSizePolicy(
1543 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1544 label_effective_tmax.setSizePolicy(
1545 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1546 label_effective_tmin.setMinimumSize(
1547 qg.QFontMetrics(label_effective_tmin.font()).width(
1548 '0000-00-00 00:00:00.000 '), 0)
1550 layout.addWidget(label_effective_tmin, 5, 1)
1551 layout.addWidget(label_effective_tmax, 6, 1)
1553 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1554 self.talkie_connect(
1555 self.state, var, self.update_effective_time_labels)
1557 self._label_effective_tmin = label_effective_tmin
1558 self._label_effective_tmax = label_effective_tmax
1560 self.talkie_connect(
1561 self.gui_state, 'tcursor', self.update_tcursor)
1563 return frame
1565 def controls_appearance(self):
1566 frame = qw.QFrame(self)
1567 frame.setSizePolicy(
1568 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1569 layout = qw.QGridLayout()
1570 frame.setLayout(layout)
1572 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1574 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1575 layout.addWidget(cb, 0, 1)
1576 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1578 self.talkie_connect(
1579 self.state, 'lighting', self.update_render_settings)
1581 # background
1583 layout.addWidget(qw.QLabel('Background'), 1, 0)
1585 cb = common.strings_to_combobox(
1586 ['black', 'white', 'skyblue1 - white'])
1588 layout.addWidget(cb, 1, 1)
1589 vstate.state_bind_combobox_background(
1590 self, self.state, 'background', cb)
1592 self.talkie_connect(
1593 self.state, 'background', self.update_render_settings)
1595 return frame
1597 def controls_snapshots(self):
1598 return snapshots_mod.SnapshotsPanel(self)
1600 def update_effective_time_labels(self, *args):
1601 tmin = self.state.tmin_effective
1602 tmax = self.state.tmax_effective
1604 stmin = common.time_or_none_to_str(tmin)
1605 stmax = common.time_or_none_to_str(tmax)
1607 self._label_effective_tmin.setText(stmin)
1608 self._label_effective_tmax.setText(stmax)
1610 def update_tcursor(self, *args):
1611 tcursor = self.gui_state.tcursor
1612 stcursor = common.time_or_none_to_str(tcursor)
1613 self._label_tcursor.setText(stcursor)
1615 def reset_strike_dip(self, *args):
1616 self.state.strike = 90.
1617 self.state.dip = 0
1618 self.gui_state.focal_point = 'center'
1620 def get_camera_geometry(self):
1622 def rtp2xyz(rtp):
1623 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1625 radius = 1.0 - self.state.depth / self.planet_radius
1627 cam_rtp = num.array([
1628 radius+self.state.distance,
1629 self.state.lat * d2r + 0.5*num.pi,
1630 self.state.lon * d2r])
1631 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1632 cam, up, foc = \
1633 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1635 foc_rtp = num.array([
1636 radius,
1637 self.state.lat * d2r + 0.5*num.pi,
1638 self.state.lon * d2r])
1640 foc = rtp2xyz(foc_rtp)
1642 rot_world = pmt.euler_to_matrix(
1643 -(self.state.lat-90.)*d2r,
1644 (self.state.lon+90.)*d2r,
1645 0.0*d2r).T
1647 rot_cam = pmt.euler_to_matrix(
1648 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1650 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1652 cam = foc + num.dot(rot, cam - foc)
1653 up = num.dot(rot, up)
1654 return cam, up, foc
1656 def update_camera(self, *args):
1657 cam, up, foc = self.get_camera_geometry()
1658 camera = self.ren.GetActiveCamera()
1659 camera.SetPosition(*cam)
1660 camera.SetFocalPoint(*foc)
1661 camera.SetViewUp(*up)
1663 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1665 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1666 self.feature_radius_min / self.planet_radius)**2))
1668 # if horizon == 0.0:
1669 # horizon = 2.0 + self.state.distance
1671 # clip_dist = max(min(self.state.distance*5., max(
1672 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1673 # , math.sqrt(num.sum(cam**2)))
1674 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1675 # clip_dist = feature_horizon
1677 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1679 self.camera_params = (
1680 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1682 self.update_view()
1684 def add_panel(
1685 self, title_label, panel,
1686 visible=False,
1687 # volatile=False,
1688 tabify=True,
1689 where=qc.Qt.RightDockWidgetArea,
1690 remove=None,
1691 title_controls=[]):
1693 dockwidget = common.MyDockWidget(
1694 self, title_label, title_controls=title_controls)
1696 if not visible:
1697 dockwidget.hide()
1699 if not self.gui_state.panels_visible:
1700 dockwidget.block()
1702 dockwidget.setWidget(panel)
1704 panel.setParent(dockwidget)
1706 dockwidgets = self.findChildren(common.MyDockWidget)
1707 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1709 self.addDockWidget(where, dockwidget)
1711 nwrap = 4
1712 if dws and len(dws) >= nwrap and tabify:
1713 self.tabifyDockWidget(
1714 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1716 mitem = dockwidget.toggleViewAction()
1718 def update_label(*args):
1719 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1720 self.update_slug_abbreviated_lengths()
1722 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1723 dockwidget.titlebar._title_label.title_changed.connect(
1724 self.update_slug_abbreviated_lengths)
1726 update_label()
1728 self._panel_togglers[dockwidget] = mitem
1729 self.panels_menu.addAction(mitem)
1730 if visible:
1731 dockwidget.setVisible(True)
1732 dockwidget.setFocus()
1733 dockwidget.raise_()
1735 def stack_panels(self):
1736 dockwidgets = self.findChildren(common.MyDockWidget)
1737 by_area = defaultdict(list)
1738 for dw in dockwidgets:
1739 area = self.dockWidgetArea(dw)
1740 by_area[area].append(dw)
1742 for dockwidgets in by_area.values():
1743 dw_last = None
1744 for dw in dockwidgets:
1745 if dw_last is not None:
1746 self.tabifyDockWidget(dw_last, dw)
1748 dw_last = dw
1750 def update_slug_abbreviated_lengths(self):
1751 dockwidgets = self.findChildren(common.MyDockWidget)
1752 title_labels = []
1753 for dw in dockwidgets:
1754 title_labels.append(dw.titlebar._title_label)
1756 by_title = defaultdict(list)
1757 for tl in title_labels:
1758 by_title[tl.get_title()].append(tl)
1760 for group in by_title.values():
1761 slugs = [tl.get_slug() for tl in group]
1763 n = max(len(slug) for slug in slugs)
1764 nunique = len(set(slugs))
1766 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1767 n -= 1
1769 if n > 0:
1770 n = max(3, n)
1772 for tl in group:
1773 tl.set_slug_abbreviated_length(n)
1775 def raise_panel(self, panel):
1776 dockwidget = panel.parent()
1777 dockwidget.setVisible(True)
1778 dockwidget.setFocus()
1779 dockwidget.raise_()
1781 def toggle_panel_visibility(self):
1782 self.gui_state.panels_visible = not self.gui_state.panels_visible
1784 def update_panel_visibility(self, *args):
1785 self.setUpdatesEnabled(False)
1786 mbar = self.menuBar()
1787 sbar = self.statusBar()
1788 dockwidgets = self.findChildren(common.MyDockWidget)
1790 # Set height to zero instead of hiding so that shortcuts still work
1791 # otherwise one would have to mess around with separate QShortcut
1792 # objects.
1793 mbar.setFixedHeight(
1794 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1796 sbar.setVisible(self.gui_state.panels_visible)
1797 for dockwidget in dockwidgets:
1798 dockwidget.setBlocked(not self.gui_state.panels_visible)
1800 self.setUpdatesEnabled(True)
1802 def remove_panel(self, panel):
1803 dockwidget = panel.parent()
1804 self.removeDockWidget(dockwidget)
1805 dockwidget.setParent(None)
1806 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1808 def register_data_provider(self, provider):
1809 if provider not in self.data_providers:
1810 self.data_providers.append(provider)
1812 def unregister_data_provider(self, provider):
1813 if provider in self.data_providers:
1814 self.data_providers.remove(provider)
1816 def iter_data(self, name):
1817 for provider in self.data_providers:
1818 for data in provider.iter_data(name):
1819 yield data
1821 def confirm_close(self):
1822 ret = qw.QMessageBox.question(
1823 self,
1824 'Sparrow',
1825 'Close Sparrow window?',
1826 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1827 qw.QMessageBox.Ok)
1829 return ret == qw.QMessageBox.Ok
1831 def closeEvent(self, event):
1832 if self.instant_close or self.confirm_close():
1833 self.attach()
1834 self.closing = True
1835 event.accept()
1836 else:
1837 event.ignore()
1839 def is_closing(self):
1840 return self.closing
1843def main(*args, **kwargs):
1845 from pyrocko import util
1846 from pyrocko.gui import util as gui_util
1847 from . import common
1848 util.setup_logging('sparrow', 'info')
1850 global win
1852 app = gui_util.get_app()
1853 win = SparrowViewer(*args, **kwargs)
1854 app.set_main_window(win)
1856 gui_util.app.install_sigint_handler()
1858 try:
1859 gui_util.app.exec_()
1860 finally:
1861 gui_util.app.uninstall_sigint_handler()
1862 app.unset_main_window()
1863 common.set_viewer(None)
1864 del win
1865 gc.collect()