1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import signal
8import gc
9import logging
10import time
11import tempfile
12import os
13import shutil
14import platform
15from subprocess import check_call
17import numpy as num
19from pyrocko import cake
20from pyrocko import guts
21from pyrocko import geonames
22from pyrocko import moment_tensor as pmt
24from pyrocko.gui.util import Progressbars, RangeEdit
25from pyrocko.gui.talkie import TalkieConnectionOwner
26from pyrocko.gui.qt_compat import qw, qc, qg
27# from pyrocko.gui import vtk_util
29from . import common, light, snapshots as snapshots_mod
31import vtk
32import vtk.qt
33vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
35from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
37from pyrocko import geometry # noqa
38from . import state as vstate, elements # noqa
40logger = logging.getLogger('pyrocko.gui.sparrow.main')
43d2r = num.pi/180.
44km = 1000.
46if platform.uname()[0] == 'Darwin':
47 g_modifier_key = '\u2318'
48else:
49 g_modifier_key = 'Ctrl'
52class ZeroFrame(qw.QFrame):
54 def sizeHint(self):
55 return qc.QSize(0, 0)
58class LocationChoice(object):
59 def __init__(self, name, lat, lon, depth=0):
60 self._name = name
61 self._lat = lat
62 self._lon = lon
63 self._depth = depth
65 def get_lat_lon_depth(self):
66 return self._lat, self._lon, self._depth
69def location_to_choices(s):
70 choices = []
71 s_vals = s.replace(',', ' ')
72 try:
73 vals = [float(x) for x in s_vals.split()]
74 if len(vals) == 3:
75 vals[2] *= km
77 choices.append(LocationChoice('', *vals))
79 except ValueError:
80 cities = geonames.get_cities_by_name(s.strip())
81 for c in cities:
82 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
84 return choices
87class NoLocationChoices(Exception):
89 def __init__(self, s):
90 self._string = s
92 def __str__(self):
93 return 'No location choices for string "%s"' % self._string
96class QVTKWidget(QVTKRenderWindowInteractor):
97 def __init__(self, viewer, *args):
98 QVTKRenderWindowInteractor.__init__(self, *args)
99 self._viewer = viewer
101 def wheelEvent(self, event):
102 return self._viewer.myWheelEvent(event)
104 def container_resized(self, ev):
105 self._viewer.update_vtk_widget_size()
108class DetachedViewer(qw.QMainWindow):
110 def __init__(self, main_window, vtk_frame):
111 qw.QMainWindow.__init__(self, main_window)
112 self.main_window = main_window
113 self.setWindowTitle('Sparrow View')
114 vtk_frame.setParent(self)
115 self.setCentralWidget(vtk_frame)
117 def closeEvent(self, ev):
118 ev.ignore()
119 self.main_window.attach()
122class CenteringScrollArea(qw.QScrollArea):
123 def __init__(self):
124 qw.QScrollArea.__init__(self)
125 self.setAlignment(qc.Qt.AlignCenter)
126 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
127 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
128 self.setFrameShape(qw.QFrame.NoFrame)
130 def resizeEvent(self, ev):
131 retval = qw.QScrollArea.resizeEvent(self, ev)
132 self.widget().container_resized(ev)
133 return retval
135 def recenter(self):
136 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
137 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
139 def wheelEvent(self, *args, **kwargs):
140 return self.widget().wheelEvent(*args, **kwargs)
143class YAMLEditor(qw.QTextEdit):
145 def __init__(self, parent):
146 qw.QTextEdit.__init__(self)
147 self._parent = parent
149 def event(self, ev):
150 if isinstance(ev, qg.QKeyEvent) \
151 and ev.key() == qc.Qt.Key_Return \
152 and ev.modifiers() & qc.Qt.ShiftModifier:
153 self._parent.state_changed()
154 return True
156 return qw.QTextEdit.event(self, ev)
159class StateEditor(qw.QFrame, TalkieConnectionOwner):
160 def __init__(self, viewer, *args, **kwargs):
161 qw.QFrame.__init__(self, *args, **kwargs)
162 TalkieConnectionOwner.__init__(self)
164 layout = qw.QGridLayout()
166 self.setLayout(layout)
168 self.source_editor = YAMLEditor(self)
169 self.source_editor.setAcceptRichText(False)
170 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
171 font = qg.QFont("Monospace")
172 self.source_editor.setCurrentFont(font)
173 layout.addWidget(self.source_editor, 0, 0, 1, 2)
175 self.error_display_label = qw.QLabel('Error')
176 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
178 self.error_display = qw.QTextEdit()
179 self.error_display.setCurrentFont(font)
180 self.error_display.setReadOnly(True)
182 self.error_display.setSizePolicy(
183 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
185 self.error_display_label.hide()
186 self.error_display.hide()
188 layout.addWidget(self.error_display, 2, 0, 1, 2)
190 self.instant_updates = qw.QCheckBox('Instant Updates')
191 self.instant_updates.toggled.connect(self.state_changed)
192 layout.addWidget(self.instant_updates, 3, 0)
194 button = qw.QPushButton('Apply')
195 button.clicked.connect(self.state_changed)
196 layout.addWidget(button, 3, 1)
198 self.viewer = viewer
199 # recommended way, but resulted in a variable-width font being used:
200 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
201 self.bind_state()
202 self.source_editor.textChanged.connect(self.text_changed_handler)
203 self.destroyed.connect(self.unbind_state)
204 self.bind_state()
206 def bind_state(self, *args):
207 self.talkie_connect(self.viewer.state, '', self.update_state)
208 self.update_state()
210 def unbind_state(self):
211 self.talkie_disconnect_all()
213 def update_state(self, *args):
214 cursor = self.source_editor.textCursor()
216 cursor_position = cursor.position()
217 vsb_position = self.source_editor.verticalScrollBar().value()
218 hsb_position = self.source_editor.horizontalScrollBar().value()
220 self.source_editor.setPlainText(str(self.viewer.state))
222 cursor.setPosition(cursor_position)
223 self.source_editor.setTextCursor(cursor)
224 self.source_editor.verticalScrollBar().setValue(vsb_position)
225 self.source_editor.horizontalScrollBar().setValue(hsb_position)
227 def text_changed_handler(self, *args):
228 if self.instant_updates.isChecked():
229 self.state_changed()
231 def state_changed(self):
232 try:
233 s = self.source_editor.toPlainText()
234 state = guts.load(string=s)
235 self.viewer.set_state(state)
236 self.error_display.setPlainText('')
237 self.error_display_label.hide()
238 self.error_display.hide()
240 except Exception as e:
241 self.error_display.show()
242 self.error_display_label.show()
243 self.error_display.setPlainText(str(e))
246class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
247 def __init__(self, use_depth_peeling=True, events=None, snapshots=None):
248 qw.QMainWindow.__init__(self)
249 TalkieConnectionOwner.__init__(self)
251 common.get_app().set_main_window(self)
253 self.state = vstate.ViewerState()
254 self.gui_state = vstate.ViewerGuiState()
256 self.setWindowTitle('Sparrow')
258 self.setTabPosition(
259 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
261 self.planet_radius = cake.earthradius
262 self.feature_radius_min = cake.earthradius - 1000. * km
264 self._panel_togglers = {}
265 self._actors = set()
266 self._actors_2d = set()
267 self._render_window_size = (0, 0)
268 self._use_depth_peeling = use_depth_peeling
269 self._in_update_elements = False
271 mbar = qw.QMenuBar()
272 self.setMenuBar(mbar)
274 menu = mbar.addMenu('File')
276 menu.addAction(
277 'Export Image...',
278 self.export_image,
279 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
280 qc.Qt.ApplicationShortcut)
282 menu.addAction(
283 'Quit',
284 self.request_quit,
285 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
286 qc.Qt.ApplicationShortcut)
288 menu = mbar.addMenu('View')
289 menu_sizes = menu.addMenu('Size')
290 self._add_vtk_widget_size_menu_entries(menu_sizes)
292 # detached/attached
293 self.talkie_connect(
294 self.gui_state, 'detached', self.update_detached)
296 action = qw.QAction('Detach')
297 action.setCheckable(True)
298 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
299 action.setShortcutContext(qc.Qt.ApplicationShortcut)
301 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
302 menu.addAction(action)
304 self.panels_menu = mbar.addMenu('Panels')
306 menu = mbar.addMenu('Add')
307 for name, estate in [
308 ('Icosphere', elements.IcosphereState(
309 level=4,
310 smooth=True,
311 opacity=0.5,
312 ambient=0.1)),
313 ('Grid', elements.GridState()),
314 ('Stations', elements.StationsState()),
315 ('Topography', elements.TopoState()),
316 ('Custom Topography', elements.CustomTopoState()),
317 ('Catalog', elements.CatalogState()),
318 ('Coastlines', elements.CoastlinesState()),
319 ('Source', elements.SourceState()),
320 ('HUD (tmax)', elements.HudState(
321 variables=['tmax'],
322 template='tmax: {0|date}',
323 position='top-left')),
324 ('HUD subtitle', elements.HudState(
325 template='Awesome')),
326 ('Volcanoes', elements.VolcanoesState()),
327 ('Faults', elements.ActiveFaultsState()),
328 ('Plate bounds', elements.PlatesBoundsState()),
329 ('InSAR Surface Displacements', elements.KiteState()),
330 ('Geometry', elements.GeometryState()),
331 ('Spheroid', elements.SpheroidState()),
332 ('Rays', elements.RaysState())]:
334 def wrap_add_element(estate):
335 def add_element(*args):
336 new_element = guts.clone(estate)
337 new_element.element_id = elements.random_id()
338 self.state.elements.append(new_element)
339 self.state.sort_elements()
341 return add_element
343 mitem = qw.QAction(name, self)
345 mitem.triggered.connect(wrap_add_element(estate))
347 menu.addAction(mitem)
349 self.data_providers = []
350 self.elements = {}
352 self.detached_window = None
354 self.main_frame = qw.QFrame()
355 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
357 self.vtk_frame = CenteringScrollArea()
359 self.vtk_widget = QVTKWidget(self, self)
360 self.vtk_frame.setWidget(self.vtk_widget)
362 self.main_layout = qw.QVBoxLayout()
363 self.main_layout.setContentsMargins(0, 0, 0, 0)
364 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
366 pb = Progressbars(self)
367 self.progressbars = pb
368 self.main_layout.addWidget(pb)
370 self.main_frame.setLayout(self.main_layout)
372 self.vtk_frame_substitute = None
374 self.add_panel(
375 'Navigation',
376 self.controls_navigation(), visible=True,
377 where=qc.Qt.LeftDockWidgetArea)
379 self.add_panel(
380 'Time',
381 self.controls_time(), visible=True,
382 where=qc.Qt.LeftDockWidgetArea)
384 self.add_panel(
385 'Appearance',
386 self.controls_appearance(), visible=True,
387 where=qc.Qt.LeftDockWidgetArea)
389 snapshots_panel = self.controls_snapshots()
390 self.add_panel(
391 'Snapshots',
392 snapshots_panel, visible=False,
393 where=qc.Qt.LeftDockWidgetArea)
395 self.setCentralWidget(self.main_frame)
397 self.mesh = None
399 ren = vtk.vtkRenderer()
401 # ren.SetBackground(0.15, 0.15, 0.15)
402 # ren.SetBackground(0.0, 0.0, 0.0)
403 # ren.TwoSidedLightingOn()
404 # ren.SetUseShadows(1)
406 self._lighting = None
407 self._background = None
409 self.ren = ren
410 self.update_render_settings()
411 self.update_camera()
413 renwin = self.vtk_widget.GetRenderWindow()
415 if self._use_depth_peeling:
416 renwin.SetAlphaBitPlanes(1)
417 renwin.SetMultiSamples(0)
419 ren.SetUseDepthPeeling(1)
420 ren.SetMaximumNumberOfPeels(100)
421 ren.SetOcclusionRatio(0.1)
423 ren.SetUseFXAA(1)
424 # ren.SetUseHiddenLineRemoval(1)
425 # ren.SetBackingStore(1)
427 self.renwin = renwin
429 # renwin.LineSmoothingOn()
430 # renwin.PointSmoothingOn()
431 # renwin.PolygonSmoothingOn()
433 renwin.AddRenderer(ren)
435 iren = renwin.GetInteractor()
436 iren.LightFollowCameraOn()
437 iren.SetInteractorStyle(None)
439 iren.AddObserver('LeftButtonPressEvent', self.button_event)
440 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
441 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
442 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
443 iren.AddObserver('RightButtonPressEvent', self.button_event)
444 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
445 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
446 iren.AddObserver('KeyPressEvent', self.key_down_event)
447 iren.AddObserver('KeyReleaseEvent', self.key_up_event)
448 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
450 renwin.Render()
452 iren.Initialize()
454 self.iren = iren
456 self.rotating = False
458 self._elements = {}
459 self._elements_active = {}
461 self.talkie_connect(
462 self.state, 'elements', self.update_elements)
464 self.state.elements.append(elements.IcosphereState(
465 element_id='icosphere',
466 level=4,
467 smooth=True,
468 opacity=0.5,
469 ambient=0.1))
471 self.state.elements.append(elements.GridState(
472 element_id='grid'))
473 self.state.elements.append(elements.CoastlinesState(
474 element_id='coastlines'))
475 self.state.elements.append(elements.CrosshairState(
476 element_id='crosshair'))
478 # self.state.elements.append(elements.StationsState())
479 # self.state.elements.append(elements.SourceState())
480 # self.state.elements.append(
481 # elements.CatalogState(
482 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
483 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
485 if events:
486 self.state.elements.append(
487 elements.CatalogState(
488 selection=elements.MemoryCatalogSelection(events=events)))
490 self.state.sort_elements()
492 if snapshots:
493 snapshots_ = []
494 for obj in snapshots:
495 if isinstance(obj, str):
496 snapshots_.extend(snapshots_mod.load_snapshots(obj))
497 else:
498 snapshots.append(obj)
500 snapshots_panel.add_snapshots(snapshots_)
501 self.raise_panel(snapshots_panel)
502 snapshots_panel.goto_snapshot(1)
504 self.timer = qc.QTimer(self)
505 self.timer.timeout.connect(self.periodical)
506 self.timer.setInterval(1000)
507 self.timer.start()
509 self._animation_saver = None
511 self.closing = False
512 self.vtk_widget.setFocus()
514 self.update_detached()
516 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
517 common.get_app().status('Let\'s fly.', 2.0)
519 self.show()
520 self.windowHandle().showMaximized()
522 self.talkie_connect(
523 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
525 self.update_vtk_widget_size()
527 def _add_vtk_widget_size_menu_entries(self, menu):
529 group = qw.QActionGroup(menu)
530 group.setExclusive(True)
532 def set_variable_size():
533 self.gui_state.fixed_size = False
535 variable_size_action = menu.addAction('Fit Window Size')
536 variable_size_action.setCheckable(True)
537 variable_size_action.setActionGroup(group)
538 variable_size_action.triggered.connect(set_variable_size)
540 fixed_size_items = []
541 for nx, ny, label in [
542 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
543 (426, 240, ''),
544 (640, 360, ''),
545 (854, 480, '(FWVGA)'),
546 (1280, 720, '(HD)'),
547 (1920, 1080, '(Full HD)'),
548 (2560, 1440, '(Quad HD)'),
549 (3840, 2160, '(4K UHD)'),
550 (3840*2, 2160*2, '',),
551 (None, None, 'Aspect 4:3'),
552 (640, 480, '(VGA)'),
553 (800, 600, '(SVGA)'),
554 (None, None, 'Other'),
555 (512, 512, ''),
556 (1024, 1024, '')]:
558 if None in (nx, ny):
559 menu.addSection(label)
560 else:
561 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
562 action = menu.addAction(name)
563 action.setCheckable(True)
564 action.setActionGroup(group)
565 fixed_size_items.append((action, (nx, ny)))
567 def make_set_fixed_size(nx, ny):
568 def set_fixed_size():
569 self.gui_state.fixed_size = (float(nx), float(ny))
571 return set_fixed_size
573 action.triggered.connect(make_set_fixed_size(nx, ny))
575 def update_widget(*args):
576 for action, (nx, ny) in fixed_size_items:
577 action.blockSignals(True)
578 action.setChecked(
579 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
580 int(z) for z in self.gui_state.fixed_size)))
581 action.blockSignals(False)
583 variable_size_action.blockSignals(True)
584 variable_size_action.setChecked(not self.gui_state.fixed_size)
585 variable_size_action.blockSignals(False)
587 update_widget()
588 self.talkie_connect(
589 self.gui_state, 'fixed_size', update_widget)
591 def update_vtk_widget_size(self, *args):
592 if self.gui_state.fixed_size:
593 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
594 wanted_size = qc.QSize(nx, ny)
595 else:
596 wanted_size = qc.QSize(
597 self.vtk_frame.window().width(), self.vtk_frame.height())
599 current_size = self.vtk_widget.size()
601 if current_size.width() != wanted_size.width() \
602 or current_size.height() != wanted_size.height():
604 self.vtk_widget.setFixedSize(wanted_size)
606 self.vtk_frame.recenter()
607 self.check_vtk_resize()
609 def update_focal_point(self, *args):
610 if self.gui_state.focal_point == 'center':
611 self.vtk_widget.setStatusTip(
612 'Click and drag: change location. %s-click and drag: '
613 'change view plane orientation.' % g_modifier_key)
614 else:
615 self.vtk_widget.setStatusTip(
616 '%s-click and drag: change location. Click and drag: '
617 'change view plane orientation. Uncheck "Navigation: Fix" to '
618 'reverse sense.' % g_modifier_key)
620 def update_detached(self, *args):
622 if self.gui_state.detached and not self.detached_window: # detach
623 logger.debug('Detaching VTK view.')
625 self.main_layout.removeWidget(self.vtk_frame)
626 self.detached_window = DetachedViewer(self, self.vtk_frame)
627 self.detached_window.show()
628 self.vtk_widget.setFocus()
630 screens = common.get_app().screens()
631 if len(screens) > 1:
632 for screen in screens:
633 if screen is not self.screen():
634 self.detached_window.windowHandle().setScreen(screen)
635 # .setScreen() does not work reliably,
636 # therefore trying also with .move()...
637 p = screen.geometry().topLeft()
638 self.detached_window.move(p.x() + 50, p.y() + 50)
639 # ... but also does not work in notion window manager.
641 self.detached_window.windowHandle().showMaximized()
643 frame = qw.QFrame()
644 # frame.setFrameShape(qw.QFrame.NoFrame)
645 # frame.setBackgroundRole(qg.QPalette.Mid)
646 # frame.setAutoFillBackground(True)
647 frame.setSizePolicy(
648 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
650 layout = qw.QGridLayout()
651 frame.setLayout(layout)
652 self.main_layout.insertWidget(0, frame)
654 self.state_editor = StateEditor(self)
656 layout.addWidget(self.state_editor, 0, 0)
658 # attach_button = qw.QPushButton('Attach View')
659 # attach_button.clicked.connect(self.attach)
660 # layout.addWidget(
661 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
663 self.vtk_frame_substitute = frame
665 if not self.gui_state.detached and self.detached_window: # attach
666 logger.debug('Attaching VTK view.')
667 self.detached_window.hide()
668 self.vtk_frame.setParent(self)
669 if self.vtk_frame_substitute:
670 self.main_layout.removeWidget(self.vtk_frame_substitute)
671 self.state_editor.unbind_state()
672 self.vtk_frame_substitute = None
674 self.main_layout.insertWidget(0, self.vtk_frame)
675 self.detached_window = None
676 self.vtk_widget.setFocus()
678 def attach(self):
679 self.gui_state.detached = False
681 def export_image(self):
683 caption = 'Export Image'
684 fn_out, _ = qw.QFileDialog.getSaveFileName(
685 self, caption, 'image.png',
686 options=common.qfiledialog_options)
688 if fn_out:
689 self.save_image(fn_out)
691 def save_image(self, path):
693 original_fixed_size = self.gui_state.fixed_size
694 if original_fixed_size is None:
695 self.gui_state.fixed_size = (1920., 1080.)
697 wif = vtk.vtkWindowToImageFilter()
698 wif.SetInput(self.renwin)
699 wif.SetInputBufferTypeToRGBA()
700 wif.ReadFrontBufferOff()
701 writer = vtk.vtkPNGWriter()
702 writer.SetInputConnection(wif.GetOutputPort())
704 self.renwin.Render()
705 wif.Modified()
706 writer.SetFileName(path)
707 writer.Write()
709 self.vtk_widget.setFixedSize(
710 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
712 self.gui_state.fixed_size = original_fixed_size
714 def update_render_settings(self, *args):
715 if self._lighting is None or self._lighting != self.state.lighting:
716 self.ren.RemoveAllLights()
717 for li in light.get_lights(self.state.lighting):
718 self.ren.AddLight(li)
720 self._lighting = self.state.lighting
722 if self._background is None \
723 or self._background != self.state.background:
725 self.state.background.vtk_apply(self.ren)
726 self._background = self.state.background
728 self.update_view()
730 def start_animation(self, interpolator, output_path=None):
731 self._animation = interpolator
732 if output_path is None:
733 self._animation_tstart = time.time()
734 self._animation_iframe = None
735 else:
736 self._animation_iframe = 0
737 self.showFullScreen()
738 self.update_view()
739 self.gui_state.panels_visible = False
740 self.update_view()
742 self._animation_timer = qc.QTimer(self)
743 self._animation_timer.timeout.connect(self.next_animation_frame)
744 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
745 self._animation_timer.start()
746 if output_path is not None:
747 self.vtk_widget.setFixedSize(qc.QSize(1920, 1080))
748 # self.vtk_widget.setFixedSize(qc.QSize(960, 540))
750 wif = vtk.vtkWindowToImageFilter()
751 wif.SetInput(self.renwin)
752 wif.SetInputBufferTypeToRGBA()
753 wif.SetScale(1, 1)
754 wif.ReadFrontBufferOff()
755 writer = vtk.vtkPNGWriter()
756 temp_path = tempfile.mkdtemp()
757 self._animation_saver = (wif, writer, temp_path, output_path)
758 writer.SetInputConnection(wif.GetOutputPort())
760 def next_animation_frame(self):
762 ani = self._animation
763 if not ani:
764 return
766 if self._animation_iframe is not None:
767 state = ani(
768 ani.tmin
769 + self._animation_iframe * ani.dt)
771 self._animation_iframe += 1
772 else:
773 tnow = time.time()
774 state = ani(min(
775 ani.tmax,
776 ani.tmin + (tnow - self._animation_tstart)))
778 self.set_state(state)
779 self.renwin.Render()
780 if self._animation_saver:
781 wif, writer, temp_path, _ = self._animation_saver
782 wif.Modified()
783 fn = os.path.join(temp_path, 'f%09i.png')
784 writer.SetFileName(fn % self._animation_iframe)
785 writer.Write()
787 if self._animation_iframe is not None:
788 t = self._animation_iframe * ani.dt
789 else:
790 t = tnow - self._animation_tstart
792 if t > ani.tmax - ani.tmin:
793 self.stop_animation()
795 def stop_animation(self):
796 if self._animation_timer:
797 self._animation_timer.stop()
799 if self._animation_saver:
800 self.vtk_widget.setFixedSize(
801 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
803 wif, writer, temp_path, output_path = self._animation_saver
804 fn_path = os.path.join(temp_path, 'f%09d.png')
805 check_call([
806 'ffmpeg', '-y',
807 '-i', fn_path,
808 '-c:v', 'libx264',
809 '-preset', 'slow',
810 '-crf', '17',
811 '-vf', 'format=yuv420p,fps=%i' % (
812 int(round(1.0/self._animation.dt))),
813 output_path])
814 shutil.rmtree(temp_path)
816 self._animation_saver = None
817 self._animation_saver
819 self.showNormal()
820 self.gui_state.panels_visible = True
822 self._animation_tstart = None
823 self._animation_iframe = None
824 self._animation = None
826 def set_state(self, state):
827 self.setUpdatesEnabled(False)
828 self.state.diff_update(state)
829 self.setUpdatesEnabled(True)
831 def periodical(self):
832 pass
834 def request_quit(self):
835 app = common.get_app()
836 app.myQuit()
838 def check_vtk_resize(self, *args):
839 render_window_size = self.renwin.GetSize()
840 if self._render_window_size != render_window_size:
841 self._render_window_size = render_window_size
842 self.resize_event(*render_window_size)
844 def update_elements(self, path, value):
845 if self._in_update_elements:
846 return
848 self._in_update_elements = True
849 for estate in self.state.elements:
850 if estate.element_id not in self._elements:
851 new_element = estate.create()
852 logger.debug('Creating "%s".' % type(new_element).__name__)
853 self._elements[estate.element_id] = new_element
855 element = self._elements[estate.element_id]
857 if estate.element_id not in self._elements_active:
858 logger.debug('Adding "%s".' % type(element).__name__)
859 element.bind_state(estate)
860 element.set_parent(self)
861 self._elements_active[estate.element_id] = element
863 state_element_ids = [el.element_id for el in self.state.elements]
864 deactivate = []
865 for element_id, element in self._elements_active.items():
866 if element_id not in state_element_ids:
867 logger.debug('Removing "%s".' % type(element).__name__)
868 element.unset_parent()
869 deactivate.append(element_id)
871 for element_id in deactivate:
872 del self._elements_active[element_id]
874 self._update_crosshair_bindings()
876 self._in_update_elements = False
878 def _update_crosshair_bindings(self):
880 def get_crosshair_element():
881 for element in self.state.elements:
882 if element.element_id == 'crosshair':
883 return element
885 return None
887 crosshair = get_crosshair_element()
888 if crosshair is None or crosshair.is_connected:
889 return
891 def to_checkbox(state, widget):
892 widget.blockSignals(True)
893 widget.setChecked(state.visible)
894 widget.blockSignals(False)
896 def to_state(widget, state):
897 state.visible = widget.isChecked()
899 cb = self._crosshair_checkbox
900 vstate.state_bind(
901 self, crosshair, ['visible'], to_state,
902 cb, [cb.toggled], to_checkbox)
904 crosshair.is_connected = True
906 def add_actor_2d(self, actor):
907 if actor not in self._actors_2d:
908 self.ren.AddActor2D(actor)
909 self._actors_2d.add(actor)
911 def remove_actor_2d(self, actor):
912 if actor in self._actors_2d:
913 self.ren.RemoveActor2D(actor)
914 self._actors_2d.remove(actor)
916 def add_actor(self, actor):
917 if actor not in self._actors:
918 self.ren.AddActor(actor)
919 self._actors.add(actor)
921 def add_actor_list(self, actorlist):
922 for actor in actorlist:
923 self.add_actor(actor)
925 def remove_actor(self, actor):
926 if actor in self._actors:
927 self.ren.RemoveActor(actor)
928 self._actors.remove(actor)
930 def update_view(self):
931 self.vtk_widget.update()
933 def resize_event(self, size_x, size_y):
934 self.gui_state.size = (size_x, size_y)
936 def button_event(self, obj, event):
937 if event == "LeftButtonPressEvent":
938 self.rotating = True
939 elif event == "LeftButtonReleaseEvent":
940 self.rotating = False
942 def mouse_move_event(self, obj, event):
943 x0, y0 = self.iren.GetLastEventPosition()
944 x, y = self.iren.GetEventPosition()
946 size_x, size_y = self.renwin.GetSize()
947 center_x = size_x / 2.0
948 center_y = size_y / 2.0
950 if self.rotating:
951 self.do_rotate(x, y, x0, y0, center_x, center_y)
953 def myWheelEvent(self, event):
955 angle = event.angleDelta().y()
957 if angle > 200:
958 angle = 200
960 if angle < -200:
961 angle = -200
963 self.do_dolly(-angle/100.)
965 def do_rotate(self, x, y, x0, y0, center_x, center_y):
967 dx = x0 - x
968 dy = y0 - y
970 phi = d2r*(self.state.strike - 90.)
971 focp = self.gui_state.focal_point
973 if focp == 'center':
974 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
975 - math.sin(phi) * dx + math.cos(phi) * dy
977 lat = self.state.lat
978 lon = self.state.lon
979 factor = self.state.distance / 10.0
980 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
981 else:
982 lat = 90. - self.state.dip
983 lon = -self.state.strike - 90.
984 factor = 0.5
985 factor_lat = 1.0
987 dlat = dy * factor
988 dlon = dx * factor * factor_lat
990 lat = max(min(lat + dlat, 90.), -90.)
991 lon += dlon
992 lon = (lon + 180.) % 360. - 180.
994 if focp == 'center':
995 self.state.lat = float(lat)
996 self.state.lon = float(lon)
997 else:
998 self.state.dip = float(90. - lat)
999 self.state.strike = float(-(lon + 90.))
1001 def do_dolly(self, v):
1002 self.state.distance *= float(1.0 + 0.1*v)
1004 def key_down_event(self, obj, event):
1005 k = obj.GetKeyCode()
1006 s = obj.GetKeySym()
1007 if k == 'f' or s == 'Control_L':
1008 self.gui_state.next_focal_point()
1010 elif k == 'r':
1011 self.reset_strike_dip()
1013 elif k == 'p':
1014 print(self.state)
1016 elif k == 'i':
1017 for elem in self.state.elements:
1018 if isinstance(elem, elements.IcosphereState):
1019 elem.visible = not elem.visible
1021 elif k == 'c':
1022 for elem in self.state.elements:
1023 if isinstance(elem, elements.CoastlinesState):
1024 elem.visible = not elem.visible
1026 elif k == 't':
1027 if not any(
1028 isinstance(elem, elements.TopoState)
1029 for elem in self.state.elements):
1031 self.state.elements.append(elements.TopoState())
1032 else:
1033 for elem in self.state.elements:
1034 if isinstance(elem, elements.TopoState):
1035 elem.visible = not elem.visible
1037 elif k == ' ':
1038 self.toggle_panel_visibility()
1040 def key_up_event(self, obj, event):
1041 s = obj.GetKeySym()
1042 if s == 'Control_L':
1043 self.gui_state.next_focal_point()
1045 def _state_bind(self, *args, **kwargs):
1046 vstate.state_bind(self, self.state, *args, **kwargs)
1048 def _gui_state_bind(self, *args, **kwargs):
1049 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1051 def controls_navigation(self):
1052 frame = qw.QFrame(self)
1053 frame.setSizePolicy(
1054 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1055 layout = qw.QGridLayout()
1056 frame.setLayout(layout)
1058 # lat, lon, depth
1060 layout.addWidget(
1061 qw.QLabel('Location'), 0, 0, 1, 2)
1063 le = qw.QLineEdit()
1064 le.setStatusTip(
1065 'Latitude, Longitude, Depth [km] or city name: '
1066 'Focal point location.')
1067 layout.addWidget(le, 1, 0, 1, 1)
1069 def lat_lon_depth_to_lineedit(state, widget):
1070 sel = str(widget.selectedText()) == str(widget.text())
1071 widget.setText('%g, %g, %g' % (
1072 state.lat, state.lon, state.depth / km))
1074 if sel:
1075 widget.selectAll()
1077 def lineedit_to_lat_lon_depth(widget, state):
1078 s = str(widget.text())
1079 choices = location_to_choices(s)
1080 if len(choices) > 0:
1081 self.state.lat, self.state.lon, self.state.depth = \
1082 choices[0].get_lat_lon_depth()
1083 else:
1084 raise NoLocationChoices(s)
1086 self._state_bind(
1087 ['lat', 'lon', 'depth'],
1088 lineedit_to_lat_lon_depth,
1089 le, [le.editingFinished, le.returnPressed],
1090 lat_lon_depth_to_lineedit)
1092 self.lat_lon_lineedit = le
1094 self.lat_lon_lineedit.returnPressed.connect(
1095 lambda *args: self.lat_lon_lineedit.selectAll())
1097 # focal point
1099 cb = qw.QCheckBox('Fix')
1100 cb.setStatusTip(
1101 'Fix location. Orbit focal point without pressing %s.'
1102 % g_modifier_key)
1103 layout.addWidget(cb, 1, 1, 1, 1)
1105 def focal_point_to_checkbox(state, widget):
1106 widget.blockSignals(True)
1107 widget.setChecked(self.gui_state.focal_point != 'center')
1108 widget.blockSignals(False)
1110 def checkbox_to_focal_point(widget, state):
1111 self.gui_state.focal_point = \
1112 'target' if widget.isChecked() else 'center'
1114 self._gui_state_bind(
1115 ['focal_point'], checkbox_to_focal_point,
1116 cb, [cb.toggled], focal_point_to_checkbox)
1118 self.focal_point_checkbox = cb
1120 self.talkie_connect(
1121 self.gui_state, 'focal_point', self.update_focal_point)
1123 self.update_focal_point()
1125 # strike, dip
1127 layout.addWidget(
1128 qw.QLabel('View Plane'), 2, 0, 1, 2)
1130 le = qw.QLineEdit()
1131 le.setStatusTip(
1132 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1133 'direction.')
1134 layout.addWidget(le, 3, 0, 1, 1)
1136 def strike_dip_to_lineedit(state, widget):
1137 sel = widget.selectedText() == widget.text()
1138 widget.setText('%g, %g' % (state.strike, state.dip))
1139 if sel:
1140 widget.selectAll()
1142 def lineedit_to_strike_dip(widget, state):
1143 s = str(widget.text())
1144 string_to_strike_dip = {
1145 'east': (0., 90.),
1146 'west': (180., 90.),
1147 'south': (90., 90.),
1148 'north': (270., 90.),
1149 'top': (90., 0.),
1150 'bottom': (90., 180.)}
1152 if s in string_to_strike_dip:
1153 state.strike, state.dip = string_to_strike_dip[s]
1155 s = s.replace(',', ' ')
1156 try:
1157 state.strike, state.dip = map(float, s.split())
1158 except Exception:
1159 raise ValueError('need two numerical values: <strike>, <dip>')
1161 self._state_bind(
1162 ['strike', 'dip'], lineedit_to_strike_dip,
1163 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1165 self.strike_dip_lineedit = le
1166 self.strike_dip_lineedit.returnPressed.connect(
1167 lambda *args: self.strike_dip_lineedit.selectAll())
1169 but = qw.QPushButton('Reset')
1170 but.setStatusTip('Reset to north-up map view.')
1171 but.clicked.connect(self.reset_strike_dip)
1172 layout.addWidget(but, 3, 1, 1, 1)
1174 # crosshair
1176 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1177 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1179 # camera bindings
1180 self.talkie_connect(
1181 self.state,
1182 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1183 self.update_camera)
1185 self.talkie_connect(
1186 self.gui_state, 'panels_visible', self.update_panel_visibility)
1188 return frame
1190 def controls_time(self):
1191 frame = qw.QFrame(self)
1192 frame.setSizePolicy(
1193 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1195 layout = qw.QGridLayout()
1196 frame.setLayout(layout)
1198 layout.addWidget(qw.QLabel('Min'), 0, 0)
1199 le_tmin = qw.QLineEdit()
1200 layout.addWidget(le_tmin, 0, 1)
1202 layout.addWidget(qw.QLabel('Max'), 1, 0)
1203 le_tmax = qw.QLineEdit()
1204 layout.addWidget(le_tmax, 1, 1)
1206 label_tcursor = qw.QLabel()
1208 label_tcursor.setSizePolicy(
1209 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1211 layout.addWidget(label_tcursor, 2, 1)
1212 self._label_tcursor = label_tcursor
1214 def time_to_lineedit(state, attribute, widget):
1215 sel = widget.selectedText() == widget.text() \
1216 and widget.text() != ''
1218 widget.setText(
1219 common.time_or_none_to_str(getattr(state, attribute)))
1221 if sel:
1222 widget.selectAll()
1224 def lineedit_to_time(widget, state, attribute):
1225 from pyrocko.util import str_to_time_fillup
1227 s = str(widget.text())
1228 if not s.strip():
1229 setattr(state, attribute, None)
1230 else:
1231 try:
1232 setattr(state, attribute, str_to_time_fillup(s))
1233 except Exception:
1234 raise ValueError(
1235 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1237 self._state_bind(
1238 ['tmin'], lineedit_to_time, le_tmin,
1239 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1240 attribute='tmin')
1241 self._state_bind(
1242 ['tmax'], lineedit_to_time, le_tmax,
1243 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1244 attribute='tmax')
1246 self.tmin_lineedit = le_tmin
1247 self.tmax_lineedit = le_tmax
1249 range_edit = RangeEdit()
1250 range_edit.set_data_provider(self)
1251 range_edit.set_data_name('time')
1253 xblock = [False]
1255 def range_to_range_edit(state, widget):
1256 if not xblock[0]:
1257 widget.blockSignals(True)
1258 widget.set_focus(state.tduration, state.tposition)
1259 widget.set_range(state.tmin, state.tmax)
1260 widget.blockSignals(False)
1262 def range_edit_to_range(widget, state):
1263 xblock[0] = True
1264 self.state.tduration, self.state.tposition = widget.get_focus()
1265 self.state.tmin, self.state.tmax = widget.get_range()
1266 xblock[0] = False
1268 self._state_bind(
1269 ['tmin', 'tmax', 'tduration', 'tposition'],
1270 range_edit_to_range,
1271 range_edit,
1272 [range_edit.rangeChanged, range_edit.focusChanged],
1273 range_to_range_edit)
1275 def handle_tcursor_changed():
1276 self.gui_state.tcursor = range_edit.get_tcursor()
1278 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1280 layout.addWidget(range_edit, 3, 0, 1, 2)
1282 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1283 le_focus = qw.QLineEdit()
1284 layout.addWidget(le_focus, 4, 1)
1286 def focus_to_lineedit(state, widget):
1287 sel = widget.selectedText() == widget.text() \
1288 and widget.text() != ''
1290 if state.tduration is None:
1291 widget.setText('')
1292 else:
1293 widget.setText('%s, %g' % (
1294 guts.str_duration(state.tduration),
1295 state.tposition))
1297 if sel:
1298 widget.selectAll()
1300 def lineedit_to_focus(widget, state):
1301 s = str(widget.text())
1302 w = [x.strip() for x in s.split(',')]
1303 try:
1304 if len(w) == 0 or not w[0]:
1305 state.tduration = None
1306 state.tposition = 0.0
1307 else:
1308 state.tduration = guts.parse_duration(w[0])
1309 if len(w) > 1:
1310 state.tposition = float(w[1])
1311 else:
1312 state.tposition = 0.0
1314 except Exception:
1315 raise ValueError('need two values: <duration>, <position>')
1317 self._state_bind(
1318 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1319 [le_focus.editingFinished, le_focus.returnPressed],
1320 focus_to_lineedit)
1322 label_effective_tmin = qw.QLabel()
1323 label_effective_tmax = qw.QLabel()
1325 label_effective_tmin.setSizePolicy(
1326 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1327 label_effective_tmax.setSizePolicy(
1328 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1329 label_effective_tmin.setMinimumSize(
1330 qg.QFontMetrics(label_effective_tmin.font()).width(
1331 '0000-00-00 00:00:00.000 '), 0)
1333 layout.addWidget(label_effective_tmin, 5, 1)
1334 layout.addWidget(label_effective_tmax, 6, 1)
1336 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1337 self.talkie_connect(
1338 self.state, var, self.update_effective_time_labels)
1340 self._label_effective_tmin = label_effective_tmin
1341 self._label_effective_tmax = label_effective_tmax
1343 self.talkie_connect(
1344 self.gui_state, 'tcursor', self.update_tcursor)
1346 return frame
1348 def controls_appearance(self):
1349 frame = qw.QFrame(self)
1350 frame.setSizePolicy(
1351 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1352 layout = qw.QGridLayout()
1353 frame.setLayout(layout)
1355 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1357 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1358 layout.addWidget(cb, 0, 1)
1359 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1361 self.talkie_connect(
1362 self.state, 'lighting', self.update_render_settings)
1364 # background
1366 layout.addWidget(qw.QLabel('Background'), 1, 0)
1368 cb = common.strings_to_combobox(
1369 ['black', 'white', 'skyblue1 - white'])
1371 layout.addWidget(cb, 1, 1)
1372 vstate.state_bind_combobox_background(
1373 self, self.state, 'background', cb)
1375 self.talkie_connect(
1376 self.state, 'background', self.update_render_settings)
1378 return frame
1380 def controls_snapshots(self):
1381 return snapshots_mod.SnapshotsPanel(self)
1383 def update_effective_time_labels(self, *args):
1384 tmin = self.state.tmin_effective
1385 tmax = self.state.tmax_effective
1387 stmin = common.time_or_none_to_str(tmin)
1388 stmax = common.time_or_none_to_str(tmax)
1390 self._label_effective_tmin.setText(stmin)
1391 self._label_effective_tmax.setText(stmax)
1393 def update_tcursor(self, *args):
1394 tcursor = self.gui_state.tcursor
1395 stcursor = common.time_or_none_to_str(tcursor)
1396 self._label_tcursor.setText(stcursor)
1398 def reset_strike_dip(self, *args):
1399 self.state.strike = 90.
1400 self.state.dip = 0
1401 self.gui_state.focal_point = 'center'
1403 def get_camera_geometry(self):
1405 def rtp2xyz(rtp):
1406 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1408 radius = 1.0 - self.state.depth / self.planet_radius
1410 cam_rtp = num.array([
1411 radius+self.state.distance,
1412 self.state.lat * d2r + 0.5*num.pi,
1413 self.state.lon * d2r])
1414 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1415 cam, up, foc = \
1416 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1418 foc_rtp = num.array([
1419 radius,
1420 self.state.lat * d2r + 0.5*num.pi,
1421 self.state.lon * d2r])
1423 foc = rtp2xyz(foc_rtp)
1425 rot_world = pmt.euler_to_matrix(
1426 -(self.state.lat-90.)*d2r,
1427 (self.state.lon+90.)*d2r,
1428 0.0*d2r).T
1430 rot_cam = pmt.euler_to_matrix(
1431 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1433 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1435 cam = foc + num.dot(rot, cam - foc)
1436 up = num.dot(rot, up)
1437 return cam, up, foc
1439 def update_camera(self, *args):
1440 cam, up, foc = self.get_camera_geometry()
1441 camera = self.ren.GetActiveCamera()
1442 camera.SetPosition(*cam)
1443 camera.SetFocalPoint(*foc)
1444 camera.SetViewUp(*up)
1446 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1448 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1449 self.feature_radius_min / self.planet_radius)**2))
1451 # if horizon == 0.0:
1452 # horizon = 2.0 + self.state.distance
1454 # clip_dist = max(min(self.state.distance*5., max(
1455 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1456 # , math.sqrt(num.sum(cam**2)))
1457 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1458 # clip_dist = feature_horizon
1460 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1462 self.camera_params = (
1463 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1465 self.update_view()
1467 def add_panel(
1468 self, name, panel,
1469 visible=False,
1470 # volatile=False,
1471 tabify=True,
1472 where=qc.Qt.RightDockWidgetArea,
1473 remove=None,
1474 title_controls=[]):
1476 dockwidget = common.MyDockWidget(
1477 name, self, title_controls=title_controls)
1479 if not visible:
1480 dockwidget.hide()
1482 if not self.gui_state.panels_visible:
1483 dockwidget.block()
1485 dockwidget.setWidget(panel)
1487 panel.setParent(dockwidget)
1489 dockwidgets = self.findChildren(common.MyDockWidget)
1490 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1492 self.addDockWidget(where, dockwidget)
1494 nwrap = 4
1495 if dws and len(dws) >= nwrap and tabify:
1496 self.tabifyDockWidget(
1497 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1499 mitem = dockwidget.toggleViewAction()
1500 self._panel_togglers[dockwidget] = mitem
1501 self.panels_menu.addAction(mitem)
1502 if visible:
1503 dockwidget.setVisible(True)
1504 dockwidget.setFocus()
1505 dockwidget.raise_()
1507 def raise_panel(self, panel):
1508 dockwidget = panel.parent()
1509 dockwidget.setVisible(True)
1510 dockwidget.setFocus()
1511 dockwidget.raise_()
1513 def toggle_panel_visibility(self):
1514 self.gui_state.panels_visible = not self.gui_state.panels_visible
1516 def update_panel_visibility(self, *args):
1517 self.setUpdatesEnabled(False)
1518 mbar = self.menuBar()
1519 dockwidgets = self.findChildren(common.MyDockWidget)
1521 mbar.setVisible(self.gui_state.panels_visible)
1522 for dockwidget in dockwidgets:
1523 dockwidget.setBlocked(not self.gui_state.panels_visible)
1525 self.setUpdatesEnabled(True)
1527 def remove_panel(self, panel):
1528 dockwidget = panel.parent()
1529 self.removeDockWidget(dockwidget)
1530 dockwidget.setParent(None)
1531 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1533 def register_data_provider(self, provider):
1534 if provider not in self.data_providers:
1535 self.data_providers.append(provider)
1537 def unregister_data_provider(self, provider):
1538 if provider in self.data_providers:
1539 self.data_providers.remove(provider)
1541 def iter_data(self, name):
1542 for provider in self.data_providers:
1543 for data in provider.iter_data(name):
1544 yield data
1546 def closeEvent(self, event):
1547 self.attach()
1548 event.accept()
1549 self.closing = True
1550 common.get_app().set_main_window(None)
1552 def is_closing(self):
1553 return self.closing
1556class SparrowApp(qw.QApplication):
1557 def __init__(self):
1558 qw.QApplication.__init__(self, ['Sparrow'])
1559 self.lastWindowClosed.connect(self.myQuit)
1560 self._main_window = None
1561 self.setApplicationDisplayName('Sparrow')
1562 self.setDesktopFileName('Sparrow')
1564 def install_sigint_handler(self):
1565 self._old_signal_handler = signal.signal(
1566 signal.SIGINT, self.myCloseAllWindows)
1568 def uninstall_sigint_handler(self):
1569 signal.signal(signal.SIGINT, self._old_signal_handler)
1571 def myQuit(self, *args):
1572 self.quit()
1574 def myCloseAllWindows(self, *args):
1575 self.closeAllWindows()
1577 def set_main_window(self, win):
1578 self._main_window = win
1580 def get_main_window(self):
1581 return self._main_window
1583 def get_progressbars(self):
1584 if self._main_window:
1585 return self._main_window.progressbars
1586 else:
1587 return None
1589 def status(self, message, duration=None):
1590 win = self.get_main_window()
1591 if not win:
1592 return
1594 win.statusBar().showMessage(
1595 message, int((duration or 0) * 1000))
1598def main(*args, **kwargs):
1600 from pyrocko import util
1601 from pyrocko.gui import util as gui_util
1602 util.setup_logging('sparrow', 'info')
1604 global win
1606 if gui_util.app is None:
1607 gui_util.app = SparrowApp()
1609 # try:
1610 # from qt_material import apply_stylesheet
1611 #
1612 # apply_stylesheet(app, theme='dark_teal.xml')
1613 #
1614 #
1615 # import qdarkgraystyle
1616 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1617 # import qdarkstyle
1618 #
1619 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1620 #
1621 #
1622 # except ImportError:
1623 # logger.info(
1624 # 'Module qdarkgraystyle not available.\n'
1625 # 'If wanted, install qdarkstyle with "pip install '
1626 # 'qdarkgraystyle".')
1627 #
1628 win = SparrowViewer(*args, **kwargs)
1630 gui_util.app.install_sigint_handler()
1631 gui_util.app.exec_()
1632 gui_util.app.uninstall_sigint_handler()
1634 del win
1636 gc.collect()
1638 del gui_util.app