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 ('Geonames', elements.GeonamesState()),
412 ('Rivers', elements.RiversState()),
413 ('Rectangular Source', elements.SourceState()),
414 ('HUD Subtitle', elements.HudState(
415 template='Subtitle')),
416 ('HUD (tmax_effective)', elements.HudState(
417 template='tmax: {tmax_effective|date}',
418 position='top-left')),
419 ('AxesBox', elements.AxesBoxState()),
420 ('Volcanoes', elements.VolcanoesState()),
421 ('Faults', elements.ActiveFaultsState()),
422 ('Plate bounds', elements.PlatesBoundsState()),
423 ('InSAR Surface Displacements', elements.KiteState()),
424 ('Geometry', elements.GeometryState()),
425 ('Spheroid', elements.SpheroidState())]):
427 def wrap_add_element(estate):
428 def add_element(*args):
429 new_element = guts.clone(estate)
430 new_element.element_id = elements.random_id()
431 self.state.elements.append(new_element)
432 self.state.sort_elements()
434 return add_element
436 mitem = qw.QAction(name, self)
438 mitem.triggered.connect(wrap_add_element(estate))
440 menu.addAction(mitem)
442 menu = mbar.addMenu('Help')
444 menu.addAction(
445 'Interactive Tour',
446 self.start_tour)
448 menu.addAction(
449 'Online Manual',
450 self.open_manual)
452 self.data_providers = []
453 self.elements = {}
455 self.detached_window = None
457 self.main_frame = qw.QFrame()
458 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
460 self.vtk_frame = CenteringScrollArea()
462 self.vtk_widget = QVTKWidget(self, self)
463 self.vtk_frame.setWidget(self.vtk_widget)
465 self.main_layout = qw.QVBoxLayout()
466 self.main_layout.setContentsMargins(0, 0, 0, 0)
467 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
469 pb = Progressbars(self)
470 self.progressbars = pb
471 self.main_layout.addWidget(pb)
473 self.main_frame.setLayout(self.main_layout)
475 self.vtk_frame_substitute = None
477 self.add_panel(
478 'Navigation',
479 self.controls_navigation(),
480 visible=True,
481 scrollable=False,
482 where=qc.Qt.LeftDockWidgetArea)
484 self.add_panel(
485 'Time',
486 self.controls_time(),
487 visible=True,
488 scrollable=False,
489 where=qc.Qt.LeftDockWidgetArea)
491 self.add_panel(
492 'Appearance',
493 self.controls_appearance(),
494 visible=True,
495 scrollable=False,
496 where=qc.Qt.LeftDockWidgetArea)
498 snapshots_panel = self.controls_snapshots()
499 self.snapshots_panel = snapshots_panel
500 self.add_panel(
501 'Snapshots',
502 snapshots_panel,
503 visible=False,
504 scrollable=False,
505 where=qc.Qt.LeftDockWidgetArea)
507 snapshots_panel.setup_menu(snapshots_menu)
509 self.setCentralWidget(self.main_frame)
511 self.mesh = None
513 ren = vtk.vtkRenderer()
515 # ren.SetBackground(0.15, 0.15, 0.15)
516 # ren.SetBackground(0.0, 0.0, 0.0)
517 # ren.TwoSidedLightingOn()
518 # ren.SetUseShadows(1)
520 self._lighting = None
521 self._background = None
523 self.ren = ren
524 self.update_render_settings()
525 self.update_camera()
527 renwin = self.vtk_widget.GetRenderWindow()
529 if self._use_depth_peeling:
530 renwin.SetAlphaBitPlanes(1)
531 renwin.SetMultiSamples(0)
533 ren.SetUseDepthPeeling(1)
534 ren.SetMaximumNumberOfPeels(100)
535 ren.SetOcclusionRatio(0.1)
537 ren.SetUseFXAA(1)
538 # ren.SetUseHiddenLineRemoval(1)
539 # ren.SetBackingStore(1)
541 self.renwin = renwin
543 # renwin.LineSmoothingOn()
544 # renwin.PointSmoothingOn()
545 # renwin.PolygonSmoothingOn()
547 renwin.AddRenderer(ren)
549 iren = renwin.GetInteractor()
550 iren.LightFollowCameraOn()
551 iren.SetInteractorStyle(None)
553 iren.AddObserver('LeftButtonPressEvent', self.button_event)
554 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
555 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
556 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
557 iren.AddObserver('RightButtonPressEvent', self.button_event)
558 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
559 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
560 iren.AddObserver('KeyPressEvent', self.key_down_event)
561 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
563 renwin.Render()
565 iren.Initialize()
567 self.iren = iren
569 self.rotating = False
571 self._elements = {}
572 self._elements_active = {}
574 self.talkie_connect(
575 self.state, 'elements', self.update_elements)
577 self.state.elements.append(elements.IcosphereState(
578 element_id='icosphere',
579 level=4,
580 smooth=True,
581 opacity=0.5,
582 ambient=0.1))
584 self.state.elements.append(elements.GridState(
585 element_id='grid'))
586 self.state.elements.append(elements.CoastlinesState(
587 element_id='coastlines'))
588 self.state.elements.append(elements.CrosshairState(
589 element_id='crosshair'))
591 # self.state.elements.append(elements.StationsState())
592 # self.state.elements.append(elements.SourceState())
593 # self.state.elements.append(
594 # elements.CatalogState(
595 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
596 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
598 if events:
599 self.state.elements.append(
600 elements.CatalogState(
601 selection=elements.MemoryCatalogSelection(events=events)))
603 self.state.sort_elements()
605 if snapshots:
606 snapshots_ = []
607 for obj in snapshots:
608 if isinstance(obj, str):
609 snapshots_.extend(snapshots_mod.load_snapshots(obj))
610 else:
611 snapshots_.append(obj)
613 snapshots_panel.add_snapshots(snapshots_)
614 self.raise_panel(snapshots_panel)
615 snapshots_panel.goto_snapshot(1)
617 self.timer = qc.QTimer(self)
618 self.timer.timeout.connect(self.periodical)
619 self.timer.setInterval(1000)
620 self.timer.start()
622 self._animation_saver = None
624 self.closing = False
625 self.vtk_widget.setFocus()
627 self.update_detached()
629 self.status(
630 'Pyrocko Sparrow - A bird\'s eye view.', 2.0)
632 self.status(
633 'Let\'s fly.', 2.0)
635 self.show()
636 self.windowHandle().showMaximized()
638 self.talkie_connect(
639 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
641 self.update_vtk_widget_size()
643 hatch_path = config.expand(os.path.join(
644 config.pyrocko_dir_tmpl, '.sparrow-has-hatched'))
646 self.talkie_connect(self.state, '', self.capture_state)
647 self.capture_state()
649 set_download_callback(self.update_download_progress)
651 if not os.path.exists(hatch_path):
652 with open(hatch_path, 'w') as f:
653 f.write('%s\n' % util.time_to_str(time.time()))
655 self.start_tour()
657 def update_download_progress(self, message, args):
658 self.download_progress_update.emit()
660 def status(self, message, duration=None):
661 self.statusBar().showMessage(
662 message, int((duration or 0) * 1000))
664 def disable_capture(self):
665 self._block_capture += 1
667 logger.debug('Undo capture block (+1): %i' % self._block_capture)
669 def enable_capture(self, drop=False, aggregate=None):
670 if self._block_capture > 0:
671 self._block_capture -= 1
673 logger.debug('Undo capture block (-1): %i' % self._block_capture)
675 if self._block_capture == 0 and not drop:
676 self.capture_state(aggregate=aggregate)
678 def capture_state(self, *args, aggregate=None):
679 if self._block_capture:
680 return
682 if len(self._undo_stack) == 0 or not state_equal(
683 self.state, self._undo_stack[-1]):
685 if aggregate is not None:
686 if aggregate == self._undo_aggregate:
687 self._undo_stack.pop()
689 self._undo_aggregate = aggregate
690 else:
691 self._undo_aggregate = None
693 logger.debug('Capture undo state (%i%s)\n%s' % (
694 len(self._undo_stack) + 1,
695 '' if aggregate is None else ', aggregate=%s' % aggregate,
696 '\n'.join(
697 ' - %s' % s
698 for s in self._undo_stack[-1].str_diff(
699 self.state).splitlines())
700 if len(self._undo_stack) > 0 else 'initial'))
702 self._undo_stack.append(guts.clone(self.state))
703 self._redo_stack.clear()
705 def undo(self):
706 self._undo_aggregate = None
708 if len(self._undo_stack) <= 1:
709 return
711 state = self._undo_stack.pop()
712 self._redo_stack.append(state)
713 state = self._undo_stack[-1]
715 logger.debug('Undo (%i)\n%s' % (
716 len(self._undo_stack),
717 '\n'.join(
718 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
720 self.disable_capture()
721 try:
722 self.set_state(state)
723 finally:
724 self.enable_capture(drop=True)
726 def redo(self):
727 self._undo_aggregate = None
729 if len(self._redo_stack) == 0:
730 return
732 state = self._redo_stack.pop()
733 self._undo_stack.append(state)
735 logger.debug('Redo (%i)\n%s' % (
736 len(self._redo_stack),
737 '\n'.join(
738 ' - %s' % s for s in self.state.str_diff(state).splitlines())))
740 self.disable_capture()
741 try:
742 self.set_state(state)
743 finally:
744 self.enable_capture(drop=True)
746 def start_tour(self):
747 snapshots_ = snapshots_mod.load_snapshots(
748 'https://data.pyrocko.org/examples/'
749 'sparrow-tour-v0.1.snapshots.yaml')
750 self.snapshots_panel.add_snapshots(snapshots_)
751 self.raise_panel(self.snapshots_panel)
752 self.snapshots_panel.transition_to_next_snapshot()
754 def open_manual(self):
755 import webbrowser
756 webbrowser.open(
757 'https://pyrocko.org/docs/current/apps/sparrow/index.html')
759 def _add_vtk_widget_size_menu_entries(self, menu):
761 group = qw.QActionGroup(menu)
762 group.setExclusive(True)
764 def set_variable_size():
765 self.gui_state.fixed_size = False
767 variable_size_action = menu.addAction('Fit Window Size')
768 variable_size_action.setCheckable(True)
769 variable_size_action.setActionGroup(group)
770 variable_size_action.triggered.connect(set_variable_size)
772 fixed_size_items = []
773 for nx, ny, label in [
774 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
775 (426, 240, ''),
776 (640, 360, ''),
777 (854, 480, '(FWVGA)'),
778 (1280, 720, '(HD)'),
779 (1920, 1080, '(Full HD)'),
780 (2560, 1440, '(Quad HD)'),
781 (3840, 2160, '(4K UHD)'),
782 (3840*2, 2160*2, '',),
783 (None, None, 'Aspect 4:3'),
784 (640, 480, '(VGA)'),
785 (800, 600, '(SVGA)'),
786 (None, None, 'Other'),
787 (512, 512, ''),
788 (1024, 1024, '')]:
790 if None in (nx, ny):
791 menu.addSection(label)
792 else:
793 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
794 action = menu.addAction(name)
795 action.setCheckable(True)
796 action.setActionGroup(group)
797 fixed_size_items.append((action, (nx, ny)))
799 def make_set_fixed_size(nx, ny):
800 def set_fixed_size():
801 self.gui_state.fixed_size = (float(nx), float(ny))
803 return set_fixed_size
805 action.triggered.connect(make_set_fixed_size(nx, ny))
807 def update_widget(*args):
808 for action, (nx, ny) in fixed_size_items:
809 action.blockSignals(True)
810 action.setChecked(
811 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
812 int(z) for z in self.gui_state.fixed_size)))
813 action.blockSignals(False)
815 variable_size_action.blockSignals(True)
816 variable_size_action.setChecked(not self.gui_state.fixed_size)
817 variable_size_action.blockSignals(False)
819 update_widget()
820 self.talkie_connect(
821 self.gui_state, 'fixed_size', update_widget)
823 def update_vtk_widget_size(self, *args):
824 if self.gui_state.fixed_size:
825 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
826 wanted_size = qc.QSize(nx, ny)
827 else:
828 wanted_size = qc.QSize(
829 self.vtk_frame.window().width(), self.vtk_frame.height())
831 current_size = self.vtk_widget.size()
833 if current_size.width() != wanted_size.width() \
834 or current_size.height() != wanted_size.height():
836 self.vtk_widget.setFixedSize(wanted_size)
838 self.vtk_frame.recenter()
839 self.check_vtk_resize()
841 def update_focal_point(self, *args):
842 if self.gui_state.focal_point == 'center':
843 self.vtk_widget.setStatusTip(
844 'Click and drag: change location. %s-click and drag: '
845 'change view plane orientation.' % g_modifier_key)
846 else:
847 self.vtk_widget.setStatusTip(
848 '%s-click and drag: change location. Click and drag: '
849 'change view plane orientation. Uncheck "Navigation: Fix" to '
850 'reverse sense.' % g_modifier_key)
852 def update_detached(self, *args):
854 if self.gui_state.detached and not self.detached_window: # detach
855 logger.debug('Detaching VTK view.')
857 self.main_layout.removeWidget(self.vtk_frame)
858 self.detached_window = DetachedViewer(self, self.vtk_frame)
859 self.detached_window.show()
860 self.vtk_widget.setFocus()
862 screens = common.get_app().screens()
863 if len(screens) > 1:
864 for screen in screens:
865 if screen is not self.screen():
866 self.detached_window.windowHandle().setScreen(screen)
867 # .setScreen() does not work reliably,
868 # therefore trying also with .move()...
869 p = screen.geometry().topLeft()
870 self.detached_window.move(p.x() + 50, p.y() + 50)
871 # ... but also does not work in notion window manager.
873 self.detached_window.windowHandle().showMaximized()
875 frame = qw.QFrame()
876 # frame.setFrameShape(qw.QFrame.NoFrame)
877 # frame.setBackgroundRole(qg.QPalette.Mid)
878 # frame.setAutoFillBackground(True)
879 frame.setSizePolicy(
880 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
882 layout = qw.QGridLayout()
883 frame.setLayout(layout)
884 self.main_layout.insertWidget(0, frame)
886 self.state_editor = StateEditor(self)
888 layout.addWidget(self.state_editor, 0, 0)
890 # attach_button = qw.QPushButton('Attach View')
891 # attach_button.clicked.connect(self.attach)
892 # layout.addWidget(
893 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
895 self.vtk_frame_substitute = frame
897 if not self.gui_state.detached and self.detached_window: # attach
898 logger.debug('Attaching VTK view.')
899 self.detached_window.hide()
900 self.vtk_frame.setParent(self)
901 if self.vtk_frame_substitute:
902 self.main_layout.removeWidget(self.vtk_frame_substitute)
903 self.state_editor.unbind_state()
904 self.vtk_frame_substitute = None
906 self.main_layout.insertWidget(0, self.vtk_frame)
907 self.detached_window = None
908 self.vtk_widget.setFocus()
910 def attach(self):
911 self.gui_state.detached = False
913 def export_image(self):
915 caption = 'Export Image'
916 fn_out, _ = qw.QFileDialog.getSaveFileName(
917 self, caption, 'image.png',
918 options=common.qfiledialog_options)
920 if fn_out:
921 self.save_image(fn_out)
923 def save_image(self, path):
925 original_fixed_size = self.gui_state.fixed_size
926 if original_fixed_size is None:
927 self.gui_state.fixed_size = (1920., 1080.)
929 wif = vtk.vtkWindowToImageFilter()
930 wif.SetInput(self.renwin)
931 wif.SetInputBufferTypeToRGBA()
932 wif.SetScale(1, 1)
933 wif.ReadFrontBufferOff()
934 writer = vtk.vtkPNGWriter()
935 writer.SetInputConnection(wif.GetOutputPort())
937 self.renwin.Render()
938 wif.Modified()
939 writer.SetFileName(path)
940 writer.Write()
942 self.gui_state.fixed_size = original_fixed_size
944 def update_render_settings(self, *args):
945 if self._lighting is None or self._lighting != self.state.lighting:
946 self.ren.RemoveAllLights()
947 for li in light.get_lights(self.state.lighting):
948 self.ren.AddLight(li)
950 self._lighting = self.state.lighting
952 if self._background is None \
953 or self._background != self.state.background:
955 self.state.background.vtk_apply(self.ren)
956 self._background = self.state.background
958 self.update_view()
960 def start_animation(self, interpolator, output_path=None):
961 if self._animation:
962 logger.debug('Aborting animation in progress to start a new one.')
963 self.stop_animation()
965 self.disable_capture()
966 self._animation = interpolator
967 if output_path is None:
968 self._animation_tstart = time.time()
969 self._animation_iframe = None
970 else:
971 self._animation_iframe = 0
972 mess = 'Rendering movie'
973 self.progressbars.set_status(mess, 0, can_abort=True)
975 self._animation_timer = qc.QTimer(self)
976 self._animation_timer.timeout.connect(self.next_animation_frame)
977 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
978 self._animation_timer.start()
979 if output_path is not None:
980 original_fixed_size = self.gui_state.fixed_size
981 if original_fixed_size is None:
982 self.gui_state.fixed_size = (1920., 1080.)
984 wif = vtk.vtkWindowToImageFilter()
985 wif.SetInput(self.renwin)
986 wif.SetInputBufferTypeToRGBA()
987 wif.SetScale(1, 1)
988 wif.ReadFrontBufferOff()
989 writer = vtk.vtkPNGWriter()
990 temp_path = tempfile.mkdtemp()
991 self._animation_saver = (
992 wif, writer, temp_path, output_path, original_fixed_size)
993 writer.SetInputConnection(wif.GetOutputPort())
995 def next_animation_frame(self):
997 ani = self._animation
998 if not ani:
999 return
1001 if self._animation_iframe is not None:
1002 state = ani(
1003 ani.tmin
1004 + self._animation_iframe * ani.dt)
1006 self._animation_iframe += 1
1007 else:
1008 tnow = time.time()
1009 state = ani(min(
1010 ani.tmax,
1011 ani.tmin + (tnow - self._animation_tstart)))
1013 self.set_state(state)
1014 self.renwin.Render()
1015 abort = False
1016 if self._animation_saver:
1017 abort = self.progressbars.set_status(
1018 'Rendering movie',
1019 100*self._animation_iframe*ani.dt / (ani.tmax - ani.tmin),
1020 can_abort=True)
1022 wif, writer, temp_path, _, _ = self._animation_saver
1023 wif.Modified()
1024 fn = os.path.join(temp_path, 'f%09i.png')
1025 writer.SetFileName(fn % self._animation_iframe)
1026 writer.Write()
1028 if self._animation_iframe is not None:
1029 t = self._animation_iframe * ani.dt
1030 else:
1031 t = tnow - self._animation_tstart
1033 if t > ani.tmax - ani.tmin or abort:
1034 self.stop_animation()
1036 def stop_animation(self):
1037 if self._animation_timer:
1038 self._animation_timer.stop()
1040 if self._animation_saver:
1042 wif, writer, temp_path, output_path, original_fixed_size \
1043 = self._animation_saver
1044 self.gui_state.fixed_size = original_fixed_size
1046 fn_path = os.path.join(temp_path, 'f%09d.png')
1047 check_call([
1048 'ffmpeg', '-y',
1049 '-i', fn_path,
1050 '-c:v', 'libx264',
1051 '-preset', 'slow',
1052 '-crf', '17',
1053 '-vf', 'format=yuv420p,fps=%i' % (
1054 int(round(1.0/self._animation.dt))),
1055 output_path])
1056 shutil.rmtree(temp_path)
1058 self._animation_saver = None
1059 self._animation_saver
1061 self.progressbars.set_status(
1062 'Rendering movie', 100, can_abort=True)
1064 self._animation_tstart = None
1065 self._animation_iframe = None
1066 self._animation = None
1067 self.enable_capture()
1069 def set_state(self, state):
1070 self.disable_capture()
1071 try:
1072 self._update_elements_enabled = False
1073 self.setUpdatesEnabled(False)
1074 self.state.diff_update(state)
1075 self.state.sort_elements()
1076 self.setUpdatesEnabled(True)
1077 self._update_elements_enabled = True
1078 self.update_elements()
1079 finally:
1080 self.enable_capture()
1082 def periodical(self):
1083 pass
1085 def check_vtk_resize(self, *args):
1086 render_window_size = self.renwin.GetSize()
1087 if self._render_window_size != render_window_size:
1088 self._render_window_size = render_window_size
1089 self.resize_event(*render_window_size)
1091 def update_elements(self, *_):
1092 if not self._update_elements_enabled:
1093 return
1095 if self._in_update_elements:
1096 return
1098 self._in_update_elements = True
1099 for estate in self.state.elements:
1100 if estate.element_id not in self._elements:
1101 new_element = estate.create()
1102 logger.debug('Creating "%s" ("%s").' % (
1103 type(new_element).__name__,
1104 estate.element_id))
1105 self._elements[estate.element_id] = new_element
1107 element = self._elements[estate.element_id]
1109 if estate.element_id not in self._elements_active:
1110 logger.debug('Adding "%s" ("%s")' % (
1111 type(element).__name__,
1112 estate.element_id))
1113 element.bind_state(estate)
1114 element.set_parent(self)
1115 self._elements_active[estate.element_id] = element
1117 state_element_ids = [el.element_id for el in self.state.elements]
1118 deactivate = []
1119 for element_id, element in self._elements_active.items():
1120 if element_id not in state_element_ids:
1121 logger.debug('Removing "%s" ("%s").' % (
1122 type(element).__name__,
1123 element_id))
1124 element.unset_parent()
1125 deactivate.append(element_id)
1127 for element_id in deactivate:
1128 del self._elements_active[element_id]
1130 self._update_crosshair_bindings()
1132 self._in_update_elements = False
1134 def _update_crosshair_bindings(self):
1136 def get_crosshair_element():
1137 for element in self.state.elements:
1138 if element.element_id == 'crosshair':
1139 return element
1141 return None
1143 crosshair = get_crosshair_element()
1144 if crosshair is None or crosshair.is_connected:
1145 return
1147 def to_checkbox(state, widget):
1148 widget.blockSignals(True)
1149 widget.setChecked(state.visible)
1150 widget.blockSignals(False)
1152 def to_state(widget, state):
1153 state.visible = widget.isChecked()
1155 cb = self._crosshair_checkbox
1156 vstate.state_bind(
1157 self, crosshair, ['visible'], to_state,
1158 cb, [cb.toggled], to_checkbox)
1160 crosshair.is_connected = True
1162 def add_actor_2d(self, actor):
1163 if actor not in self._actors_2d:
1164 self.ren.AddActor2D(actor)
1165 self._actors_2d.add(actor)
1167 def remove_actor_2d(self, actor):
1168 if actor in self._actors_2d:
1169 self.ren.RemoveActor2D(actor)
1170 self._actors_2d.remove(actor)
1172 def add_actor(self, actor):
1173 if actor not in self._actors:
1174 self.ren.AddActor(actor)
1175 self._actors.add(actor)
1177 def add_actor_list(self, actorlist):
1178 for actor in actorlist:
1179 self.add_actor(actor)
1181 def remove_actor(self, actor):
1182 if actor in self._actors:
1183 self.ren.RemoveActor(actor)
1184 self._actors.remove(actor)
1186 def update_view(self):
1187 self.vtk_widget.update()
1189 def resize_event(self, size_x, size_y):
1190 self.gui_state.size = (size_x, size_y)
1192 def button_event(self, obj, event):
1193 if event == "LeftButtonPressEvent":
1194 self.rotating = True
1195 elif event == "LeftButtonReleaseEvent":
1196 self.rotating = False
1198 def mouse_move_event(self, obj, event):
1199 x0, y0 = self.iren.GetLastEventPosition()
1200 x, y = self.iren.GetEventPosition()
1202 size_x, size_y = self.renwin.GetSize()
1203 center_x = size_x / 2.0
1204 center_y = size_y / 2.0
1206 if self.rotating:
1207 self.do_rotate(x, y, x0, y0, center_x, center_y)
1209 def myWheelEvent(self, event):
1211 angle = event.angleDelta().y()
1213 if angle > 200:
1214 angle = 200
1216 if angle < -200:
1217 angle = -200
1219 self.disable_capture()
1220 try:
1221 self.do_dolly(-angle/100.)
1222 finally:
1223 self.enable_capture(aggregate='distance')
1225 def do_rotate(self, x, y, x0, y0, center_x, center_y):
1227 dx = x0 - x
1228 dy = y0 - y
1230 phi = d2r*(self.state.strike - 90.)
1231 focp = self.gui_state.focal_point
1233 if focp == 'center':
1234 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1235 - math.sin(phi) * dx + math.cos(phi) * dy
1237 lat = self.state.lat
1238 lon = self.state.lon
1239 factor = self.state.distance / 10.0
1240 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1241 else:
1242 lat = 90. - self.state.dip
1243 lon = -self.state.strike - 90.
1244 factor = 0.5
1245 factor_lat = 1.0
1247 dlat = dy * factor
1248 dlon = dx * factor * factor_lat
1250 lat = max(min(lat + dlat, 90.), -90.)
1251 lon += dlon
1252 lon = (lon + 180.) % 360. - 180.
1254 if focp == 'center':
1255 self.state.lat = float(lat)
1256 self.state.lon = float(lon)
1257 else:
1258 self.state.dip = float(90. - lat)
1259 self.state.strike = float(((-(lon + 90.))+180.) % 360. - 180.)
1261 def do_dolly(self, v):
1262 self.state.distance *= float(1.0 + 0.1*v)
1264 def key_down_event(self, obj, event):
1265 k = obj.GetKeyCode()
1266 if k == 'f':
1267 self.gui_state.next_focal_point()
1269 elif k == 'r':
1270 self.reset_strike_dip()
1272 elif k == 'p':
1273 print(self.state)
1275 elif k == 'i':
1276 for elem in self.state.elements:
1277 if isinstance(elem, elements.IcosphereState):
1278 elem.visible = not elem.visible
1280 elif k == 'c':
1281 for elem in self.state.elements:
1282 if isinstance(elem, elements.CoastlinesState):
1283 elem.visible = not elem.visible
1285 elif k == 't':
1286 if not any(
1287 isinstance(elem, elements.TopoState)
1288 for elem in self.state.elements):
1290 self.state.elements.append(elements.TopoState())
1291 else:
1292 for elem in self.state.elements:
1293 if isinstance(elem, elements.TopoState):
1294 elem.visible = not elem.visible
1296 # elif k == ' ':
1297 # self.toggle_panel_visibility()
1299 def _state_bind(self, *args, **kwargs):
1300 vstate.state_bind(self, self.state, *args, **kwargs)
1302 def _gui_state_bind(self, *args, **kwargs):
1303 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1305 def controls_navigation(self):
1306 frame = qw.QFrame(self)
1307 frame.setSizePolicy(
1308 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1309 layout = qw.QGridLayout()
1310 frame.setLayout(layout)
1312 # lat, lon, depth
1314 layout.addWidget(
1315 qw.QLabel('Location'), 0, 0, 1, 2)
1317 le = qw.QLineEdit()
1318 le.setStatusTip(
1319 'Latitude, Longitude, Depth [km] or city name: '
1320 'Focal point location.')
1321 layout.addWidget(le, 1, 0, 1, 1)
1323 def lat_lon_depth_to_lineedit(state, widget):
1324 widget.setText('%g, %g, %g' % (
1325 state.lat, state.lon, state.depth / km))
1327 def lineedit_to_lat_lon_depth(widget, state):
1328 self.disable_capture()
1329 try:
1330 s = str(widget.text())
1331 choices = location_to_choices(s)
1332 if len(choices) > 0:
1333 self.state.lat, self.state.lon, self.state.depth = \
1334 choices[0].get_lat_lon_depth()
1335 else:
1336 raise NoLocationChoices(s)
1338 finally:
1339 self.enable_capture()
1341 self._state_bind(
1342 ['lat', 'lon', 'depth'],
1343 lineedit_to_lat_lon_depth,
1344 le, [le.editingFinished, le.returnPressed],
1345 lat_lon_depth_to_lineedit)
1347 self.lat_lon_lineedit = le
1349 # focal point
1351 cb = qw.QCheckBox('Fix')
1352 cb.setStatusTip(
1353 'Fix location. Orbit focal point without pressing %s.'
1354 % g_modifier_key)
1355 layout.addWidget(cb, 1, 1, 1, 1)
1357 def focal_point_to_checkbox(state, widget):
1358 widget.blockSignals(True)
1359 widget.setChecked(self.gui_state.focal_point != 'center')
1360 widget.blockSignals(False)
1362 def checkbox_to_focal_point(widget, state):
1363 self.gui_state.focal_point = \
1364 'target' if widget.isChecked() else 'center'
1366 self._gui_state_bind(
1367 ['focal_point'], checkbox_to_focal_point,
1368 cb, [cb.toggled], focal_point_to_checkbox)
1370 self.focal_point_checkbox = cb
1372 self.talkie_connect(
1373 self.gui_state, 'focal_point', self.update_focal_point)
1375 self.update_focal_point()
1377 # strike, dip
1379 layout.addWidget(
1380 qw.QLabel('View Plane'), 2, 0, 1, 2)
1382 le = qw.QLineEdit()
1383 le.setStatusTip(
1384 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1385 'direction.')
1386 layout.addWidget(le, 3, 0, 1, 1)
1388 def strike_dip_to_lineedit(state, widget):
1389 widget.setText('%g, %g' % (state.strike, state.dip))
1391 def lineedit_to_strike_dip(widget, state):
1392 s = str(widget.text())
1393 string_to_strike_dip = {
1394 'east': (0., 90.),
1395 'west': (180., 90.),
1396 'south': (90., 90.),
1397 'north': (270., 90.),
1398 'top': (90., 0.),
1399 'bottom': (90., 180.)}
1401 self.disable_capture()
1402 if s in string_to_strike_dip:
1403 state.strike, state.dip = string_to_strike_dip[s]
1405 s = s.replace(',', ' ')
1406 try:
1407 state.strike, state.dip = map(float, s.split())
1408 except Exception:
1409 raise ValueError('need two numerical values: <strike>, <dip>')
1410 finally:
1411 self.enable_capture()
1413 self._state_bind(
1414 ['strike', 'dip'], lineedit_to_strike_dip,
1415 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1417 self.strike_dip_lineedit = le
1419 but = qw.QPushButton('Reset')
1420 but.setStatusTip('Reset to north-up map view.')
1421 but.clicked.connect(self.reset_strike_dip)
1422 layout.addWidget(but, 3, 1, 1, 1)
1424 # crosshair
1426 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1427 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1429 # camera bindings
1430 self.talkie_connect(
1431 self.state,
1432 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1433 self.update_camera)
1435 self.talkie_connect(
1436 self.gui_state, 'panels_visible', self.update_panel_visibility)
1438 return frame
1440 def controls_time(self):
1441 frame = qw.QFrame(self)
1442 frame.setSizePolicy(
1443 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1445 layout = qw.QGridLayout()
1446 frame.setLayout(layout)
1448 layout.addWidget(qw.QLabel('Min'), 0, 0)
1449 le_tmin = qw.QLineEdit()
1450 layout.addWidget(le_tmin, 0, 1)
1452 layout.addWidget(qw.QLabel('Max'), 1, 0)
1453 le_tmax = qw.QLineEdit()
1454 layout.addWidget(le_tmax, 1, 1)
1456 label_tcursor = qw.QLabel()
1458 label_tcursor.setSizePolicy(
1459 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1461 layout.addWidget(label_tcursor, 2, 1)
1462 self._label_tcursor = label_tcursor
1464 self._state_bind(
1465 ['tmin'], common.lineedit_to_time, le_tmin,
1466 [le_tmin.editingFinished, le_tmin.returnPressed],
1467 common.time_to_lineedit,
1468 attribute='tmin')
1469 self._state_bind(
1470 ['tmax'], common.lineedit_to_time, le_tmax,
1471 [le_tmax.editingFinished, le_tmax.returnPressed],
1472 common.time_to_lineedit,
1473 attribute='tmax')
1475 self.tmin_lineedit = le_tmin
1476 self.tmax_lineedit = le_tmax
1478 range_edit = RangeEdit()
1479 range_edit.rangeEditPressed.connect(self.disable_capture)
1480 range_edit.rangeEditReleased.connect(self.enable_capture)
1481 range_edit.set_data_provider(self)
1482 range_edit.set_data_name('time')
1484 xblock = [False]
1486 def range_to_range_edit(state, widget):
1487 if not xblock[0]:
1488 widget.blockSignals(True)
1489 widget.set_focus(state.tduration, state.tposition)
1490 widget.set_range(state.tmin, state.tmax)
1491 widget.blockSignals(False)
1493 def range_edit_to_range(widget, state):
1494 xblock[0] = True
1495 self.state.tduration, self.state.tposition = widget.get_focus()
1496 self.state.tmin, self.state.tmax = widget.get_range()
1497 xblock[0] = False
1499 self._state_bind(
1500 ['tmin', 'tmax', 'tduration', 'tposition'],
1501 range_edit_to_range,
1502 range_edit,
1503 [range_edit.rangeChanged, range_edit.focusChanged],
1504 range_to_range_edit)
1506 def handle_tcursor_changed():
1507 self.gui_state.tcursor = range_edit.get_tcursor()
1509 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1511 layout.addWidget(range_edit, 3, 0, 1, 2)
1513 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1514 le_focus = qw.QLineEdit()
1515 layout.addWidget(le_focus, 4, 1)
1517 def focus_to_lineedit(state, widget):
1518 if state.tduration is None:
1519 widget.setText('')
1520 else:
1521 widget.setText('%s, %g' % (
1522 guts.str_duration(state.tduration),
1523 state.tposition))
1525 def lineedit_to_focus(widget, state):
1526 s = str(widget.text())
1527 w = [x.strip() for x in s.split(',')]
1528 try:
1529 if len(w) == 0 or not w[0]:
1530 state.tduration = None
1531 state.tposition = 0.0
1532 else:
1533 state.tduration = guts.parse_duration(w[0])
1534 if len(w) > 1:
1535 state.tposition = float(w[1])
1536 else:
1537 state.tposition = 0.0
1539 except Exception:
1540 raise ValueError('need two values: <duration>, <position>')
1542 self._state_bind(
1543 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1544 [le_focus.editingFinished, le_focus.returnPressed],
1545 focus_to_lineedit)
1547 label_effective_tmin = qw.QLabel()
1548 label_effective_tmax = qw.QLabel()
1550 label_effective_tmin.setSizePolicy(
1551 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1552 label_effective_tmax.setSizePolicy(
1553 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1554 label_effective_tmin.setMinimumSize(
1555 qg.QFontMetrics(label_effective_tmin.font()).width(
1556 '0000-00-00 00:00:00.000 '), 0)
1558 layout.addWidget(label_effective_tmin, 5, 1)
1559 layout.addWidget(label_effective_tmax, 6, 1)
1561 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1562 self.talkie_connect(
1563 self.state, var, self.update_effective_time_labels)
1565 self._label_effective_tmin = label_effective_tmin
1566 self._label_effective_tmax = label_effective_tmax
1568 self.talkie_connect(
1569 self.gui_state, 'tcursor', self.update_tcursor)
1571 return frame
1573 def controls_appearance(self):
1574 frame = qw.QFrame(self)
1575 frame.setSizePolicy(
1576 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1577 layout = qw.QGridLayout()
1578 frame.setLayout(layout)
1580 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1582 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1583 layout.addWidget(cb, 0, 1)
1584 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1586 self.talkie_connect(
1587 self.state, 'lighting', self.update_render_settings)
1589 # background
1591 layout.addWidget(qw.QLabel('Background'), 1, 0)
1593 cb = common.strings_to_combobox(
1594 ['black', 'white', 'skyblue1 - white'])
1596 layout.addWidget(cb, 1, 1)
1597 vstate.state_bind_combobox_background(
1598 self, self.state, 'background', cb)
1600 self.talkie_connect(
1601 self.state, 'background', self.update_render_settings)
1603 return frame
1605 def controls_snapshots(self):
1606 return snapshots_mod.SnapshotsPanel(self)
1608 def update_effective_time_labels(self, *args):
1609 tmin = self.state.tmin_effective
1610 tmax = self.state.tmax_effective
1612 stmin = common.time_or_none_to_str(tmin)
1613 stmax = common.time_or_none_to_str(tmax)
1615 self._label_effective_tmin.setText(stmin)
1616 self._label_effective_tmax.setText(stmax)
1618 def update_tcursor(self, *args):
1619 tcursor = self.gui_state.tcursor
1620 stcursor = common.time_or_none_to_str(tcursor)
1621 self._label_tcursor.setText(stcursor)
1623 def reset_strike_dip(self, *args):
1624 self.state.strike = 90.
1625 self.state.dip = 0
1626 self.gui_state.focal_point = 'center'
1628 def get_camera_geometry(self):
1630 def rtp2xyz(rtp):
1631 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1633 radius = 1.0 - self.state.depth / self.planet_radius
1635 cam_rtp = num.array([
1636 radius+self.state.distance,
1637 self.state.lat * d2r + 0.5*num.pi,
1638 self.state.lon * d2r])
1639 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1640 cam, up, foc = \
1641 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1643 foc_rtp = num.array([
1644 radius,
1645 self.state.lat * d2r + 0.5*num.pi,
1646 self.state.lon * d2r])
1648 foc = rtp2xyz(foc_rtp)
1650 rot_world = pmt.euler_to_matrix(
1651 -(self.state.lat-90.)*d2r,
1652 (self.state.lon+90.)*d2r,
1653 0.0*d2r).T
1655 rot_cam = pmt.euler_to_matrix(
1656 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1658 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1660 cam = foc + num.dot(rot, cam - foc)
1661 up = num.dot(rot, up)
1662 return cam, up, foc
1664 def update_camera(self, *args):
1665 cam, up, foc = self.get_camera_geometry()
1666 camera = self.ren.GetActiveCamera()
1667 camera.SetPosition(*cam)
1668 camera.SetFocalPoint(*foc)
1669 camera.SetViewUp(*up)
1671 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1673 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1674 self.feature_radius_min / self.planet_radius)**2))
1676 # if horizon == 0.0:
1677 # horizon = 2.0 + self.state.distance
1679 # clip_dist = max(min(self.state.distance*5., max(
1680 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1681 # , math.sqrt(num.sum(cam**2)))
1682 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1683 # clip_dist = feature_horizon
1685 camera.SetClippingRange(
1686 max(clip_dist*0.00001, clip_dist-3.0), clip_dist)
1688 self.camera_params = (
1689 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1691 self.update_view()
1693 def add_panel(
1694 self, title_label, panel,
1695 visible=False,
1696 # volatile=False,
1697 tabify=True,
1698 where=qc.Qt.RightDockWidgetArea,
1699 remove=None,
1700 title_controls=[],
1701 scrollable=True):
1703 dockwidget = common.MyDockWidget(
1704 self, title_label, title_controls=title_controls)
1706 if not visible:
1707 dockwidget.hide()
1709 if not self.gui_state.panels_visible:
1710 dockwidget.block()
1712 if scrollable:
1713 scrollarea = common.MyScrollArea()
1714 scrollarea.setWidget(panel)
1715 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
1716 scrollarea.setSizeAdjustPolicy(
1717 qw.QAbstractScrollArea.AdjustToContents)
1718 scrollarea.setFrameShape(qw.QFrame.NoFrame)
1720 dockwidget.setWidget(scrollarea)
1721 else:
1722 dockwidget.setWidget(panel)
1724 dockwidgets = self.findChildren(common.MyDockWidget)
1725 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1727 self.addDockWidget(where, dockwidget)
1729 nwrap = 4
1730 if dws and len(dws) >= nwrap and tabify:
1731 self.tabifyDockWidget(
1732 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1734 mitem = dockwidget.toggleViewAction()
1736 def update_label(*args):
1737 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1738 self.update_slug_abbreviated_lengths()
1740 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1741 dockwidget.titlebar._title_label.title_changed.connect(
1742 self.update_slug_abbreviated_lengths)
1744 update_label()
1746 self._panel_togglers[dockwidget] = mitem
1747 self.panels_menu.addAction(mitem)
1748 if visible:
1749 dockwidget.setVisible(True)
1750 dockwidget.setFocus()
1751 dockwidget.raise_()
1753 def stack_panels(self):
1754 dockwidgets = self.findChildren(common.MyDockWidget)
1755 by_area = defaultdict(list)
1756 for dw in dockwidgets:
1757 area = self.dockWidgetArea(dw)
1758 by_area[area].append(dw)
1760 for dockwidgets in by_area.values():
1761 dw_last = None
1762 for dw in dockwidgets:
1763 if dw_last is not None:
1764 self.tabifyDockWidget(dw_last, dw)
1766 dw_last = dw
1768 def update_slug_abbreviated_lengths(self):
1769 dockwidgets = self.findChildren(common.MyDockWidget)
1770 title_labels = []
1771 for dw in dockwidgets:
1772 title_labels.append(dw.titlebar._title_label)
1774 by_title = defaultdict(list)
1775 for tl in title_labels:
1776 by_title[tl.get_title()].append(tl)
1778 for group in by_title.values():
1779 slugs = [tl.get_slug() for tl in group]
1781 n = max(len(slug) for slug in slugs)
1782 nunique = len(set(slugs))
1784 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1785 n -= 1
1787 if n > 0:
1788 n = max(3, n)
1790 for tl in group:
1791 tl.set_slug_abbreviated_length(n)
1793 def get_dockwidget(self, panel):
1794 dockwidget = panel
1795 while not isinstance(dockwidget, qw.QDockWidget):
1796 dockwidget = dockwidget.parent()
1798 return dockwidget
1800 def raise_panel(self, panel):
1801 dockwidget = self.get_dockwidget(panel)
1802 dockwidget.setVisible(True)
1803 dockwidget.setFocus()
1804 dockwidget.raise_()
1806 def toggle_panel_visibility(self):
1807 self.gui_state.panels_visible = not self.gui_state.panels_visible
1809 def update_panel_visibility(self, *args):
1810 self.setUpdatesEnabled(False)
1811 mbar = self.menuBar()
1812 sbar = self.statusBar()
1813 dockwidgets = self.findChildren(common.MyDockWidget)
1815 # Set height to zero instead of hiding so that shortcuts still work
1816 # otherwise one would have to mess around with separate QShortcut
1817 # objects.
1818 mbar.setFixedHeight(
1819 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0)
1821 sbar.setVisible(self.gui_state.panels_visible)
1822 for dockwidget in dockwidgets:
1823 dockwidget.setBlocked(not self.gui_state.panels_visible)
1825 self.setUpdatesEnabled(True)
1827 def remove_panel(self, panel):
1828 dockwidget = self.get_dockwidget(panel)
1829 self.removeDockWidget(dockwidget)
1830 dockwidget.setParent(None)
1831 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1833 def register_data_provider(self, provider):
1834 if provider not in self.data_providers:
1835 self.data_providers.append(provider)
1837 def unregister_data_provider(self, provider):
1838 if provider in self.data_providers:
1839 self.data_providers.remove(provider)
1841 def iter_data(self, name):
1842 for provider in self.data_providers:
1843 for data in provider.iter_data(name):
1844 yield data
1846 def confirm_close(self):
1847 ret = qw.QMessageBox.question(
1848 self,
1849 'Sparrow',
1850 'Close Sparrow window?',
1851 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
1852 qw.QMessageBox.Ok)
1854 return ret == qw.QMessageBox.Ok
1856 def closeEvent(self, event):
1857 if self.instant_close or self.confirm_close():
1858 self.attach()
1859 self.closing = True
1860 event.accept()
1861 else:
1862 event.ignore()
1864 def is_closing(self):
1865 return self.closing
1868def main(*args, **kwargs):
1870 from pyrocko import util
1871 from pyrocko.gui import util as gui_util
1872 from . import common
1873 util.setup_logging('sparrow', 'info')
1875 global win
1877 app = gui_util.get_app()
1878 win = SparrowViewer(*args, **kwargs)
1879 app.set_main_window(win)
1881 gui_util.app.install_sigint_handler()
1883 try:
1884 gui_util.app.exec_()
1885 finally:
1886 gui_util.app.uninstall_sigint_handler()
1887 app.unset_main_window()
1888 common.set_viewer(None)
1889 del win
1890 gc.collect()