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 ('Borders', elements.BordersState()),
411 ('Rivers', elements.RiversState()),
412 ('Rectangular Source', elements.SourceState()),
413 ('HUD Subtitle', elements.HudState(
414 template='Subtitle')),
415 ('HUD (tmax_effective)', elements.HudState(
416 template='tmax: {tmax_effective|date}',
417 position='top-left')),
418 ('AxesBox', elements.AxesBoxState()),
419 ('Volcanoes', elements.VolcanoesState()),
420 ('Faults', elements.ActiveFaultsState()),
421 ('Plate bounds', elements.PlatesBoundsState()),
422 ('InSAR Surface Displacements', elements.KiteState()),
423 ('Geometry', elements.GeometryState()),
424 ('Spheroid', elements.SpheroidState())]):
426 def wrap_add_element(estate):
427 def add_element(*args):
428 new_element = guts.clone(estate)
429 new_element.element_id = elements.random_id()
430 self.state.elements.append(new_element)
431 self.state.sort_elements()
433 return add_element
435 mitem = qw.QAction(name, self)
437 mitem.triggered.connect(wrap_add_element(estate))
439 menu.addAction(mitem)
441 menu = mbar.addMenu('Help')
443 menu.addAction(
444 'Interactive Tour',
445 self.start_tour)
447 menu.addAction(
448 'Online Manual',
449 self.open_manual)
451 self.data_providers = []
452 self.elements = {}
454 self.detached_window = None
456 self.main_frame = qw.QFrame()
457 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
459 self.vtk_frame = CenteringScrollArea()
461 self.vtk_widget = QVTKWidget(self, self)
462 self.vtk_frame.setWidget(self.vtk_widget)
464 self.main_layout = qw.QVBoxLayout()
465 self.main_layout.setContentsMargins(0, 0, 0, 0)
466 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
468 pb = Progressbars(self)
469 self.progressbars = pb
470 self.main_layout.addWidget(pb)
472 self.main_frame.setLayout(self.main_layout)
474 self.vtk_frame_substitute = None
476 self.add_panel(
477 'Navigation',
478 self.controls_navigation(),
479 visible=True,
480 scrollable=False,
481 where=qc.Qt.LeftDockWidgetArea)
483 self.add_panel(
484 'Time',
485 self.controls_time(),
486 visible=True,
487 scrollable=False,
488 where=qc.Qt.LeftDockWidgetArea)
490 self.add_panel(
491 'Appearance',
492 self.controls_appearance(),
493 visible=True,
494 scrollable=False,
495 where=qc.Qt.LeftDockWidgetArea)
497 snapshots_panel = self.controls_snapshots()
498 self.snapshots_panel = snapshots_panel
499 self.add_panel(
500 'Snapshots',
501 snapshots_panel,
502 visible=False,
503 scrollable=False,
504 where=qc.Qt.LeftDockWidgetArea)
506 snapshots_panel.setup_menu(snapshots_menu)
508 self.setCentralWidget(self.main_frame)
510 self.mesh = None
512 ren = vtk.vtkRenderer()
514 # ren.SetBackground(0.15, 0.15, 0.15)
515 # ren.SetBackground(0.0, 0.0, 0.0)
516 # ren.TwoSidedLightingOn()
517 # ren.SetUseShadows(1)
519 self._lighting = None
520 self._background = None
522 self.ren = ren
523 self.update_render_settings()
524 self.update_camera()
526 renwin = self.vtk_widget.GetRenderWindow()
528 if self._use_depth_peeling:
529 renwin.SetAlphaBitPlanes(1)
530 renwin.SetMultiSamples(0)
532 ren.SetUseDepthPeeling(1)
533 ren.SetMaximumNumberOfPeels(100)
534 ren.SetOcclusionRatio(0.1)
536 ren.SetUseFXAA(1)
537 # ren.SetUseHiddenLineRemoval(1)
538 # ren.SetBackingStore(1)
540 self.renwin = renwin
542 # renwin.LineSmoothingOn()
543 # renwin.PointSmoothingOn()
544 # renwin.PolygonSmoothingOn()
546 renwin.AddRenderer(ren)
548 iren = renwin.GetInteractor()
549 iren.LightFollowCameraOn()
550 iren.SetInteractorStyle(None)
552 iren.AddObserver('LeftButtonPressEvent', self.button_event)
553 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
554 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
555 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
556 iren.AddObserver('RightButtonPressEvent', self.button_event)
557 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
558 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
559 iren.AddObserver('KeyPressEvent', self.key_down_event)
560 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
562 renwin.Render()
564 iren.Initialize()
566 self.iren = iren
568 self.rotating = False
570 self._elements = {}
571 self._elements_active = {}
573 self.talkie_connect(
574 self.state, 'elements', self.update_elements)
576 self.state.elements.append(elements.IcosphereState(
577 element_id='icosphere',
578 level=4,
579 smooth=True,
580 opacity=0.5,
581 ambient=0.1))
583 self.state.elements.append(elements.GridState(
584 element_id='grid'))
585 self.state.elements.append(elements.CoastlinesState(
586 element_id='coastlines'))
587 self.state.elements.append(elements.CrosshairState(
588 element_id='crosshair'))
590 # self.state.elements.append(elements.StationsState())
591 # self.state.elements.append(elements.SourceState())
592 # self.state.elements.append(
593 # elements.CatalogState(
594 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
595 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
597 if events:
598 self.state.elements.append(
599 elements.CatalogState(
600 selection=elements.MemoryCatalogSelection(events=events)))
602 self.state.sort_elements()
604 if snapshots:
605 snapshots_ = []
606 for obj in snapshots:
607 if isinstance(obj, str):
608 snapshots_.extend(snapshots_mod.load_snapshots(obj))
609 else:
610 snapshots_.append(obj)
612 snapshots_panel.add_snapshots(snapshots_)
613 self.raise_panel(snapshots_panel)
614 snapshots_panel.goto_snapshot(1)
616 self.timer = qc.QTimer(self)
617 self.timer.timeout.connect(self.periodical)
618 self.timer.setInterval(1000)
619 self.timer.start()
621 self._animation_saver = None
623 self.closing = False
624 self.vtk_widget.setFocus()
626 self.update_detached()
628 self.status(
629 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
631 self.status(
632 'Let\'s fly.', 2.0)
634 self.show()
635 self.windowHandle().showMaximized()
637 self.talkie_connect(
638 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
640 self.update_vtk_widget_size()
642 hatch_path = config.expand(os.path.join(
643 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
645 self.talkie_connect(self.state, '', self.capture_state)
646 self.capture_state()
648 set_download_callback(self.update_download_progress)
650 if not os.path.exists(hatch_path):
651 with open(hatch_path, 'w') as f:
652 f.write('%s\n' % util.time_to_str(time.time()))
654 self.start_tour()
656 def update_download_progress(self, message, args):
657 self.download_progress_update.emit()
659 def status(self, message, duration=None):
660 self.statusBar().showMessage(
661 message, int((duration or 0) * 1000))
663 def disable_capture(self):
664 self._block_capture += 1
666 logger.debug('Undo capture block (+1): %i' % self._block_capture)
668 def enable_capture(self, drop=False, aggregate=None):
669 if self._block_capture > 0:
670 self._block_capture -= 1
672 logger.debug('Undo capture block (-1): %i' % self._block_capture)
674 if self._block_capture == 0 and not drop:
675 self.capture_state(aggregate=aggregate)
677 def capture_state(self, *args, aggregate=None):
678 if self._block_capture:
679 return
681 if len(self._undo_stack) == 0 or not state_equal(
682 self.state, self._undo_stack[-1]):
684 if aggregate is not None:
685 if aggregate == self._undo_aggregate:
686 self._undo_stack.pop()
688 self._undo_aggregate = aggregate
689 else:
690 self._undo_aggregate = None
692 logger.debug('Capture undo state (%i%s)\n%s' % (
693 len(self._undo_stack) + 1,
694 '' if aggregate is None else ', aggregate=%s' % aggregate,
695 '\n'.join(
696 ' - %s' % s
697 for s in self._undo_stack[-1].str_diff(
698 self.state).splitlines())
699 if len(self._undo_stack) > 0 else 'initial'))
701 self._undo_stack.append(guts.clone(self.state))
702 self._redo_stack.clear()
704 def undo(self):
705 self._undo_aggregate = None
707 if len(self._undo_stack) <= 1:
708 return
710 state = self._undo_stack.pop()
711 self._redo_stack.append(state)
712 state = self._undo_stack[-1]
714 logger.debug('Undo (%i)\n%s' % (
715 len(self._undo_stack),
716 '\n'.join(
717 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
719 self.disable_capture()
720 try:
721 self.set_state(state)
722 finally:
723 self.enable_capture(drop=True)
725 def redo(self):
726 self._undo_aggregate = None
728 if len(self._redo_stack) == 0:
729 return
731 state = self._redo_stack.pop()
732 self._undo_stack.append(state)
734 logger.debug('Redo (%i)\n%s' % (
735 len(self._redo_stack),
736 '\n'.join(
737 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
739 self.disable_capture()
740 try:
741 self.set_state(state)
742 finally:
743 self.enable_capture(drop=True)
745 def start_tour(self):
746 snapshots_ = snapshots_mod.load_snapshots(
747 'https://data.pyrocko.org/examples/'
748 'sparrow-tour-v0.1.snapshots.yaml')
749 self.snapshots_panel.add_snapshots(snapshots_)
750 self.raise_panel(self.snapshots_panel)
751 self.snapshots_panel.transition_to_next_snapshot()
753 def open_manual(self):
754 import webbrowser
755 webbrowser.open(
756 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
758 def _add_vtk_widget_size_menu_entries(self, menu):
760 group = qw.QActionGroup(menu)
761 group.setExclusive(True)
763 def set_variable_size():
764 self.gui_state.fixed_size = False
766 variable_size_action = menu.addAction('Fit Window Size')
767 variable_size_action.setCheckable(True)
768 variable_size_action.setActionGroup(group)
769 variable_size_action.triggered.connect(set_variable_size)
771 fixed_size_items = []
772 for nx, ny, label in [
773 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
774 (426, 240, ''),
775 (640, 360, ''),
776 (854, 480, '(FWVGA)'),
777 (1280, 720, '(HD)'),
778 (1920, 1080, '(Full HD)'),
779 (2560, 1440, '(Quad HD)'),
780 (3840, 2160, '(4K UHD)'),
781 (3840*2, 2160*2, '',),
782 (None, None, 'Aspect 4:3'),
783 (640, 480, '(VGA)'),
784 (800, 600, '(SVGA)'),
785 (None, None, 'Other'),
786 (512, 512, ''),
787 (1024, 1024, '')]:
789 if None in (nx, ny):
790 menu.addSection(label)
791 else:
792 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
793 action = menu.addAction(name)
794 action.setCheckable(True)
795 action.setActionGroup(group)
796 fixed_size_items.append((action, (nx, ny)))
798 def make_set_fixed_size(nx, ny):
799 def set_fixed_size():
800 self.gui_state.fixed_size = (float(nx), float(ny))
802 return set_fixed_size
804 action.triggered.connect(make_set_fixed_size(nx, ny))
806 def update_widget(*args):
807 for action, (nx, ny) in fixed_size_items:
808 action.blockSignals(True)
809 action.setChecked(
810 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
811 int(z) for z in self.gui_state.fixed_size)))
812 action.blockSignals(False)
814 variable_size_action.blockSignals(True)
815 variable_size_action.setChecked(not self.gui_state.fixed_size)
816 variable_size_action.blockSignals(False)
818 update_widget()
819 self.talkie_connect(
820 self.gui_state, 'fixed_size', update_widget)
822 def update_vtk_widget_size(self, *args):
823 if self.gui_state.fixed_size:
824 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
825 wanted_size = qc.QSize(nx, ny)
826 else:
827 wanted_size = qc.QSize(
828 self.vtk_frame.window().width(), self.vtk_frame.height())
830 current_size = self.vtk_widget.size()
832 if current_size.width() != wanted_size.width() \
833 or current_size.height() != wanted_size.height():
835 self.vtk_widget.setFixedSize(wanted_size)
837 self.vtk_frame.recenter()
838 self.check_vtk_resize()
840 def update_focal_point(self, *args):
841 if self.gui_state.focal_point == 'center':
842 self.vtk_widget.setStatusTip(
843 'Click and drag: change location. %s-click and drag: '
844 'change view plane orientation.' % g_modifier_key)
845 else:
846 self.vtk_widget.setStatusTip(
847 '%s-click and drag: change location. Click and drag: '
848 'change view plane orientation. Uncheck "Navigation: Fix" to '
849 'reverse sense.' % g_modifier_key)
851 def update_detached(self, *args):
853 if self.gui_state.detached and not self.detached_window: # detach
854 logger.debug('Detaching VTK view.')
856 self.main_layout.removeWidget(self.vtk_frame)
857 self.detached_window = DetachedViewer(self, self.vtk_frame)
858 self.detached_window.show()
859 self.vtk_widget.setFocus()
861 screens = common.get_app().screens()
862 if len(screens) > 1:
863 for screen in screens:
864 if screen is not self.screen():
865 self.detached_window.windowHandle().setScreen(screen)
866 # .setScreen() does not work reliably,
867 # therefore trying also with .move()...
868 p = screen.geometry().topLeft()
869 self.detached_window.move(p.x() + 50, p.y() + 50)
870 # ... but also does not work in notion window manager.
872 self.detached_window.windowHandle().showMaximized()
874 frame = qw.QFrame()
875 # frame.setFrameShape(qw.QFrame.NoFrame)
876 # frame.setBackgroundRole(qg.QPalette.Mid)
877 # frame.setAutoFillBackground(True)
878 frame.setSizePolicy(
879 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
881 layout = qw.QGridLayout()
882 frame.setLayout(layout)
883 self.main_layout.insertWidget(0, frame)
885 self.state_editor = StateEditor(self)
887 layout.addWidget(self.state_editor, 0, 0)
889 # attach_button = qw.QPushButton('Attach View')
890 # attach_button.clicked.connect(self.attach)
891 # layout.addWidget(
892 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
894 self.vtk_frame_substitute = frame
896 if not self.gui_state.detached and self.detached_window: # attach
897 logger.debug('Attaching VTK view.')
898 self.detached_window.hide()
899 self.vtk_frame.setParent(self)
900 if self.vtk_frame_substitute:
901 self.main_layout.removeWidget(self.vtk_frame_substitute)
902 self.state_editor.unbind_state()
903 self.vtk_frame_substitute = None
905 self.main_layout.insertWidget(0, self.vtk_frame)
906 self.detached_window = None
907 self.vtk_widget.setFocus()
909 def attach(self):
910 self.gui_state.detached = False
912 def export_image(self):
914 caption = 'Export Image'
915 fn_out, _ = qw.QFileDialog.getSaveFileName(
916 self, caption, 'image.png',
917 options=common.qfiledialog_options)
919 if fn_out:
920 self.save_image(fn_out)
922 def save_image(self, path):
924 original_fixed_size = self.gui_state.fixed_size
925 if original_fixed_size is None:
926 self.gui_state.fixed_size = (1920., 1080.)
928 wif = vtk.vtkWindowToImageFilter()
929 wif.SetInput(self.renwin)
930 wif.SetInputBufferTypeToRGBA()
931 wif.SetScale(1, 1)
932 wif.ReadFrontBufferOff()
933 writer = vtk.vtkPNGWriter()
934 writer.SetInputConnection(wif.GetOutputPort())
936 self.renwin.Render()
937 wif.Modified()
938 writer.SetFileName(path)
939 writer.Write()
941 self.gui_state.fixed_size = original_fixed_size
943 def update_render_settings(self, *args):
944 if self._lighting is None or self._lighting != self.state.lighting:
945 self.ren.RemoveAllLights()
946 for li in light.get_lights(self.state.lighting):
947 self.ren.AddLight(li)
949 self._lighting = self.state.lighting
951 if self._background is None \
952 or self._background != self.state.background:
954 self.state.background.vtk_apply(self.ren)
955 self._background = self.state.background
957 self.update_view()
959 def start_animation(self, interpolator, output_path=None):
960 if self._animation:
961 logger.debug('Aborting animation in progress to start a new one.')
962 self.stop_animation()
964 self.disable_capture()
965 self._animation = interpolator
966 if output_path is None:
967 self._animation_tstart = time.time()
968 self._animation_iframe = None
969 else:
970 self._animation_iframe = 0
971 mess = 'Rendering movie'
972 self.progressbars.set_status(mess, 0, can_abort=True)
974 self._animation_timer = qc.QTimer(self)
975 self._animation_timer.timeout.connect(self.next_animation_frame)
976 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
977 self._animation_timer.start()
978 if output_path is not None:
979 original_fixed_size = self.gui_state.fixed_size
980 if original_fixed_size is None:
981 self.gui_state.fixed_size = (1920., 1080.)
983 wif = vtk.vtkWindowToImageFilter()
984 wif.SetInput(self.renwin)
985 wif.SetInputBufferTypeToRGBA()
986 wif.SetScale(1, 1)
987 wif.ReadFrontBufferOff()
988 writer = vtk.vtkPNGWriter()
989 temp_path = tempfile.mkdtemp()
990 self._animation_saver = (
991 wif, writer, temp_path, output_path, original_fixed_size)
992 writer.SetInputConnection(wif.GetOutputPort())
994 def next_animation_frame(self):
996 ani = self._animation
997 if not ani:
998 return
1000 if self._animation_iframe is not None:
1001 state = ani(
1002 ani.tmin
1003 + self._animation_iframe * ani.dt)
1005 self._animation_iframe += 1
1006 else:
1007 tnow = time.time()
1008 state = ani(min(
1009 ani.tmax,
1010 ani.tmin + (tnow - self._animation_tstart)))
1012 self.set_state(state)
1013 self.renwin.Render()
1014 abort = False
1015 if self._animation_saver:
1016 abort = self.progressbars.set_status(
1017 'Rendering movie',
1018 100*self._animation_iframe*ani.dt / (ani.tmax - ani.tmin),
1019 can_abort=True)
1021 wif, writer, temp_path, _, _ = self._animation_saver
1022 wif.Modified()
1023 fn = os.path.join(temp_path, 'f%09i.png')
1024 writer.SetFileName(fn % self._animation_iframe)
1025 writer.Write()
1027 if self._animation_iframe is not None:
1028 t = self._animation_iframe * ani.dt
1029 else:
1030 t = tnow - self._animation_tstart
1032 if t > ani.tmax - ani.tmin or abort:
1033 self.stop_animation()
1035 def stop_animation(self):
1036 if self._animation_timer:
1037 self._animation_timer.stop()
1039 if self._animation_saver:
1041 wif, writer, temp_path, output_path, original_fixed_size \
1042 = self._animation_saver
1043 self.gui_state.fixed_size = original_fixed_size
1045 fn_path = os.path.join(temp_path, 'f%09d.png')
1046 check_call([
1047 'ffmpeg', '-y',
1048 '-i', fn_path,
1049 '-c:v', 'libx264',
1050 '-preset', 'slow',
1051 '-crf', '17',
1052 '-vf', 'format=yuv420p,fps=%i' % (
1053 int(round(1.0/self._animation.dt))),
1054 output_path])
1055 shutil.rmtree(temp_path)
1057 self._animation_saver = None
1058 self._animation_saver
1060 self.progressbars.set_status(
1061 'Rendering movie', 100, can_abort=True)
1063 self._animation_tstart = None
1064 self._animation_iframe = None
1065 self._animation = None
1066 self.enable_capture()
1068 def set_state(self, state):
1069 self.disable_capture()
1070 try:
1071 self._update_elements_enabled = False
1072 self.setUpdatesEnabled(False)
1073 self.state.diff_update(state)
1074 self.state.sort_elements()
1075 self.setUpdatesEnabled(True)
1076 self._update_elements_enabled = True
1077 self.update_elements()
1078 finally:
1079 self.enable_capture()
1081 def periodical(self):
1082 pass
1084 def check_vtk_resize(self, *args):
1085 render_window_size = self.renwin.GetSize()
1086 if self._render_window_size != render_window_size:
1087 self._render_window_size = render_window_size
1088 self.resize_event(*render_window_size)
1090 def update_elements(self, *_):
1091 if not self._update_elements_enabled:
1092 return
1094 if self._in_update_elements:
1095 return
1097 self._in_update_elements = True
1098 for estate in self.state.elements:
1099 if estate.element_id not in self._elements:
1100 new_element = estate.create()
1101 logger.debug('Creating "%s" ("%s").' % (
1102 type(new_element).__name__,
1103 estate.element_id))
1104 self._elements[estate.element_id] = new_element
1106 element = self._elements[estate.element_id]
1108 if estate.element_id not in self._elements_active:
1109 logger.debug('Adding "%s" ("%s")' % (
1110 type(element).__name__,
1111 estate.element_id))
1112 element.bind_state(estate)
1113 element.set_parent(self)
1114 self._elements_active[estate.element_id] = element
1116 state_element_ids = [el.element_id for el in self.state.elements]
1117 deactivate = []
1118 for element_id, element in self._elements_active.items():
1119 if element_id not in state_element_ids:
1120 logger.debug('Removing "%s" ("%s").' % (
1121 type(element).__name__,
1122 element_id))
1123 element.unset_parent()
1124 deactivate.append(element_id)
1126 for element_id in deactivate:
1127 del self._elements_active[element_id]
1129 self._update_crosshair_bindings()
1131 self._in_update_elements = False
1133 def _update_crosshair_bindings(self):
1135 def get_crosshair_element():
1136 for element in self.state.elements:
1137 if element.element_id == 'crosshair':
1138 return element
1140 return None
1142 crosshair = get_crosshair_element()
1143 if crosshair is None or crosshair.is_connected:
1144 return
1146 def to_checkbox(state, widget):
1147 widget.blockSignals(True)
1148 widget.setChecked(state.visible)
1149 widget.blockSignals(False)
1151 def to_state(widget, state):
1152 state.visible = widget.isChecked()
1154 cb = self._crosshair_checkbox
1155 vstate.state_bind(
1156 self, crosshair, ['visible'], to_state,
1157 cb, [cb.toggled], to_checkbox)
1159 crosshair.is_connected = True
1161 def add_actor_2d(self, actor):
1162 if actor not in self._actors_2d:
1163 self.ren.AddActor2D(actor)
1164 self._actors_2d.add(actor)
1166 def remove_actor_2d(self, actor):
1167 if actor in self._actors_2d:
1168 self.ren.RemoveActor2D(actor)
1169 self._actors_2d.remove(actor)
1171 def add_actor(self, actor):
1172 if actor not in self._actors:
1173 self.ren.AddActor(actor)
1174 self._actors.add(actor)
1176 def add_actor_list(self, actorlist):
1177 for actor in actorlist:
1178 self.add_actor(actor)
1180 def remove_actor(self, actor):
1181 if actor in self._actors:
1182 self.ren.RemoveActor(actor)
1183 self._actors.remove(actor)
1185 def update_view(self):
1186 self.vtk_widget.update()
1188 def resize_event(self, size_x, size_y):
1189 self.gui_state.size = (size_x, size_y)
1191 def button_event(self, obj, event):
1192 if event == "LeftButtonPressEvent":
1193 self.rotating = True
1194 elif event == "LeftButtonReleaseEvent":
1195 self.rotating = False
1197 def mouse_move_event(self, obj, event):
1198 x0, y0 = self.iren.GetLastEventPosition()
1199 x, y = self.iren.GetEventPosition()
1201 size_x, size_y = self.renwin.GetSize()
1202 center_x = size_x / 2.0
1203 center_y = size_y / 2.0
1205 if self.rotating:
1206 self.do_rotate(x, y, x0, y0, center_x, center_y)
1208 def myWheelEvent(self, event):
1210 angle = event.angleDelta().y()
1212 if angle > 200:
1213 angle = 200
1215 if angle < -200:
1216 angle = -200
1218 self.disable_capture()
1219 try:
1220 self.do_dolly(-angle/100.)
1221 finally:
1222 self.enable_capture(aggregate='distance')
1224 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1226 dx = x0 - x
1227 dy = y0 - y
1229 phi = d2r*(self.state.strike - 90.)
1230 focp = self.gui_state.focal_point
1232 if focp == 'center':
1233 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1234 - math.sin(phi) * dx + math.cos(phi) * dy
1236 lat = self.state.lat
1237 lon = self.state.lon
1238 factor = self.state.distance / 10.0
1239 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1240 else:
1241 lat = 90. - self.state.dip
1242 lon = -self.state.strike - 90.
1243 factor = 0.5
1244 factor_lat = 1.0
1246 dlat = dy * factor
1247 dlon = dx * factor * factor_lat
1249 lat = max(min(lat + dlat, 90.), -90.)
1250 lon += dlon
1251 lon = (lon + 180.) % 360. - 180.
1253 if focp == 'center':
1254 self.state.lat = float(lat)
1255 self.state.lon = float(lon)
1256 else:
1257 self.state.dip = float(90. - lat)
1258 self.state.strike = float(((-(lon + 90.))+180.) % 360. - 180.)
1260 def do_dolly(self, v):
1261 self.state.distance *= float(1.0 + 0.1*v)
1263 def key_down_event(self, obj, event):
1264 k = obj.GetKeyCode()
1265 if k == 'f':
1266 self.gui_state.next_focal_point()
1268 elif k == 'r':
1269 self.reset_strike_dip()
1271 elif k == 'p':
1272 print(self.state)
1274 elif k == 'i':
1275 for elem in self.state.elements:
1276 if isinstance(elem, elements.IcosphereState):
1277 elem.visible = not elem.visible
1279 elif k == 'c':
1280 for elem in self.state.elements:
1281 if isinstance(elem, elements.CoastlinesState):
1282 elem.visible = not elem.visible
1284 elif k == 't':
1285 if not any(
1286 isinstance(elem, elements.TopoState)
1287 for elem in self.state.elements):
1289 self.state.elements.append(elements.TopoState())
1290 else:
1291 for elem in self.state.elements:
1292 if isinstance(elem, elements.TopoState):
1293 elem.visible = not elem.visible
1295 # elif k == ' ':
1296 # self.toggle_panel_visibility()
1298 def _state_bind(self, *args, **kwargs):
1299 vstate.state_bind(self, self.state, *args, **kwargs)
1301 def _gui_state_bind(self, *args, **kwargs):
1302 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1304 def controls_navigation(self):
1305 frame = qw.QFrame(self)
1306 frame.setSizePolicy(
1307 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1308 layout = qw.QGridLayout()
1309 frame.setLayout(layout)
1311 # lat, lon, depth
1313 layout.addWidget(
1314 qw.QLabel('Location'), 0, 0, 1, 2)
1316 le = qw.QLineEdit()
1317 le.setStatusTip(
1318 'Latitude, Longitude, Depth [km] or city name: '
1319 'Focal point location.')
1320 layout.addWidget(le, 1, 0, 1, 1)
1322 def lat_lon_depth_to_lineedit(state, widget):
1323 widget.setText('%g, %g, %g' % (
1324 state.lat, state.lon, state.depth / km))
1326 def lineedit_to_lat_lon_depth(widget, state):
1327 self.disable_capture()
1328 try:
1329 s = str(widget.text())
1330 choices = location_to_choices(s)
1331 if len(choices) > 0:
1332 self.state.lat, self.state.lon, self.state.depth = \
1333 choices[0].get_lat_lon_depth()
1334 else:
1335 raise NoLocationChoices(s)
1337 finally:
1338 self.enable_capture()
1340 self._state_bind(
1341 ['lat', 'lon', 'depth'],
1342 lineedit_to_lat_lon_depth,
1343 le, [le.editingFinished, le.returnPressed],
1344 lat_lon_depth_to_lineedit)
1346 self.lat_lon_lineedit = le
1348 # focal point
1350 cb = qw.QCheckBox('Fix')
1351 cb.setStatusTip(
1352 'Fix location. Orbit focal point without pressing %s.'
1353 % g_modifier_key)
1354 layout.addWidget(cb, 1, 1, 1, 1)
1356 def focal_point_to_checkbox(state, widget):
1357 widget.blockSignals(True)
1358 widget.setChecked(self.gui_state.focal_point != 'center')
1359 widget.blockSignals(False)
1361 def checkbox_to_focal_point(widget, state):
1362 self.gui_state.focal_point = \
1363 'target' if widget.isChecked() else 'center'
1365 self._gui_state_bind(
1366 ['focal_point'], checkbox_to_focal_point,
1367 cb, [cb.toggled], focal_point_to_checkbox)
1369 self.focal_point_checkbox = cb
1371 self.talkie_connect(
1372 self.gui_state, 'focal_point', self.update_focal_point)
1374 self.update_focal_point()
1376 # strike, dip
1378 layout.addWidget(
1379 qw.QLabel('View Plane'), 2, 0, 1, 2)
1381 le = qw.QLineEdit()
1382 le.setStatusTip(
1383 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1384 'direction.')
1385 layout.addWidget(le, 3, 0, 1, 1)
1387 def strike_dip_to_lineedit(state, widget):
1388 widget.setText('%g, %g' % (state.strike, state.dip))
1390 def lineedit_to_strike_dip(widget, state):
1391 s = str(widget.text())
1392 string_to_strike_dip = {
1393 'east': (0., 90.),
1394 'west': (180., 90.),
1395 'south': (90., 90.),
1396 'north': (270., 90.),
1397 'top': (90., 0.),
1398 'bottom': (90., 180.)}
1400 self.disable_capture()
1401 if s in string_to_strike_dip:
1402 state.strike, state.dip = string_to_strike_dip[s]
1404 s = s.replace(',', ' ')
1405 try:
1406 state.strike, state.dip = map(float, s.split())
1407 except Exception:
1408 raise ValueError('need two numerical values: <strike>, <dip>')
1409 finally:
1410 self.enable_capture()
1412 self._state_bind(
1413 ['strike', 'dip'], lineedit_to_strike_dip,
1414 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1416 self.strike_dip_lineedit = le
1418 but = qw.QPushButton('Reset')
1419 but.setStatusTip('Reset to north-up map view.')
1420 but.clicked.connect(self.reset_strike_dip)
1421 layout.addWidget(but, 3, 1, 1, 1)
1423 # crosshair
1425 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1426 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1428 # camera bindings
1429 self.talkie_connect(
1430 self.state,
1431 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1432 self.update_camera)
1434 self.talkie_connect(
1435 self.gui_state, 'panels_visible', self.update_panel_visibility)
1437 return frame
1439 def controls_time(self):
1440 frame = qw.QFrame(self)
1441 frame.setSizePolicy(
1442 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1444 layout = qw.QGridLayout()
1445 frame.setLayout(layout)
1447 layout.addWidget(qw.QLabel('Min'), 0, 0)
1448 le_tmin = qw.QLineEdit()
1449 layout.addWidget(le_tmin, 0, 1)
1451 layout.addWidget(qw.QLabel('Max'), 1, 0)
1452 le_tmax = qw.QLineEdit()
1453 layout.addWidget(le_tmax, 1, 1)
1455 label_tcursor = qw.QLabel()
1457 label_tcursor.setSizePolicy(
1458 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1460 layout.addWidget(label_tcursor, 2, 1)
1461 self._label_tcursor = label_tcursor
1463 self._state_bind(
1464 ['tmin'], common.lineedit_to_time, le_tmin,
1465 [le_tmin.editingFinished, le_tmin.returnPressed],
1466 common.time_to_lineedit,
1467 attribute='tmin')
1468 self._state_bind(
1469 ['tmax'], common.lineedit_to_time, le_tmax,
1470 [le_tmax.editingFinished, le_tmax.returnPressed],
1471 common.time_to_lineedit,
1472 attribute='tmax')
1474 self.tmin_lineedit = le_tmin
1475 self.tmax_lineedit = le_tmax
1477 range_edit = RangeEdit()
1478 range_edit.rangeEditPressed.connect(self.disable_capture)
1479 range_edit.rangeEditReleased.connect(self.enable_capture)
1480 range_edit.set_data_provider(self)
1481 range_edit.set_data_name('time')
1483 xblock = [False]
1485 def range_to_range_edit(state, widget):
1486 if not xblock[0]:
1487 widget.blockSignals(True)
1488 widget.set_focus(state.tduration, state.tposition)
1489 widget.set_range(state.tmin, state.tmax)
1490 widget.blockSignals(False)
1492 def range_edit_to_range(widget, state):
1493 xblock[0] = True
1494 self.state.tduration, self.state.tposition = widget.get_focus()
1495 self.state.tmin, self.state.tmax = widget.get_range()
1496 xblock[0] = False
1498 self._state_bind(
1499 ['tmin', 'tmax', 'tduration', 'tposition'],
1500 range_edit_to_range,
1501 range_edit,
1502 [range_edit.rangeChanged, range_edit.focusChanged],
1503 range_to_range_edit)
1505 def handle_tcursor_changed():
1506 self.gui_state.tcursor = range_edit.get_tcursor()
1508 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1510 layout.addWidget(range_edit, 3, 0, 1, 2)
1512 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1513 le_focus = qw.QLineEdit()
1514 layout.addWidget(le_focus, 4, 1)
1516 def focus_to_lineedit(state, widget):
1517 if state.tduration is None:
1518 widget.setText('')
1519 else:
1520 widget.setText('%s, %g' % (
1521 guts.str_duration(state.tduration),
1522 state.tposition))
1524 def lineedit_to_focus(widget, state):
1525 s = str(widget.text())
1526 w = [x.strip() for x in s.split(',')]
1527 try:
1528 if len(w) == 0 or not w[0]:
1529 state.tduration = None
1530 state.tposition = 0.0
1531 else:
1532 state.tduration = guts.parse_duration(w[0])
1533 if len(w) > 1:
1534 state.tposition = float(w[1])
1535 else:
1536 state.tposition = 0.0
1538 except Exception:
1539 raise ValueError('need two values: <duration>, <position>')
1541 self._state_bind(
1542 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1543 [le_focus.editingFinished, le_focus.returnPressed],
1544 focus_to_lineedit)
1546 label_effective_tmin = qw.QLabel()
1547 label_effective_tmax = qw.QLabel()
1549 label_effective_tmin.setSizePolicy(
1550 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1551 label_effective_tmax.setSizePolicy(
1552 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1553 label_effective_tmin.setMinimumSize(
1554 qg.QFontMetrics(label_effective_tmin.font()).width(
1555 '0000-00-00 00:00:00.000 '), 0)
1557 layout.addWidget(label_effective_tmin, 5, 1)
1558 layout.addWidget(label_effective_tmax, 6, 1)
1560 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1561 self.talkie_connect(
1562 self.state, var, self.update_effective_time_labels)
1564 self._label_effective_tmin = label_effective_tmin
1565 self._label_effective_tmax = label_effective_tmax
1567 self.talkie_connect(
1568 self.gui_state, 'tcursor', self.update_tcursor)
1570 return frame
1572 def controls_appearance(self):
1573 frame = qw.QFrame(self)
1574 frame.setSizePolicy(
1575 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1576 layout = qw.QGridLayout()
1577 frame.setLayout(layout)
1579 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1581 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1582 layout.addWidget(cb, 0, 1)
1583 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1585 self.talkie_connect(
1586 self.state, 'lighting', self.update_render_settings)
1588 # background
1590 layout.addWidget(qw.QLabel('Background'), 1, 0)
1592 cb = common.strings_to_combobox(
1593 ['black', 'white', 'skyblue1 - white'])
1595 layout.addWidget(cb, 1, 1)
1596 vstate.state_bind_combobox_background(
1597 self, self.state, 'background', cb)
1599 self.talkie_connect(
1600 self.state, 'background', self.update_render_settings)
1602 return frame
1604 def controls_snapshots(self):
1605 return snapshots_mod.SnapshotsPanel(self)
1607 def update_effective_time_labels(self, *args):
1608 tmin = self.state.tmin_effective
1609 tmax = self.state.tmax_effective
1611 stmin = common.time_or_none_to_str(tmin)
1612 stmax = common.time_or_none_to_str(tmax)
1614 self._label_effective_tmin.setText(stmin)
1615 self._label_effective_tmax.setText(stmax)
1617 def update_tcursor(self, *args):
1618 tcursor = self.gui_state.tcursor
1619 stcursor = common.time_or_none_to_str(tcursor)
1620 self._label_tcursor.setText(stcursor)
1622 def reset_strike_dip(self, *args):
1623 self.state.strike = 90.
1624 self.state.dip = 0
1625 self.gui_state.focal_point = 'center'
1627 def get_camera_geometry(self):
1629 def rtp2xyz(rtp):
1630 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1632 radius = 1.0 - self.state.depth / self.planet_radius
1634 cam_rtp = num.array([
1635 radius+self.state.distance,
1636 self.state.lat * d2r + 0.5*num.pi,
1637 self.state.lon * d2r])
1638 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1639 cam, up, foc = \
1640 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1642 foc_rtp = num.array([
1643 radius,
1644 self.state.lat * d2r + 0.5*num.pi,
1645 self.state.lon * d2r])
1647 foc = rtp2xyz(foc_rtp)
1649 rot_world = pmt.euler_to_matrix(
1650 -(self.state.lat-90.)*d2r,
1651 (self.state.lon+90.)*d2r,
1652 0.0*d2r).T
1654 rot_cam = pmt.euler_to_matrix(
1655 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1657 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1659 cam = foc + num.dot(rot, cam - foc)
1660 up = num.dot(rot, up)
1661 return cam, up, foc
1663 def update_camera(self, *args):
1664 cam, up, foc = self.get_camera_geometry()
1665 camera = self.ren.GetActiveCamera()
1666 camera.SetPosition(*cam)
1667 camera.SetFocalPoint(*foc)
1668 camera.SetViewUp(*up)
1670 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1672 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1673 self.feature_radius_min / self.planet_radius)**2))
1675 # if horizon == 0.0:
1676 # horizon = 2.0 + self.state.distance
1678 # clip_dist = max(min(self.state.distance*5., max(
1679 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1680 # , math.sqrt(num.sum(cam**2)))
1681 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1682 # clip_dist = feature_horizon
1684 camera.SetClippingRange(
1685 max(clip_dist*0.00001, clip_dist-3.0), clip_dist)
1687 self.camera_params = (
1688 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1690 self.update_view()
1692 def add_panel(
1693 self, title_label, panel,
1694 visible=False,
1695 # volatile=False,
1696 tabify=True,
1697 where=qc.Qt.RightDockWidgetArea,
1698 remove=None,
1699 title_controls=[],
1700 scrollable=True):
1702 dockwidget = common.MyDockWidget(
1703 self, title_label, title_controls=title_controls)
1705 if not visible:
1706 dockwidget.hide()
1708 if not self.gui_state.panels_visible:
1709 dockwidget.block()
1711 if scrollable:
1712 scrollarea = common.MyScrollArea()
1713 scrollarea.setWidget(panel)
1714 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
1715 scrollarea.setSizeAdjustPolicy(
1716 qw.QAbstractScrollArea.AdjustToContents)
1717 scrollarea.setFrameShape(qw.QFrame.NoFrame)
1719 dockwidget.setWidget(scrollarea)
1720 else:
1721 dockwidget.setWidget(panel)
1723 dockwidgets = self.findChildren(common.MyDockWidget)
1724 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1726 self.addDockWidget(where, dockwidget)
1728 nwrap = 4
1729 if dws and len(dws) >= nwrap and tabify:
1730 self.tabifyDockWidget(
1731 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1733 mitem = dockwidget.toggleViewAction()
1735 def update_label(*args):
1736 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1737 self.update_slug_abbreviated_lengths()
1739 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1740 dockwidget.titlebar._title_label.title_changed.connect(
1741 self.update_slug_abbreviated_lengths)
1743 update_label()
1745 self._panel_togglers[dockwidget] = mitem
1746 self.panels_menu.addAction(mitem)
1747 if visible:
1748 dockwidget.setVisible(True)
1749 dockwidget.setFocus()
1750 dockwidget.raise_()
1752 def stack_panels(self):
1753 dockwidgets = self.findChildren(common.MyDockWidget)
1754 by_area = defaultdict(list)
1755 for dw in dockwidgets:
1756 area = self.dockWidgetArea(dw)
1757 by_area[area].append(dw)
1759 for dockwidgets in by_area.values():
1760 dw_last = None
1761 for dw in dockwidgets:
1762 if dw_last is not None:
1763 self.tabifyDockWidget(dw_last, dw)
1765 dw_last = dw
1767 def update_slug_abbreviated_lengths(self):
1768 dockwidgets = self.findChildren(common.MyDockWidget)
1769 title_labels = []
1770 for dw in dockwidgets:
1771 title_labels.append(dw.titlebar._title_label)
1773 by_title = defaultdict(list)
1774 for tl in title_labels:
1775 by_title[tl.get_title()].append(tl)
1777 for group in by_title.values():
1778 slugs = [tl.get_slug() for tl in group]
1780 n = max(len(slug) for slug in slugs)
1781 nunique = len(set(slugs))
1783 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1784 n -= 1
1786 if n > 0:
1787 n = max(3, n)
1789 for tl in group:
1790 tl.set_slug_abbreviated_length(n)
1792 def get_dockwidget(self, panel):
1793 dockwidget = panel
1794 while not isinstance(dockwidget, qw.QDockWidget):
1795 dockwidget = dockwidget.parent()
1797 return dockwidget
1799 def raise_panel(self, panel):
1800 dockwidget = self.get_dockwidget(panel)
1801 dockwidget.setVisible(True)
1802 dockwidget.setFocus()
1803 dockwidget.raise_()
1805 def toggle_panel_visibility(self):
1806 self.gui_state.panels_visible = not self.gui_state.panels_visible
1808 def update_panel_visibility(self, *args):
1809 self.setUpdatesEnabled(False)
1810 mbar = self.menuBar()
1811 sbar = self.statusBar()
1812 dockwidgets = self.findChildren(common.MyDockWidget)
1814 # Set height to zero instead of hiding so that shortcuts still work
1815 # otherwise one would have to mess around with separate QShortcut
1816 # objects.
1817 mbar.setFixedHeight(
1818 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1820 sbar.setVisible(self.gui_state.panels_visible)
1821 for dockwidget in dockwidgets:
1822 dockwidget.setBlocked(not self.gui_state.panels_visible)
1824 self.setUpdatesEnabled(True)
1826 def remove_panel(self, panel):
1827 dockwidget = self.get_dockwidget(panel)
1828 self.removeDockWidget(dockwidget)
1829 dockwidget.setParent(None)
1830 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1832 def register_data_provider(self, provider):
1833 if provider not in self.data_providers:
1834 self.data_providers.append(provider)
1836 def unregister_data_provider(self, provider):
1837 if provider in self.data_providers:
1838 self.data_providers.remove(provider)
1840 def iter_data(self, name):
1841 for provider in self.data_providers:
1842 for data in provider.iter_data(name):
1843 yield data
1845 def confirm_close(self):
1846 ret = qw.QMessageBox.question(
1847 self,
1848 'Sparrow',
1849 'Close Sparrow window?',
1850 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1851 qw.QMessageBox.Ok)
1853 return ret == qw.QMessageBox.Ok
1855 def closeEvent(self, event):
1856 if self.instant_close or self.confirm_close():
1857 self.attach()
1858 self.closing = True
1859 event.accept()
1860 else:
1861 event.ignore()
1863 def is_closing(self):
1864 return self.closing
1867def main(*args, **kwargs):
1869 from pyrocko import util
1870 from pyrocko.gui import util as gui_util
1871 from . import common
1872 util.setup_logging('sparrow', 'info')
1874 global win
1876 app = gui_util.get_app()
1877 win = SparrowViewer(*args, **kwargs)
1878 app.set_main_window(win)
1880 gui_util.app.install_sigint_handler()
1882 try:
1883 gui_util.app.exec_()
1884 finally:
1885 gui_util.app.uninstall_sigint_handler()
1886 app.unset_main_window()
1887 common.set_viewer(None)
1888 del win
1889 gc.collect()