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.qt_compat import qw, qc, qg
26# from pyrocko.gui import vtk_util
28from . import common, light, snapshots as snapshots_mod
30import vtk
31import vtk.qt
32vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
34from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
36from pyrocko import geometry # noqa
37from . import state as vstate, elements # noqa
39logger = logging.getLogger('pyrocko.gui.sparrow.main')
42d2r = num.pi/180.
43km = 1000.
45if platform.uname()[0] == 'Darwin':
46 g_modifier_key = '\u2318'
47else:
48 g_modifier_key = 'Ctrl'
51class ZeroFrame(qw.QFrame):
53 def sizeHint(self):
54 return qc.QSize(0, 0)
57class LocationChoice(object):
58 def __init__(self, name, lat, lon, depth=0):
59 self._name = name
60 self._lat = lat
61 self._lon = lon
62 self._depth = depth
64 def get_lat_lon_depth(self):
65 return self._lat, self._lon, self._depth
68def location_to_choices(s):
69 choices = []
70 s_vals = s.replace(',', ' ')
71 try:
72 vals = [float(x) for x in s_vals.split()]
73 if len(vals) == 3:
74 vals[2] *= km
76 choices.append(LocationChoice('', *vals))
78 except ValueError:
79 cities = geonames.get_cities_by_name(s.strip())
80 for c in cities:
81 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
83 return choices
86class NoLocationChoices(Exception):
88 def __init__(self, s):
89 self._string = s
91 def __str__(self):
92 return 'No location choices for string "%s"' % self._string
95class QVTKWidget(QVTKRenderWindowInteractor):
96 def __init__(self, viewer, *args):
97 QVTKRenderWindowInteractor.__init__(self, *args)
98 self._viewer = viewer
100 def wheelEvent(self, event):
101 return self._viewer.myWheelEvent(event)
103 def container_resized(self, ev):
104 self._viewer.update_vtk_widget_size()
107class DetachedViewer(qw.QMainWindow):
109 def __init__(self, main_window, vtk_frame):
110 qw.QMainWindow.__init__(self, main_window)
111 self.main_window = main_window
112 self.setWindowTitle('Sparrow View')
113 vtk_frame.setParent(self)
114 self.setCentralWidget(vtk_frame)
116 def closeEvent(self, ev):
117 ev.ignore()
118 self.main_window.attach()
121class CenteringScrollArea(qw.QScrollArea):
122 def __init__(self):
123 qw.QScrollArea.__init__(self)
124 self.setAlignment(qc.Qt.AlignCenter)
125 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
126 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
127 self.setFrameShape(qw.QFrame.NoFrame)
129 def resizeEvent(self, ev):
130 retval = qw.QScrollArea.resizeEvent(self, ev)
131 self.widget().container_resized(ev)
132 return retval
134 def recenter(self):
135 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
136 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
138 def wheelEvent(self, *args, **kwargs):
139 return self.widget().wheelEvent(*args, **kwargs)
142class YAMLEditor(qw.QTextEdit):
144 def __init__(self, parent):
145 qw.QTextEdit.__init__(self)
146 self._parent = parent
148 def event(self, ev):
149 if isinstance(ev, qg.QKeyEvent) \
150 and ev.key() == qc.Qt.Key_Return \
151 and ev.modifiers() & qc.Qt.ShiftModifier:
152 self._parent.state_changed()
153 return True
155 return qw.QTextEdit.event(self, ev)
158class StateEditor(qw.QFrame):
159 def __init__(self, viewer, *args, **kwargs):
160 qw.QFrame.__init__(self, *args, **kwargs)
161 self.listeners = []
163 layout = qw.QGridLayout()
165 self.setLayout(layout)
167 self.source_editor = YAMLEditor(self)
168 self.source_editor.setAcceptRichText(False)
169 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
170 font = qg.QFont("Monospace")
171 self.source_editor.setCurrentFont(font)
172 layout.addWidget(self.source_editor, 0, 0, 1, 2)
174 self.error_display_label = qw.QLabel('Error')
175 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
177 self.error_display = qw.QTextEdit()
178 self.error_display.setCurrentFont(font)
179 self.error_display.setReadOnly(True)
181 self.error_display.setSizePolicy(
182 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
184 self.error_display_label.hide()
185 self.error_display.hide()
187 layout.addWidget(self.error_display, 2, 0, 1, 2)
189 self.instant_updates = qw.QCheckBox('Instant Updates')
190 self.instant_updates.toggled.connect(self.state_changed)
191 layout.addWidget(self.instant_updates, 3, 0)
193 button = qw.QPushButton('Apply')
194 button.clicked.connect(self.state_changed)
195 layout.addWidget(button, 3, 1)
197 self.viewer = viewer
198 # recommended way, but resulted in a variable-width font being used:
199 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
200 self.bind_state()
201 self.source_editor.textChanged.connect(self.text_changed_handler)
202 self.destroyed.connect(self.unbind_state)
203 self.bind_state()
205 def bind_state(self, *args):
206 ref = self.viewer.state.add_listener(self.update_state)
207 self.listeners.append(ref)
208 self.update_state()
210 def unbind_state(self):
211 while self.listeners:
212 listener = self.listeners.pop()
213 self.viewer.state.remove_listener(listener)
215 def update_state(self, *args):
216 cursor = self.source_editor.textCursor()
218 cursor_position = cursor.position()
219 vsb_position = self.source_editor.verticalScrollBar().value()
220 hsb_position = self.source_editor.horizontalScrollBar().value()
222 self.source_editor.setPlainText(str(self.viewer.state))
224 cursor.setPosition(cursor_position)
225 self.source_editor.setTextCursor(cursor)
226 self.source_editor.verticalScrollBar().setValue(vsb_position)
227 self.source_editor.horizontalScrollBar().setValue(hsb_position)
229 def text_changed_handler(self, *args):
230 if self.instant_updates.isChecked():
231 self.state_changed()
233 def state_changed(self):
234 try:
235 s = self.source_editor.toPlainText()
236 state = guts.load(string=s)
237 self.viewer.set_state(state)
238 self.error_display.setPlainText('')
239 self.error_display_label.hide()
240 self.error_display.hide()
242 except Exception as e:
243 self.error_display.show()
244 self.error_display_label.show()
245 self.error_display.setPlainText(str(e))
248class SparrowViewer(qw.QMainWindow):
249 def __init__(self, use_depth_peeling=True, events=None, snapshots=None):
250 qw.QMainWindow.__init__(self)
252 common.get_app().set_main_window(self)
254 self.listeners = []
256 self.state = vstate.ViewerState()
257 self.gui_state = vstate.ViewerGuiState()
259 self.setWindowTitle('Sparrow')
261 self.setTabPosition(
262 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
264 self.planet_radius = cake.earthradius
265 self.feature_radius_min = cake.earthradius - 1000. * km
267 self._panel_togglers = {}
268 self._actors = set()
269 self._actors_2d = set()
270 self._render_window_size = (0, 0)
271 self._use_depth_peeling = use_depth_peeling
272 self._in_update_elements = False
274 mbar = qw.QMenuBar()
275 self.setMenuBar(mbar)
277 menu = mbar.addMenu('File')
279 menu.addAction(
280 'Export Image...',
281 self.export_image,
282 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
283 qc.Qt.ApplicationShortcut)
285 menu.addAction(
286 'Quit',
287 self.request_quit,
288 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
289 qc.Qt.ApplicationShortcut)
291 menu = mbar.addMenu('View')
292 menu_sizes = menu.addMenu('Size')
293 self._add_vtk_widget_size_menu_entries(menu_sizes)
295 # detached/attached
296 self.register_state_listener3(
297 self.update_detached, self.gui_state, 'detached')
299 action = qw.QAction('Detach')
300 action.setCheckable(True)
301 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
302 action.setShortcutContext(qc.Qt.ApplicationShortcut)
304 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
305 menu.addAction(action)
307 self.panels_menu = mbar.addMenu('Panels')
309 menu = mbar.addMenu('Add')
310 for name, estate in [
311 ('Icosphere', elements.IcosphereState(
312 level=4,
313 smooth=True,
314 opacity=0.5,
315 ambient=0.1)),
316 ('Grid', elements.GridState()),
317 ('Stations', elements.StationsState()),
318 ('Topography', elements.TopoState()),
319 ('Custom Topography', elements.CustomTopoState()),
320 ('Catalog', elements.CatalogState()),
321 ('Coastlines', elements.CoastlinesState()),
322 ('Source', elements.SourceState()),
323 ('HUD (tmax)', elements.HudState(
324 variables=['tmax'],
325 template='tmax: {0|date}',
326 position='top-left')),
327 ('HUD subtitle', elements.HudState(
328 template='Awesome')),
329 ('Volcanoes', elements.VolcanoesState()),
330 ('Faults', elements.ActiveFaultsState()),
331 ('Plate bounds', elements.PlatesBoundsState()),
332 ('InSAR Surface Displacements', elements.KiteState()),
333 ('Geometry', elements.GeometryState()),
334 ('Spheroid', elements.SpheroidState()),
335 ('Rays', elements.RaysState())]:
337 def wrap_add_element(estate):
338 def add_element(*args):
339 new_element = guts.clone(estate)
340 new_element.element_id = elements.random_id()
341 self.state.elements.append(new_element)
342 self.state.sort_elements()
344 return add_element
346 mitem = qw.QAction(name, self)
348 mitem.triggered.connect(wrap_add_element(estate))
350 menu.addAction(mitem)
352 self.data_providers = []
353 self.elements = {}
355 self.detached_window = None
357 self.main_frame = qw.QFrame()
358 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
360 self.vtk_frame = CenteringScrollArea()
362 self.vtk_widget = QVTKWidget(self, self)
363 self.vtk_frame.setWidget(self.vtk_widget)
365 self.main_layout = qw.QVBoxLayout()
366 self.main_layout.setContentsMargins(0, 0, 0, 0)
367 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
369 pb = Progressbars(self)
370 self.progressbars = pb
371 self.main_layout.addWidget(pb)
373 self.main_frame.setLayout(self.main_layout)
375 self.vtk_frame_substitute = None
377 self.add_panel(
378 'Navigation',
379 self.controls_navigation(), visible=True,
380 where=qc.Qt.LeftDockWidgetArea)
382 self.add_panel(
383 'Time',
384 self.controls_time(), visible=True,
385 where=qc.Qt.LeftDockWidgetArea)
387 self.add_panel(
388 'Appearance',
389 self.controls_appearance(), visible=True,
390 where=qc.Qt.LeftDockWidgetArea)
392 snapshots_panel = self.controls_snapshots()
393 self.add_panel(
394 'Snapshots',
395 snapshots_panel, visible=False,
396 where=qc.Qt.LeftDockWidgetArea)
398 self.setCentralWidget(self.main_frame)
400 self.mesh = None
402 ren = vtk.vtkRenderer()
404 # ren.SetBackground(0.15, 0.15, 0.15)
405 # ren.SetBackground(0.0, 0.0, 0.0)
406 # ren.TwoSidedLightingOn()
407 # ren.SetUseShadows(1)
409 self._lighting = None
410 self._background = None
412 self.ren = ren
413 self.update_render_settings()
414 self.update_camera()
416 renwin = self.vtk_widget.GetRenderWindow()
418 if self._use_depth_peeling:
419 renwin.SetAlphaBitPlanes(1)
420 renwin.SetMultiSamples(0)
422 ren.SetUseDepthPeeling(1)
423 ren.SetMaximumNumberOfPeels(100)
424 ren.SetOcclusionRatio(0.1)
426 ren.SetUseFXAA(1)
427 # ren.SetUseHiddenLineRemoval(1)
428 # ren.SetBackingStore(1)
430 self.renwin = renwin
432 # renwin.LineSmoothingOn()
433 # renwin.PointSmoothingOn()
434 # renwin.PolygonSmoothingOn()
436 renwin.AddRenderer(ren)
438 iren = renwin.GetInteractor()
439 iren.LightFollowCameraOn()
440 iren.SetInteractorStyle(None)
442 iren.AddObserver('LeftButtonPressEvent', self.button_event)
443 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
444 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
445 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
446 iren.AddObserver('RightButtonPressEvent', self.button_event)
447 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
448 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
449 iren.AddObserver('KeyPressEvent', self.key_down_event)
450 iren.AddObserver('KeyReleaseEvent', self.key_up_event)
451 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
453 renwin.Render()
455 iren.Initialize()
457 self.iren = iren
459 self.rotating = False
461 self._elements = {}
462 self._elements_active = {}
464 self.register_state_listener3(
465 self.update_elements, self.state, 'elements')
467 self.state.elements.append(elements.IcosphereState(
468 element_id='icosphere',
469 level=4,
470 smooth=True,
471 opacity=0.5,
472 ambient=0.1))
474 self.state.elements.append(elements.GridState(
475 element_id='grid'))
476 self.state.elements.append(elements.CoastlinesState(
477 element_id='coastlines'))
478 self.state.elements.append(elements.CrosshairState(
479 element_id='crosshair'))
481 # self.state.elements.append(elements.StationsState())
482 # self.state.elements.append(elements.SourceState())
483 # self.state.elements.append(
484 # elements.CatalogState(
485 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
486 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
488 if events:
489 self.state.elements.append(
490 elements.CatalogState(
491 selection=elements.MemoryCatalogSelection(events=events)))
493 self.state.sort_elements()
495 if snapshots:
496 snapshots_ = []
497 for obj in snapshots:
498 if isinstance(obj, str):
499 snapshots_.extend(snapshots_mod.load_snapshots(obj))
500 else:
501 snapshots.append(obj)
503 snapshots_panel.add_snapshots(snapshots_)
504 self.raise_panel(snapshots_panel)
505 snapshots_panel.goto_snapshot(1)
507 self.timer = qc.QTimer(self)
508 self.timer.timeout.connect(self.periodical)
509 self.timer.setInterval(1000)
510 self.timer.start()
512 self._animation_saver = None
514 self.closing = False
515 self.vtk_widget.setFocus()
517 self.update_detached()
519 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
520 common.get_app().status('Let\'s fly.', 2.0)
522 self.show()
523 self.windowHandle().showMaximized()
525 self.register_state_listener3(
526 self.update_vtk_widget_size, self.gui_state, 'fixed_size')
528 self.update_vtk_widget_size()
530 def _add_vtk_widget_size_menu_entries(self, menu):
532 group = qw.QActionGroup(menu)
533 group.setExclusive(True)
535 def set_variable_size():
536 self.gui_state.fixed_size = False
538 variable_size_action = menu.addAction('Fit Window Size')
539 variable_size_action.setCheckable(True)
540 variable_size_action.setActionGroup(group)
541 variable_size_action.triggered.connect(set_variable_size)
543 fixed_size_items = []
544 for nx, ny, label in [
545 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
546 (426, 240, ''),
547 (640, 360, ''),
548 (854, 480, '(FWVGA)'),
549 (1280, 720, '(HD)'),
550 (1920, 1080, '(Full HD)'),
551 (2560, 1440, '(Quad HD)'),
552 (3840, 2160, '(4K UHD)'),
553 (3840*2, 2160*2, '',),
554 (None, None, 'Aspect 4:3'),
555 (640, 480, '(VGA)'),
556 (800, 600, '(SVGA)'),
557 (None, None, 'Other'),
558 (512, 512, ''),
559 (1024, 1024, '')]:
561 if None in (nx, ny):
562 menu.addSection(label)
563 else:
564 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
565 action = menu.addAction(name)
566 action.setCheckable(True)
567 action.setActionGroup(group)
568 fixed_size_items.append((action, (nx, ny)))
570 def make_set_fixed_size(nx, ny):
571 def set_fixed_size():
572 self.gui_state.fixed_size = (float(nx), float(ny))
574 return set_fixed_size
576 action.triggered.connect(make_set_fixed_size(nx, ny))
578 def update_widget(*args):
579 for action, (nx, ny) in fixed_size_items:
580 action.blockSignals(True)
581 action.setChecked(
582 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
583 int(z) for z in self.gui_state.fixed_size)))
584 action.blockSignals(False)
586 variable_size_action.blockSignals(True)
587 variable_size_action.setChecked(not self.gui_state.fixed_size)
588 variable_size_action.blockSignals(False)
590 update_widget()
591 self.register_state_listener3(
592 update_widget, self.gui_state, 'fixed_size')
594 def update_vtk_widget_size(self, *args):
595 if self.gui_state.fixed_size:
596 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
597 wanted_size = qc.QSize(nx, ny)
598 else:
599 wanted_size = qc.QSize(
600 self.vtk_frame.window().width(), self.vtk_frame.height())
602 current_size = self.vtk_widget.size()
604 if current_size.width() != wanted_size.width() \
605 or current_size.height() != wanted_size.height():
607 self.vtk_widget.setFixedSize(wanted_size)
609 self.vtk_frame.recenter()
610 self.check_vtk_resize()
612 def update_focal_point(self, *args):
613 if self.gui_state.focal_point == 'center':
614 self.vtk_widget.setStatusTip(
615 'Click and drag: change location. %s-click and drag: '
616 'change view plane orientation.' % g_modifier_key)
617 else:
618 self.vtk_widget.setStatusTip(
619 '%s-click and drag: change location. Click and drag: '
620 'change view plane orientation. Uncheck "Navigation: Fix" to '
621 'reverse sense.' % g_modifier_key)
623 def update_detached(self, *args):
625 if self.gui_state.detached and not self.detached_window: # detach
626 logger.debug('Detaching VTK view.')
628 self.main_layout.removeWidget(self.vtk_frame)
629 self.detached_window = DetachedViewer(self, self.vtk_frame)
630 self.detached_window.show()
631 self.vtk_widget.setFocus()
633 screens = common.get_app().screens()
634 if len(screens) > 1:
635 for screen in screens:
636 if screen is not self.screen():
637 self.detached_window.windowHandle().setScreen(screen)
638 # .setScreen() does not work reliably,
639 # therefore trying also with .move()...
640 p = screen.geometry().topLeft()
641 self.detached_window.move(p.x() + 50, p.y() + 50)
642 # ... but also does not work in notion window manager.
644 self.detached_window.windowHandle().showMaximized()
646 frame = qw.QFrame()
647 # frame.setFrameShape(qw.QFrame.NoFrame)
648 # frame.setBackgroundRole(qg.QPalette.Mid)
649 # frame.setAutoFillBackground(True)
650 frame.setSizePolicy(
651 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
653 layout = qw.QGridLayout()
654 frame.setLayout(layout)
655 self.main_layout.insertWidget(0, frame)
657 self.state_editor = StateEditor(self)
659 layout.addWidget(self.state_editor, 0, 0)
661 # attach_button = qw.QPushButton('Attach View')
662 # attach_button.clicked.connect(self.attach)
663 # layout.addWidget(
664 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
666 self.vtk_frame_substitute = frame
668 if not self.gui_state.detached and self.detached_window: # attach
669 logger.debug('Attaching VTK view.')
670 self.detached_window.hide()
671 self.vtk_frame.setParent(self)
672 if self.vtk_frame_substitute:
673 self.main_layout.removeWidget(self.vtk_frame_substitute)
674 self.state_editor.unbind_state()
675 self.vtk_frame_substitute = None
677 self.main_layout.insertWidget(0, self.vtk_frame)
678 self.detached_window = None
679 self.vtk_widget.setFocus()
681 def attach(self):
682 self.gui_state.detached = False
684 def export_image(self):
686 caption = 'Export Image'
687 fn_out, _ = qw.QFileDialog.getSaveFileName(
688 self, caption, 'image.png',
689 options=common.qfiledialog_options)
691 if fn_out:
692 self.save_image(fn_out)
694 def save_image(self, path):
696 original_fixed_size = self.gui_state.fixed_size
697 if original_fixed_size is None:
698 self.gui_state.fixed_size = (1920., 1080.)
700 wif = vtk.vtkWindowToImageFilter()
701 wif.SetInput(self.renwin)
702 wif.SetInputBufferTypeToRGBA()
703 wif.ReadFrontBufferOff()
704 writer = vtk.vtkPNGWriter()
705 writer.SetInputConnection(wif.GetOutputPort())
707 self.renwin.Render()
708 wif.Modified()
709 writer.SetFileName(path)
710 writer.Write()
712 self.vtk_widget.setFixedSize(
713 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
715 self.gui_state.fixed_size = original_fixed_size
717 def update_render_settings(self, *args):
718 if self._lighting is None or self._lighting != self.state.lighting:
719 self.ren.RemoveAllLights()
720 for li in light.get_lights(self.state.lighting):
721 self.ren.AddLight(li)
723 self._lighting = self.state.lighting
725 if self._background is None \
726 or self._background != self.state.background:
728 self.state.background.vtk_apply(self.ren)
729 self._background = self.state.background
731 self.update_view()
733 def start_animation(self, interpolator, output_path=None):
734 self._animation = interpolator
735 if output_path is None:
736 self._animation_tstart = time.time()
737 self._animation_iframe = None
738 else:
739 self._animation_iframe = 0
740 self.showFullScreen()
741 self.update_view()
742 self.gui_state.panels_visible = False
743 self.update_view()
745 self._animation_timer = qc.QTimer(self)
746 self._animation_timer.timeout.connect(self.next_animation_frame)
747 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
748 self._animation_timer.start()
749 if output_path is not None:
750 self.vtk_widget.setFixedSize(qc.QSize(1920, 1080))
751 # self.vtk_widget.setFixedSize(qc.QSize(960, 540))
753 wif = vtk.vtkWindowToImageFilter()
754 wif.SetInput(self.renwin)
755 wif.SetInputBufferTypeToRGBA()
756 wif.SetScale(1, 1)
757 wif.ReadFrontBufferOff()
758 writer = vtk.vtkPNGWriter()
759 temp_path = tempfile.mkdtemp()
760 self._animation_saver = (wif, writer, temp_path, output_path)
761 writer.SetInputConnection(wif.GetOutputPort())
763 def next_animation_frame(self):
765 ani = self._animation
766 if not ani:
767 return
769 if self._animation_iframe is not None:
770 state = ani(
771 ani.tmin
772 + self._animation_iframe * ani.dt)
774 self._animation_iframe += 1
775 else:
776 tnow = time.time()
777 state = ani(min(
778 ani.tmax,
779 ani.tmin + (tnow - self._animation_tstart)))
781 self.set_state(state)
782 self.renwin.Render()
783 if self._animation_saver:
784 wif, writer, temp_path, _ = self._animation_saver
785 wif.Modified()
786 fn = os.path.join(temp_path, 'f%09i.png')
787 writer.SetFileName(fn % self._animation_iframe)
788 writer.Write()
790 if self._animation_iframe is not None:
791 t = self._animation_iframe * ani.dt
792 else:
793 t = tnow - self._animation_tstart
795 if t > ani.tmax - ani.tmin:
796 self.stop_animation()
798 def stop_animation(self):
799 if self._animation_timer:
800 self._animation_timer.stop()
802 if self._animation_saver:
803 self.vtk_widget.setFixedSize(
804 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
806 wif, writer, temp_path, output_path = self._animation_saver
807 fn_path = os.path.join(temp_path, 'f%09d.png')
808 check_call([
809 'ffmpeg', '-y',
810 '-i', fn_path,
811 '-c:v', 'libx264',
812 '-preset', 'slow',
813 '-crf', '17',
814 '-vf', 'format=yuv420p,fps=%i' % (
815 int(round(1.0/self._animation.dt))),
816 output_path])
817 shutil.rmtree(temp_path)
819 self._animation_saver = None
820 self._animation_saver
822 self.showNormal()
823 self.gui_state.panels_visible = True
825 self._animation_tstart = None
826 self._animation_iframe = None
827 self._animation = None
829 def set_state(self, state):
830 self.setUpdatesEnabled(False)
831 self.state.diff_update(state)
832 self.setUpdatesEnabled(True)
834 def periodical(self):
835 pass
837 def request_quit(self):
838 app = common.get_app()
839 app.myQuit()
841 def check_vtk_resize(self, *args):
842 render_window_size = self.renwin.GetSize()
843 if self._render_window_size != render_window_size:
844 self._render_window_size = render_window_size
845 self.resize_event(*render_window_size)
847 def update_elements(self, path, value):
848 if self._in_update_elements:
849 return
851 self._in_update_elements = True
852 for estate in self.state.elements:
853 if estate.element_id not in self._elements:
854 new_element = estate.create()
855 logger.debug('Creating "%s".' % type(new_element).__name__)
856 self._elements[estate.element_id] = new_element
858 element = self._elements[estate.element_id]
860 if estate.element_id not in self._elements_active:
861 logger.debug('Adding "%s".' % type(element).__name__)
862 element.bind_state(estate)
863 element.set_parent(self)
864 self._elements_active[estate.element_id] = element
866 state_element_ids = [el.element_id for el in self.state.elements]
867 deactivate = []
868 for element_id, element in self._elements_active.items():
869 if element_id not in state_element_ids:
870 logger.debug('Removing "%s".' % type(element).__name__)
871 element.unset_parent()
872 deactivate.append(element_id)
874 for element_id in deactivate:
875 del self._elements_active[element_id]
877 self._update_crosshair_bindings()
879 self._in_update_elements = False
881 def _update_crosshair_bindings(self):
883 def get_crosshair_element():
884 for element in self.state.elements:
885 if element.element_id == 'crosshair':
886 return element
888 return None
890 crosshair = get_crosshair_element()
891 if crosshair is None or crosshair.is_connected:
892 return
894 def to_checkbox(state, widget):
895 widget.blockSignals(True)
896 widget.setChecked(state.visible)
897 widget.blockSignals(False)
899 def to_state(widget, state):
900 state.visible = widget.isChecked()
902 cb = self._crosshair_checkbox
903 vstate.state_bind(
904 self, crosshair, ['visible'], to_state,
905 cb, [cb.toggled], to_checkbox)
907 crosshair.is_connected = True
909 def add_actor_2d(self, actor):
910 if actor not in self._actors_2d:
911 self.ren.AddActor2D(actor)
912 self._actors_2d.add(actor)
914 def remove_actor_2d(self, actor):
915 if actor in self._actors_2d:
916 self.ren.RemoveActor2D(actor)
917 self._actors_2d.remove(actor)
919 def add_actor(self, actor):
920 if actor not in self._actors:
921 self.ren.AddActor(actor)
922 self._actors.add(actor)
924 def add_actor_list(self, actorlist):
925 for actor in actorlist:
926 self.add_actor(actor)
928 def remove_actor(self, actor):
929 if actor in self._actors:
930 self.ren.RemoveActor(actor)
931 self._actors.remove(actor)
933 def update_view(self):
934 self.vtk_widget.update()
936 def resize_event(self, size_x, size_y):
937 self.gui_state.size = (size_x, size_y)
939 def button_event(self, obj, event):
940 if event == "LeftButtonPressEvent":
941 self.rotating = True
942 elif event == "LeftButtonReleaseEvent":
943 self.rotating = False
945 def mouse_move_event(self, obj, event):
946 x0, y0 = self.iren.GetLastEventPosition()
947 x, y = self.iren.GetEventPosition()
949 size_x, size_y = self.renwin.GetSize()
950 center_x = size_x / 2.0
951 center_y = size_y / 2.0
953 if self.rotating:
954 self.do_rotate(x, y, x0, y0, center_x, center_y)
956 def myWheelEvent(self, event):
958 angle = event.angleDelta().y()
960 if angle > 200:
961 angle = 200
963 if angle < -200:
964 angle = -200
966 self.do_dolly(-angle/100.)
968 def do_rotate(self, x, y, x0, y0, center_x, center_y):
970 dx = x0 - x
971 dy = y0 - y
973 phi = d2r*(self.state.strike - 90.)
974 focp = self.gui_state.focal_point
976 if focp == 'center':
977 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
978 - math.sin(phi) * dx + math.cos(phi) * dy
980 lat = self.state.lat
981 lon = self.state.lon
982 factor = self.state.distance / 10.0
983 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
984 else:
985 lat = 90. - self.state.dip
986 lon = -self.state.strike - 90.
987 factor = 0.5
988 factor_lat = 1.0
990 dlat = dy * factor
991 dlon = dx * factor * factor_lat
993 lat = max(min(lat + dlat, 90.), -90.)
994 lon += dlon
995 lon = (lon + 180.) % 360. - 180.
997 if focp == 'center':
998 self.state.lat = float(lat)
999 self.state.lon = float(lon)
1000 else:
1001 self.state.dip = float(90. - lat)
1002 self.state.strike = float(-(lon + 90.))
1004 def do_dolly(self, v):
1005 self.state.distance *= float(1.0 + 0.1*v)
1007 def key_down_event(self, obj, event):
1008 k = obj.GetKeyCode()
1009 s = obj.GetKeySym()
1010 if k == 'f' or s == 'Control_L':
1011 self.gui_state.next_focal_point()
1013 elif k == 'r':
1014 self.reset_strike_dip()
1016 elif k == 'p':
1017 print(self.state)
1019 elif k == 'i':
1020 for elem in self.state.elements:
1021 if isinstance(elem, elements.IcosphereState):
1022 elem.visible = not elem.visible
1024 elif k == 'c':
1025 for elem in self.state.elements:
1026 if isinstance(elem, elements.CoastlinesState):
1027 elem.visible = not elem.visible
1029 elif k == 't':
1030 if not any(
1031 isinstance(elem, elements.TopoState)
1032 for elem in self.state.elements):
1034 self.state.elements.append(elements.TopoState())
1035 else:
1036 for elem in self.state.elements:
1037 if isinstance(elem, elements.TopoState):
1038 elem.visible = not elem.visible
1040 elif k == ' ':
1041 self.toggle_panel_visibility()
1043 def key_up_event(self, obj, event):
1044 s = obj.GetKeySym()
1045 if s == 'Control_L':
1046 self.gui_state.next_focal_point()
1048 def _state_bind(self, *args, **kwargs):
1049 vstate.state_bind(self, self.state, *args, **kwargs)
1051 def _gui_state_bind(self, *args, **kwargs):
1052 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1054 def controls_navigation(self):
1055 frame = qw.QFrame(self)
1056 frame.setSizePolicy(
1057 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1058 layout = qw.QGridLayout()
1059 frame.setLayout(layout)
1061 # lat, lon, depth
1063 layout.addWidget(
1064 qw.QLabel('Location'), 0, 0, 1, 2)
1066 le = qw.QLineEdit()
1067 le.setStatusTip(
1068 'Latitude, Longitude, Depth [km] or city name: '
1069 'Focal point location.')
1070 layout.addWidget(le, 1, 0, 1, 1)
1072 def lat_lon_depth_to_lineedit(state, widget):
1073 sel = str(widget.selectedText()) == str(widget.text())
1074 widget.setText('%g, %g, %g' % (
1075 state.lat, state.lon, state.depth / km))
1077 if sel:
1078 widget.selectAll()
1080 def lineedit_to_lat_lon_depth(widget, state):
1081 s = str(widget.text())
1082 choices = location_to_choices(s)
1083 if len(choices) > 0:
1084 self.state.lat, self.state.lon, self.state.depth = \
1085 choices[0].get_lat_lon_depth()
1086 else:
1087 raise NoLocationChoices(s)
1089 self._state_bind(
1090 ['lat', 'lon', 'depth'],
1091 lineedit_to_lat_lon_depth,
1092 le, [le.editingFinished, le.returnPressed],
1093 lat_lon_depth_to_lineedit)
1095 self.lat_lon_lineedit = le
1097 self.lat_lon_lineedit.returnPressed.connect(
1098 lambda *args: self.lat_lon_lineedit.selectAll())
1100 # focal point
1102 cb = qw.QCheckBox('Fix')
1103 cb.setStatusTip(
1104 'Fix location. Orbit focal point without pressing %s.'
1105 % g_modifier_key)
1106 layout.addWidget(cb, 1, 1, 1, 1)
1108 def focal_point_to_checkbox(state, widget):
1109 widget.blockSignals(True)
1110 widget.setChecked(self.gui_state.focal_point != 'center')
1111 widget.blockSignals(False)
1113 def checkbox_to_focal_point(widget, state):
1114 self.gui_state.focal_point = \
1115 'target' if widget.isChecked() else 'center'
1117 self._gui_state_bind(
1118 ['focal_point'], checkbox_to_focal_point,
1119 cb, [cb.toggled], focal_point_to_checkbox)
1121 self.focal_point_checkbox = cb
1123 self.register_state_listener3(
1124 self.update_focal_point, self.gui_state, 'focal_point')
1126 self.update_focal_point()
1128 # strike, dip
1130 layout.addWidget(
1131 qw.QLabel('View Plane'), 2, 0, 1, 2)
1133 le = qw.QLineEdit()
1134 le.setStatusTip(
1135 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1136 'direction.')
1137 layout.addWidget(le, 3, 0, 1, 1)
1139 def strike_dip_to_lineedit(state, widget):
1140 sel = widget.selectedText() == widget.text()
1141 widget.setText('%g, %g' % (state.strike, state.dip))
1142 if sel:
1143 widget.selectAll()
1145 def lineedit_to_strike_dip(widget, state):
1146 s = str(widget.text())
1147 string_to_strike_dip = {
1148 'east': (0., 90.),
1149 'west': (180., 90.),
1150 'south': (90., 90.),
1151 'north': (270., 90.),
1152 'top': (90., 0.),
1153 'bottom': (90., 180.)}
1155 if s in string_to_strike_dip:
1156 state.strike, state.dip = string_to_strike_dip[s]
1158 s = s.replace(',', ' ')
1159 try:
1160 state.strike, state.dip = map(float, s.split())
1161 except Exception:
1162 raise ValueError('need two numerical values: <strike>, <dip>')
1164 self._state_bind(
1165 ['strike', 'dip'], lineedit_to_strike_dip,
1166 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1168 self.strike_dip_lineedit = le
1169 self.strike_dip_lineedit.returnPressed.connect(
1170 lambda *args: self.strike_dip_lineedit.selectAll())
1172 but = qw.QPushButton('Reset')
1173 but.setStatusTip('Reset to north-up map view.')
1174 but.clicked.connect(self.reset_strike_dip)
1175 layout.addWidget(but, 3, 1, 1, 1)
1177 # crosshair
1179 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1180 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1182 # camera bindings
1183 for var in ['lat', 'lon', 'depth', 'strike', 'dip', 'distance']:
1184 self.register_state_listener3(self.update_camera, self.state, var)
1186 self.register_state_listener3(
1187 self.update_panel_visibility, self.gui_state, 'panels_visible')
1189 return frame
1191 def controls_time(self):
1192 frame = qw.QFrame(self)
1193 frame.setSizePolicy(
1194 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1196 layout = qw.QGridLayout()
1197 frame.setLayout(layout)
1199 layout.addWidget(qw.QLabel('Min'), 0, 0)
1200 le_tmin = qw.QLineEdit()
1201 layout.addWidget(le_tmin, 0, 1)
1203 layout.addWidget(qw.QLabel('Max'), 1, 0)
1204 le_tmax = qw.QLineEdit()
1205 layout.addWidget(le_tmax, 1, 1)
1207 label_tcursor = qw.QLabel()
1209 label_tcursor.setSizePolicy(
1210 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1212 layout.addWidget(label_tcursor, 2, 1)
1213 self._label_tcursor = label_tcursor
1215 def time_to_lineedit(state, attribute, widget):
1216 sel = widget.selectedText() == widget.text() \
1217 and widget.text() != ''
1219 widget.setText(
1220 common.time_or_none_to_str(getattr(state, attribute)))
1222 if sel:
1223 widget.selectAll()
1225 def lineedit_to_time(widget, state, attribute):
1226 from pyrocko.util import str_to_time_fillup
1228 s = str(widget.text())
1229 if not s.strip():
1230 setattr(state, attribute, None)
1231 else:
1232 try:
1233 setattr(state, attribute, str_to_time_fillup(s))
1234 except Exception:
1235 raise ValueError(
1236 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1238 self._state_bind(
1239 ['tmin'], lineedit_to_time, le_tmin,
1240 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1241 attribute='tmin')
1242 self._state_bind(
1243 ['tmax'], lineedit_to_time, le_tmax,
1244 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1245 attribute='tmax')
1247 self.tmin_lineedit = le_tmin
1248 self.tmax_lineedit = le_tmax
1250 range_edit = RangeEdit()
1251 range_edit.set_data_provider(self)
1252 range_edit.set_data_name('time')
1254 xblock = [False]
1256 def range_to_range_edit(state, widget):
1257 if not xblock[0]:
1258 widget.blockSignals(True)
1259 widget.set_focus(state.tduration, state.tposition)
1260 widget.set_range(state.tmin, state.tmax)
1261 widget.blockSignals(False)
1263 def range_edit_to_range(widget, state):
1264 xblock[0] = True
1265 self.state.tduration, self.state.tposition = widget.get_focus()
1266 self.state.tmin, self.state.tmax = widget.get_range()
1267 xblock[0] = False
1269 self._state_bind(
1270 ['tmin', 'tmax', 'tduration', 'tposition'],
1271 range_edit_to_range,
1272 range_edit,
1273 [range_edit.rangeChanged, range_edit.focusChanged],
1274 range_to_range_edit)
1276 def handle_tcursor_changed():
1277 self.gui_state.tcursor = range_edit.get_tcursor()
1279 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1281 layout.addWidget(range_edit, 3, 0, 1, 2)
1283 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1284 le_focus = qw.QLineEdit()
1285 layout.addWidget(le_focus, 4, 1)
1287 def focus_to_lineedit(state, widget):
1288 sel = widget.selectedText() == widget.text() \
1289 and widget.text() != ''
1291 if state.tduration is None:
1292 widget.setText('')
1293 else:
1294 widget.setText('%s, %g' % (
1295 guts.str_duration(state.tduration),
1296 state.tposition))
1298 if sel:
1299 widget.selectAll()
1301 def lineedit_to_focus(widget, state):
1302 s = str(widget.text())
1303 w = [x.strip() for x in s.split(',')]
1304 try:
1305 if len(w) == 0 or not w[0]:
1306 state.tduration = None
1307 state.tposition = 0.0
1308 else:
1309 state.tduration = guts.parse_duration(w[0])
1310 if len(w) > 1:
1311 state.tposition = float(w[1])
1312 else:
1313 state.tposition = 0.0
1315 except Exception:
1316 raise ValueError('need two values: <duration>, <position>')
1318 self._state_bind(
1319 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1320 [le_focus.editingFinished, le_focus.returnPressed],
1321 focus_to_lineedit)
1323 label_effective_tmin = qw.QLabel()
1324 label_effective_tmax = qw.QLabel()
1326 label_effective_tmin.setSizePolicy(
1327 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1328 label_effective_tmax.setSizePolicy(
1329 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1330 label_effective_tmin.setMinimumSize(
1331 qg.QFontMetrics(label_effective_tmin.font()).width(
1332 '0000-00-00 00:00:00.000 '), 0)
1334 layout.addWidget(label_effective_tmin, 5, 1)
1335 layout.addWidget(label_effective_tmax, 6, 1)
1337 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1338 self.register_state_listener3(
1339 self.update_effective_time_labels, self.state, var)
1341 self._label_effective_tmin = label_effective_tmin
1342 self._label_effective_tmax = label_effective_tmax
1344 self.register_state_listener3(
1345 self.update_tcursor, self.gui_state, 'tcursor')
1347 return frame
1349 def controls_appearance(self):
1350 frame = qw.QFrame(self)
1351 frame.setSizePolicy(
1352 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1353 layout = qw.QGridLayout()
1354 frame.setLayout(layout)
1356 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1358 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1359 layout.addWidget(cb, 0, 1)
1360 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1362 self.register_state_listener3(
1363 self.update_render_settings, self.state, 'lighting')
1365 # background
1367 layout.addWidget(qw.QLabel('Background'), 1, 0)
1369 cb = common.strings_to_combobox(
1370 ['black', 'white', 'skyblue1 - white'])
1372 layout.addWidget(cb, 1, 1)
1373 vstate.state_bind_combobox_background(
1374 self, self.state, 'background', cb)
1376 self.register_state_listener3(
1377 self.update_render_settings, self.state, 'background')
1379 return frame
1381 def controls_snapshots(self):
1382 return snapshots_mod.SnapshotsPanel(self)
1384 def update_effective_time_labels(self, *args):
1385 tmin = self.state.tmin_effective
1386 tmax = self.state.tmax_effective
1388 stmin = common.time_or_none_to_str(tmin)
1389 stmax = common.time_or_none_to_str(tmax)
1391 self._label_effective_tmin.setText(stmin)
1392 self._label_effective_tmax.setText(stmax)
1394 def update_tcursor(self, *args):
1395 tcursor = self.gui_state.tcursor
1396 stcursor = common.time_or_none_to_str(tcursor)
1397 self._label_tcursor.setText(stcursor)
1399 def reset_strike_dip(self, *args):
1400 self.state.strike = 90.
1401 self.state.dip = 0
1402 self.gui_state.focal_point = 'center'
1404 def register_state_listener(self, listener):
1405 self.listeners.append(listener) # keep listeners alive
1407 def register_state_listener3(self, listener, state, path):
1408 self.register_state_listener(state.add_listener(listener, path))
1410 def get_camera_geometry(self):
1412 def rtp2xyz(rtp):
1413 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1415 radius = 1.0 - self.state.depth / self.planet_radius
1417 cam_rtp = num.array([
1418 radius+self.state.distance,
1419 self.state.lat * d2r + 0.5*num.pi,
1420 self.state.lon * d2r])
1421 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1422 cam, up, foc = \
1423 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1425 foc_rtp = num.array([
1426 radius,
1427 self.state.lat * d2r + 0.5*num.pi,
1428 self.state.lon * d2r])
1430 foc = rtp2xyz(foc_rtp)
1432 rot_world = pmt.euler_to_matrix(
1433 -(self.state.lat-90.)*d2r,
1434 (self.state.lon+90.)*d2r,
1435 0.0*d2r).T
1437 rot_cam = pmt.euler_to_matrix(
1438 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1440 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1442 cam = foc + num.dot(rot, cam - foc)
1443 up = num.dot(rot, up)
1444 return cam, up, foc
1446 def update_camera(self, *args):
1447 cam, up, foc = self.get_camera_geometry()
1448 camera = self.ren.GetActiveCamera()
1449 camera.SetPosition(*cam)
1450 camera.SetFocalPoint(*foc)
1451 camera.SetViewUp(*up)
1453 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1455 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1456 self.feature_radius_min / self.planet_radius)**2))
1458 # if horizon == 0.0:
1459 # horizon = 2.0 + self.state.distance
1461 # clip_dist = max(min(self.state.distance*5., max(
1462 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1463 # , math.sqrt(num.sum(cam**2)))
1464 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1465 # clip_dist = feature_horizon
1467 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1469 self.camera_params = (
1470 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1472 self.update_view()
1474 def add_panel(
1475 self, name, panel,
1476 visible=False,
1477 # volatile=False,
1478 tabify=True,
1479 where=qc.Qt.RightDockWidgetArea,
1480 remove=None,
1481 title_controls=[]):
1483 dockwidget = common.MyDockWidget(
1484 name, self, title_controls=title_controls)
1486 if not visible:
1487 dockwidget.hide()
1489 if not self.gui_state.panels_visible:
1490 dockwidget.block()
1492 dockwidget.setWidget(panel)
1494 panel.setParent(dockwidget)
1496 dockwidgets = self.findChildren(common.MyDockWidget)
1497 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1499 self.addDockWidget(where, dockwidget)
1501 nwrap = 4
1502 if dws and len(dws) >= nwrap and tabify:
1503 self.tabifyDockWidget(
1504 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1506 mitem = dockwidget.toggleViewAction()
1507 self._panel_togglers[dockwidget] = mitem
1508 self.panels_menu.addAction(mitem)
1509 if visible:
1510 dockwidget.setVisible(True)
1511 dockwidget.setFocus()
1512 dockwidget.raise_()
1514 def raise_panel(self, panel):
1515 dockwidget = panel.parent()
1516 dockwidget.setVisible(True)
1517 dockwidget.setFocus()
1518 dockwidget.raise_()
1520 def toggle_panel_visibility(self):
1521 self.gui_state.panels_visible = not self.gui_state.panels_visible
1523 def update_panel_visibility(self, *args):
1524 self.setUpdatesEnabled(False)
1525 mbar = self.menuBar()
1526 dockwidgets = self.findChildren(common.MyDockWidget)
1528 mbar.setVisible(self.gui_state.panels_visible)
1529 for dockwidget in dockwidgets:
1530 dockwidget.setBlocked(not self.gui_state.panels_visible)
1532 self.setUpdatesEnabled(True)
1534 def remove_panel(self, panel):
1535 dockwidget = panel.parent()
1536 self.removeDockWidget(dockwidget)
1537 dockwidget.setParent(None)
1538 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1540 def register_data_provider(self, provider):
1541 if provider not in self.data_providers:
1542 self.data_providers.append(provider)
1544 def unregister_data_provider(self, provider):
1545 if provider in self.data_providers:
1546 self.data_providers.remove(provider)
1548 def iter_data(self, name):
1549 for provider in self.data_providers:
1550 for data in provider.iter_data(name):
1551 yield data
1553 def closeEvent(self, event):
1554 self.attach()
1555 event.accept()
1556 self.closing = True
1557 common.get_app().set_main_window(None)
1559 def is_closing(self):
1560 return self.closing
1563class SparrowApp(qw.QApplication):
1564 def __init__(self):
1565 qw.QApplication.__init__(self, ['Sparrow'])
1566 self.lastWindowClosed.connect(self.myQuit)
1567 self._main_window = None
1568 self.setApplicationDisplayName('Sparrow')
1569 self.setDesktopFileName('Sparrow')
1571 def install_sigint_handler(self):
1572 self._old_signal_handler = signal.signal(
1573 signal.SIGINT, self.myCloseAllWindows)
1575 def uninstall_sigint_handler(self):
1576 signal.signal(signal.SIGINT, self._old_signal_handler)
1578 def myQuit(self, *args):
1579 self.quit()
1581 def myCloseAllWindows(self, *args):
1582 self.closeAllWindows()
1584 def set_main_window(self, win):
1585 self._main_window = win
1587 def get_main_window(self):
1588 return self._main_window
1590 def get_progressbars(self):
1591 if self._main_window:
1592 return self._main_window.progressbars
1593 else:
1594 return None
1596 def status(self, message, duration=None):
1597 win = self.get_main_window()
1598 if not win:
1599 return
1601 win.statusBar().showMessage(
1602 message, int((duration or 0) * 1000))
1605def main(*args, **kwargs):
1607 from pyrocko import util
1608 from pyrocko.gui import util as gui_util
1609 util.setup_logging('sparrow', 'info')
1611 global win
1613 if gui_util.app is None:
1614 gui_util.app = SparrowApp()
1616 # try:
1617 # from qt_material import apply_stylesheet
1618 #
1619 # apply_stylesheet(app, theme='dark_teal.xml')
1620 #
1621 #
1622 # import qdarkgraystyle
1623 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1624 # import qdarkstyle
1625 #
1626 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1627 #
1628 #
1629 # except ImportError:
1630 # logger.info(
1631 # 'Module qdarkgraystyle not available.\n'
1632 # 'If wanted, install qdarkstyle with "pip install '
1633 # 'qdarkgraystyle".')
1634 #
1635 win = SparrowViewer(*args, **kwargs)
1637 gui_util.app.install_sigint_handler()
1638 gui_util.app.exec_()
1639 gui_util.app.uninstall_sigint_handler()
1641 del win
1643 gc.collect()
1645 del gui_util.app