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 collections import defaultdict
16from subprocess import check_call
18import numpy as num
20from pyrocko import cake
21from pyrocko import guts
22from pyrocko import geonames
23from pyrocko import moment_tensor as pmt
25from pyrocko.gui.util import Progressbars, RangeEdit
26from pyrocko.gui.talkie import TalkieConnectionOwner
27from pyrocko.gui.qt_compat import qw, qc, qg
28# from pyrocko.gui import vtk_util
30from . import common, light, snapshots as snapshots_mod
32import vtk
33import vtk.qt
34vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa
36from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa
38from pyrocko import geometry # noqa
39from . import state as vstate, elements # noqa
41logger = logging.getLogger('pyrocko.gui.sparrow.main')
44d2r = num.pi/180.
45km = 1000.
47if platform.uname()[0] == 'Darwin':
48 g_modifier_key = '\u2318'
49else:
50 g_modifier_key = 'Ctrl'
53class ZeroFrame(qw.QFrame):
55 def sizeHint(self):
56 return qc.QSize(0, 0)
59class LocationChoice(object):
60 def __init__(self, name, lat, lon, depth=0):
61 self._name = name
62 self._lat = lat
63 self._lon = lon
64 self._depth = depth
66 def get_lat_lon_depth(self):
67 return self._lat, self._lon, self._depth
70def location_to_choices(s):
71 choices = []
72 s_vals = s.replace(',', ' ')
73 try:
74 vals = [float(x) for x in s_vals.split()]
75 if len(vals) == 3:
76 vals[2] *= km
78 choices.append(LocationChoice('', *vals))
80 except ValueError:
81 cities = geonames.get_cities_by_name(s.strip())
82 for c in cities:
83 choices.append(LocationChoice(c.asciiname, c.lat, c.lon))
85 return choices
88class NoLocationChoices(Exception):
90 def __init__(self, s):
91 self._string = s
93 def __str__(self):
94 return 'No location choices for string "%s"' % self._string
97class QVTKWidget(QVTKRenderWindowInteractor):
98 def __init__(self, viewer, *args):
99 QVTKRenderWindowInteractor.__init__(self, *args)
100 self._viewer = viewer
102 def wheelEvent(self, event):
103 return self._viewer.myWheelEvent(event)
105 def container_resized(self, ev):
106 self._viewer.update_vtk_widget_size()
109class DetachedViewer(qw.QMainWindow):
111 def __init__(self, main_window, vtk_frame):
112 qw.QMainWindow.__init__(self, main_window)
113 self.main_window = main_window
114 self.setWindowTitle('Sparrow View')
115 vtk_frame.setParent(self)
116 self.setCentralWidget(vtk_frame)
118 def closeEvent(self, ev):
119 ev.ignore()
120 self.main_window.attach()
123class CenteringScrollArea(qw.QScrollArea):
124 def __init__(self):
125 qw.QScrollArea.__init__(self)
126 self.setAlignment(qc.Qt.AlignCenter)
127 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
128 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff)
129 self.setFrameShape(qw.QFrame.NoFrame)
131 def resizeEvent(self, ev):
132 retval = qw.QScrollArea.resizeEvent(self, ev)
133 self.widget().container_resized(ev)
134 return retval
136 def recenter(self):
137 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()):
138 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum()))))
140 def wheelEvent(self, *args, **kwargs):
141 return self.widget().wheelEvent(*args, **kwargs)
144class YAMLEditor(qw.QTextEdit):
146 def __init__(self, parent):
147 qw.QTextEdit.__init__(self)
148 self._parent = parent
150 def event(self, ev):
151 if isinstance(ev, qg.QKeyEvent) \
152 and ev.key() == qc.Qt.Key_Return \
153 and ev.modifiers() & qc.Qt.ShiftModifier:
154 self._parent.state_changed()
155 return True
157 return qw.QTextEdit.event(self, ev)
160class StateEditor(qw.QFrame, TalkieConnectionOwner):
161 def __init__(self, viewer, *args, **kwargs):
162 qw.QFrame.__init__(self, *args, **kwargs)
163 TalkieConnectionOwner.__init__(self)
165 layout = qw.QGridLayout()
167 self.setLayout(layout)
169 self.source_editor = YAMLEditor(self)
170 self.source_editor.setAcceptRichText(False)
171 self.source_editor.setStatusTip('Press Shift-Return to apply changes')
172 font = qg.QFont("Monospace")
173 self.source_editor.setCurrentFont(font)
174 layout.addWidget(self.source_editor, 0, 0, 1, 2)
176 self.error_display_label = qw.QLabel('Error')
177 layout.addWidget(self.error_display_label, 1, 0, 1, 2)
179 self.error_display = qw.QTextEdit()
180 self.error_display.setCurrentFont(font)
181 self.error_display.setReadOnly(True)
183 self.error_display.setSizePolicy(
184 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum)
186 self.error_display_label.hide()
187 self.error_display.hide()
189 layout.addWidget(self.error_display, 2, 0, 1, 2)
191 self.instant_updates = qw.QCheckBox('Instant Updates')
192 self.instant_updates.toggled.connect(self.state_changed)
193 layout.addWidget(self.instant_updates, 3, 0)
195 button = qw.QPushButton('Apply')
196 button.clicked.connect(self.state_changed)
197 layout.addWidget(button, 3, 1)
199 self.viewer = viewer
200 # recommended way, but resulted in a variable-width font being used:
201 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont)
202 self.bind_state()
203 self.source_editor.textChanged.connect(self.text_changed_handler)
204 self.destroyed.connect(self.unbind_state)
205 self.bind_state()
207 def bind_state(self, *args):
208 self.talkie_connect(self.viewer.state, '', self.update_state)
209 self.update_state()
211 def unbind_state(self):
212 self.talkie_disconnect_all()
214 def update_state(self, *args):
215 cursor = self.source_editor.textCursor()
217 cursor_position = cursor.position()
218 vsb_position = self.source_editor.verticalScrollBar().value()
219 hsb_position = self.source_editor.horizontalScrollBar().value()
221 self.source_editor.setPlainText(str(self.viewer.state))
223 cursor.setPosition(cursor_position)
224 self.source_editor.setTextCursor(cursor)
225 self.source_editor.verticalScrollBar().setValue(vsb_position)
226 self.source_editor.horizontalScrollBar().setValue(hsb_position)
228 def text_changed_handler(self, *args):
229 if self.instant_updates.isChecked():
230 self.state_changed()
232 def state_changed(self):
233 try:
234 s = self.source_editor.toPlainText()
235 state = guts.load(string=s)
236 self.viewer.set_state(state)
237 self.error_display.setPlainText('')
238 self.error_display_label.hide()
239 self.error_display.hide()
241 except Exception as e:
242 self.error_display.show()
243 self.error_display_label.show()
244 self.error_display.setPlainText(str(e))
247class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner):
248 def __init__(self, use_depth_peeling=True, events=None, snapshots=None):
249 qw.QMainWindow.__init__(self)
250 TalkieConnectionOwner.__init__(self)
252 common.get_app().set_main_window(self)
254 self.state = vstate.ViewerState()
255 self.gui_state = vstate.ViewerGuiState()
257 self.setWindowTitle('Sparrow')
259 self.setTabPosition(
260 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West)
262 self.planet_radius = cake.earthradius
263 self.feature_radius_min = cake.earthradius - 1000. * km
265 self._panel_togglers = {}
266 self._actors = set()
267 self._actors_2d = set()
268 self._render_window_size = (0, 0)
269 self._use_depth_peeling = use_depth_peeling
270 self._in_update_elements = False
271 self._update_elements_enabled = True
273 mbar = qw.QMenuBar()
274 self.setMenuBar(mbar)
276 menu = mbar.addMenu('File')
278 menu.addAction(
279 'Export Image...',
280 self.export_image,
281 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext(
282 qc.Qt.ApplicationShortcut)
284 menu.addAction(
285 'Quit',
286 self.request_quit,
287 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext(
288 qc.Qt.ApplicationShortcut)
290 menu = mbar.addMenu('View')
291 menu_sizes = menu.addMenu('Size')
292 self._add_vtk_widget_size_menu_entries(menu_sizes)
294 # detached/attached
295 self.talkie_connect(
296 self.gui_state, 'detached', self.update_detached)
298 action = qw.QAction('Detach')
299 action.setCheckable(True)
300 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D)
301 action.setShortcutContext(qc.Qt.ApplicationShortcut)
303 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action)
304 menu.addAction(action)
306 self.panels_menu = mbar.addMenu('Panels')
308 menu = mbar.addMenu('Add')
309 for name, estate in [
310 ('Icosphere', elements.IcosphereState(
311 level=4,
312 smooth=True,
313 opacity=0.5,
314 ambient=0.1)),
315 ('Grid', elements.GridState()),
316 ('Stations', elements.StationsState()),
317 ('Topography', elements.TopoState()),
318 ('Custom Topography', elements.CustomTopoState()),
319 ('Catalog', elements.CatalogState()),
320 ('Coastlines', elements.CoastlinesState()),
321 ('Source', elements.SourceState()),
322 ('HUD (tmax)', elements.HudState(
323 variables=['tmax'],
324 template='tmax: {0|date}',
325 position='top-left')),
326 ('HUD subtitle', elements.HudState(
327 template='Awesome')),
328 ('Volcanoes', elements.VolcanoesState()),
329 ('Faults', elements.ActiveFaultsState()),
330 ('Plate bounds', elements.PlatesBoundsState()),
331 ('InSAR Surface Displacements', elements.KiteState()),
332 ('Geometry', elements.GeometryState()),
333 ('Spheroid', elements.SpheroidState()),
334 ('Rays', elements.RaysState())]:
336 def wrap_add_element(estate):
337 def add_element(*args):
338 new_element = guts.clone(estate)
339 new_element.element_id = elements.random_id()
340 self.state.elements.append(new_element)
341 self.state.sort_elements()
343 return add_element
345 mitem = qw.QAction(name, self)
347 mitem.triggered.connect(wrap_add_element(estate))
349 menu.addAction(mitem)
351 self.data_providers = []
352 self.elements = {}
354 self.detached_window = None
356 self.main_frame = qw.QFrame()
357 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
359 self.vtk_frame = CenteringScrollArea()
361 self.vtk_widget = QVTKWidget(self, self)
362 self.vtk_frame.setWidget(self.vtk_widget)
364 self.main_layout = qw.QVBoxLayout()
365 self.main_layout.setContentsMargins(0, 0, 0, 0)
366 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
368 pb = Progressbars(self)
369 self.progressbars = pb
370 self.main_layout.addWidget(pb)
372 self.main_frame.setLayout(self.main_layout)
374 self.vtk_frame_substitute = None
376 self.add_panel(
377 'Navigation',
378 self.controls_navigation(), visible=True,
379 where=qc.Qt.LeftDockWidgetArea)
381 self.add_panel(
382 'Time',
383 self.controls_time(), visible=True,
384 where=qc.Qt.LeftDockWidgetArea)
386 self.add_panel(
387 'Appearance',
388 self.controls_appearance(), visible=True,
389 where=qc.Qt.LeftDockWidgetArea)
391 snapshots_panel = self.controls_snapshots()
392 self.add_panel(
393 'Snapshots',
394 snapshots_panel, visible=False,
395 where=qc.Qt.LeftDockWidgetArea)
397 self.setCentralWidget(self.main_frame)
399 self.mesh = None
401 ren = vtk.vtkRenderer()
403 # ren.SetBackground(0.15, 0.15, 0.15)
404 # ren.SetBackground(0.0, 0.0, 0.0)
405 # ren.TwoSidedLightingOn()
406 # ren.SetUseShadows(1)
408 self._lighting = None
409 self._background = None
411 self.ren = ren
412 self.update_render_settings()
413 self.update_camera()
415 renwin = self.vtk_widget.GetRenderWindow()
417 if self._use_depth_peeling:
418 renwin.SetAlphaBitPlanes(1)
419 renwin.SetMultiSamples(0)
421 ren.SetUseDepthPeeling(1)
422 ren.SetMaximumNumberOfPeels(100)
423 ren.SetOcclusionRatio(0.1)
425 ren.SetUseFXAA(1)
426 # ren.SetUseHiddenLineRemoval(1)
427 # ren.SetBackingStore(1)
429 self.renwin = renwin
431 # renwin.LineSmoothingOn()
432 # renwin.PointSmoothingOn()
433 # renwin.PolygonSmoothingOn()
435 renwin.AddRenderer(ren)
437 iren = renwin.GetInteractor()
438 iren.LightFollowCameraOn()
439 iren.SetInteractorStyle(None)
441 iren.AddObserver('LeftButtonPressEvent', self.button_event)
442 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
443 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
444 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
445 iren.AddObserver('RightButtonPressEvent', self.button_event)
446 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
447 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
448 iren.AddObserver('KeyPressEvent', self.key_down_event)
449 iren.AddObserver('KeyReleaseEvent', self.key_up_event)
450 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
452 renwin.Render()
454 iren.Initialize()
456 self.iren = iren
458 self.rotating = False
460 self._elements = {}
461 self._elements_active = {}
463 self.talkie_connect(
464 self.state, 'elements', self.update_elements)
466 self.state.elements.append(elements.IcosphereState(
467 element_id='icosphere',
468 level=4,
469 smooth=True,
470 opacity=0.5,
471 ambient=0.1))
473 self.state.elements.append(elements.GridState(
474 element_id='grid'))
475 self.state.elements.append(elements.CoastlinesState(
476 element_id='coastlines'))
477 self.state.elements.append(elements.CrosshairState(
478 element_id='crosshair'))
480 # self.state.elements.append(elements.StationsState())
481 # self.state.elements.append(elements.SourceState())
482 # self.state.elements.append(
483 # elements.CatalogState(
484 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
485 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
487 if events:
488 self.state.elements.append(
489 elements.CatalogState(
490 selection=elements.MemoryCatalogSelection(events=events)))
492 self.state.sort_elements()
494 if snapshots:
495 snapshots_ = []
496 for obj in snapshots:
497 if isinstance(obj, str):
498 snapshots_.extend(snapshots_mod.load_snapshots(obj))
499 else:
500 snapshots_.append(obj)
502 snapshots_panel.add_snapshots(snapshots_)
503 self.raise_panel(snapshots_panel)
504 snapshots_panel.goto_snapshot(1)
506 self.timer = qc.QTimer(self)
507 self.timer.timeout.connect(self.periodical)
508 self.timer.setInterval(1000)
509 self.timer.start()
511 self._animation_saver = None
513 self.closing = False
514 self.vtk_widget.setFocus()
516 self.update_detached()
518 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
519 common.get_app().status('Let\'s fly.', 2.0)
521 self.show()
522 self.windowHandle().showMaximized()
524 self.talkie_connect(
525 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
527 self.update_vtk_widget_size()
529 def _add_vtk_widget_size_menu_entries(self, menu):
531 group = qw.QActionGroup(menu)
532 group.setExclusive(True)
534 def set_variable_size():
535 self.gui_state.fixed_size = False
537 variable_size_action = menu.addAction('Fit Window Size')
538 variable_size_action.setCheckable(True)
539 variable_size_action.setActionGroup(group)
540 variable_size_action.triggered.connect(set_variable_size)
542 fixed_size_items = []
543 for nx, ny, label in [
544 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
545 (426, 240, ''),
546 (640, 360, ''),
547 (854, 480, '(FWVGA)'),
548 (1280, 720, '(HD)'),
549 (1920, 1080, '(Full HD)'),
550 (2560, 1440, '(Quad HD)'),
551 (3840, 2160, '(4K UHD)'),
552 (3840*2, 2160*2, '',),
553 (None, None, 'Aspect 4:3'),
554 (640, 480, '(VGA)'),
555 (800, 600, '(SVGA)'),
556 (None, None, 'Other'),
557 (512, 512, ''),
558 (1024, 1024, '')]:
560 if None in (nx, ny):
561 menu.addSection(label)
562 else:
563 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
564 action = menu.addAction(name)
565 action.setCheckable(True)
566 action.setActionGroup(group)
567 fixed_size_items.append((action, (nx, ny)))
569 def make_set_fixed_size(nx, ny):
570 def set_fixed_size():
571 self.gui_state.fixed_size = (float(nx), float(ny))
573 return set_fixed_size
575 action.triggered.connect(make_set_fixed_size(nx, ny))
577 def update_widget(*args):
578 for action, (nx, ny) in fixed_size_items:
579 action.blockSignals(True)
580 action.setChecked(
581 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
582 int(z) for z in self.gui_state.fixed_size)))
583 action.blockSignals(False)
585 variable_size_action.blockSignals(True)
586 variable_size_action.setChecked(not self.gui_state.fixed_size)
587 variable_size_action.blockSignals(False)
589 update_widget()
590 self.talkie_connect(
591 self.gui_state, 'fixed_size', update_widget)
593 def update_vtk_widget_size(self, *args):
594 if self.gui_state.fixed_size:
595 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
596 wanted_size = qc.QSize(nx, ny)
597 else:
598 wanted_size = qc.QSize(
599 self.vtk_frame.window().width(), self.vtk_frame.height())
601 current_size = self.vtk_widget.size()
603 if current_size.width() != wanted_size.width() \
604 or current_size.height() != wanted_size.height():
606 self.vtk_widget.setFixedSize(wanted_size)
608 self.vtk_frame.recenter()
609 self.check_vtk_resize()
611 def update_focal_point(self, *args):
612 if self.gui_state.focal_point == 'center':
613 self.vtk_widget.setStatusTip(
614 'Click and drag: change location. %s-click and drag: '
615 'change view plane orientation.' % g_modifier_key)
616 else:
617 self.vtk_widget.setStatusTip(
618 '%s-click and drag: change location. Click and drag: '
619 'change view plane orientation. Uncheck "Navigation: Fix" to '
620 'reverse sense.' % g_modifier_key)
622 def update_detached(self, *args):
624 if self.gui_state.detached and not self.detached_window: # detach
625 logger.debug('Detaching VTK view.')
627 self.main_layout.removeWidget(self.vtk_frame)
628 self.detached_window = DetachedViewer(self, self.vtk_frame)
629 self.detached_window.show()
630 self.vtk_widget.setFocus()
632 screens = common.get_app().screens()
633 if len(screens) > 1:
634 for screen in screens:
635 if screen is not self.screen():
636 self.detached_window.windowHandle().setScreen(screen)
637 # .setScreen() does not work reliably,
638 # therefore trying also with .move()...
639 p = screen.geometry().topLeft()
640 self.detached_window.move(p.x() + 50, p.y() + 50)
641 # ... but also does not work in notion window manager.
643 self.detached_window.windowHandle().showMaximized()
645 frame = qw.QFrame()
646 # frame.setFrameShape(qw.QFrame.NoFrame)
647 # frame.setBackgroundRole(qg.QPalette.Mid)
648 # frame.setAutoFillBackground(True)
649 frame.setSizePolicy(
650 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
652 layout = qw.QGridLayout()
653 frame.setLayout(layout)
654 self.main_layout.insertWidget(0, frame)
656 self.state_editor = StateEditor(self)
658 layout.addWidget(self.state_editor, 0, 0)
660 # attach_button = qw.QPushButton('Attach View')
661 # attach_button.clicked.connect(self.attach)
662 # layout.addWidget(
663 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
665 self.vtk_frame_substitute = frame
667 if not self.gui_state.detached and self.detached_window: # attach
668 logger.debug('Attaching VTK view.')
669 self.detached_window.hide()
670 self.vtk_frame.setParent(self)
671 if self.vtk_frame_substitute:
672 self.main_layout.removeWidget(self.vtk_frame_substitute)
673 self.state_editor.unbind_state()
674 self.vtk_frame_substitute = None
676 self.main_layout.insertWidget(0, self.vtk_frame)
677 self.detached_window = None
678 self.vtk_widget.setFocus()
680 def attach(self):
681 self.gui_state.detached = False
683 def export_image(self):
685 caption = 'Export Image'
686 fn_out, _ = qw.QFileDialog.getSaveFileName(
687 self, caption, 'image.png',
688 options=common.qfiledialog_options)
690 if fn_out:
691 self.save_image(fn_out)
693 def save_image(self, path):
695 original_fixed_size = self.gui_state.fixed_size
696 if original_fixed_size is None:
697 self.gui_state.fixed_size = (1920., 1080.)
699 wif = vtk.vtkWindowToImageFilter()
700 wif.SetInput(self.renwin)
701 wif.SetInputBufferTypeToRGBA()
702 wif.SetScale(1, 1)
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.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 original_fixed_size = self.gui_state.fixed_size
748 if original_fixed_size is None:
749 self.gui_state.fixed_size = (1920., 1080.)
751 wif = vtk.vtkWindowToImageFilter()
752 wif.SetInput(self.renwin)
753 wif.SetInputBufferTypeToRGBA()
754 wif.SetScale(1, 1)
755 wif.ReadFrontBufferOff()
756 writer = vtk.vtkPNGWriter()
757 temp_path = tempfile.mkdtemp()
758 self._animation_saver = (
759 wif, writer, temp_path, output_path, original_fixed_size)
760 writer.SetInputConnection(wif.GetOutputPort())
762 def next_animation_frame(self):
764 ani = self._animation
765 if not ani:
766 return
768 if self._animation_iframe is not None:
769 state = ani(
770 ani.tmin
771 + self._animation_iframe * ani.dt)
773 self._animation_iframe += 1
774 else:
775 tnow = time.time()
776 state = ani(min(
777 ani.tmax,
778 ani.tmin + (tnow - self._animation_tstart)))
780 self.set_state(state)
781 self.renwin.Render()
782 if self._animation_saver:
783 wif, writer, temp_path, _, _ = self._animation_saver
784 wif.Modified()
785 fn = os.path.join(temp_path, 'f%09i.png')
786 writer.SetFileName(fn % self._animation_iframe)
787 writer.Write()
789 if self._animation_iframe is not None:
790 t = self._animation_iframe * ani.dt
791 else:
792 t = tnow - self._animation_tstart
794 if t > ani.tmax - ani.tmin:
795 self.stop_animation()
797 def stop_animation(self):
798 if self._animation_timer:
799 self._animation_timer.stop()
801 if self._animation_saver:
803 wif, writer, temp_path, output_path, original_fixed_size \
804 = self._animation_saver
805 self.gui_state.fixed_size = original_fixed_size
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._update_elements_enabled = False
831 self.setUpdatesEnabled(False)
832 self.state.diff_update(state)
833 self.state.sort_elements()
834 self.setUpdatesEnabled(True)
835 self._update_elements_enabled = True
836 self.update_elements()
838 def periodical(self):
839 pass
841 def request_quit(self):
842 app = common.get_app()
843 app.myQuit()
845 def check_vtk_resize(self, *args):
846 render_window_size = self.renwin.GetSize()
847 if self._render_window_size != render_window_size:
848 self._render_window_size = render_window_size
849 self.resize_event(*render_window_size)
851 def update_elements(self, *_):
852 if not self._update_elements_enabled:
853 return
855 if self._in_update_elements:
856 return
858 self._in_update_elements = True
859 for estate in self.state.elements:
860 if estate.element_id not in self._elements:
861 new_element = estate.create()
862 logger.debug('Creating "%s" ("%s").' % (
863 type(new_element).__name__,
864 estate.element_id))
865 self._elements[estate.element_id] = new_element
867 element = self._elements[estate.element_id]
869 if estate.element_id not in self._elements_active:
870 logger.debug('Adding "%s" ("%s")' % (
871 type(element).__name__,
872 estate.element_id))
873 element.bind_state(estate)
874 element.set_parent(self)
875 self._elements_active[estate.element_id] = element
877 state_element_ids = [el.element_id for el in self.state.elements]
878 deactivate = []
879 for element_id, element in self._elements_active.items():
880 if element_id not in state_element_ids:
881 logger.debug('Removing "%s" ("%s").' % (
882 type(element).__name__,
883 element_id))
884 element.unset_parent()
885 deactivate.append(element_id)
887 for element_id in deactivate:
888 del self._elements_active[element_id]
890 self._update_crosshair_bindings()
892 self._in_update_elements = False
894 def _update_crosshair_bindings(self):
896 def get_crosshair_element():
897 for element in self.state.elements:
898 if element.element_id == 'crosshair':
899 return element
901 return None
903 crosshair = get_crosshair_element()
904 if crosshair is None or crosshair.is_connected:
905 return
907 def to_checkbox(state, widget):
908 widget.blockSignals(True)
909 widget.setChecked(state.visible)
910 widget.blockSignals(False)
912 def to_state(widget, state):
913 state.visible = widget.isChecked()
915 cb = self._crosshair_checkbox
916 vstate.state_bind(
917 self, crosshair, ['visible'], to_state,
918 cb, [cb.toggled], to_checkbox)
920 crosshair.is_connected = True
922 def add_actor_2d(self, actor):
923 if actor not in self._actors_2d:
924 self.ren.AddActor2D(actor)
925 self._actors_2d.add(actor)
927 def remove_actor_2d(self, actor):
928 if actor in self._actors_2d:
929 self.ren.RemoveActor2D(actor)
930 self._actors_2d.remove(actor)
932 def add_actor(self, actor):
933 if actor not in self._actors:
934 self.ren.AddActor(actor)
935 self._actors.add(actor)
937 def add_actor_list(self, actorlist):
938 for actor in actorlist:
939 self.add_actor(actor)
941 def remove_actor(self, actor):
942 if actor in self._actors:
943 self.ren.RemoveActor(actor)
944 self._actors.remove(actor)
946 def update_view(self):
947 self.vtk_widget.update()
949 def resize_event(self, size_x, size_y):
950 self.gui_state.size = (size_x, size_y)
952 def button_event(self, obj, event):
953 if event == "LeftButtonPressEvent":
954 self.rotating = True
955 elif event == "LeftButtonReleaseEvent":
956 self.rotating = False
958 def mouse_move_event(self, obj, event):
959 x0, y0 = self.iren.GetLastEventPosition()
960 x, y = self.iren.GetEventPosition()
962 size_x, size_y = self.renwin.GetSize()
963 center_x = size_x / 2.0
964 center_y = size_y / 2.0
966 if self.rotating:
967 self.do_rotate(x, y, x0, y0, center_x, center_y)
969 def myWheelEvent(self, event):
971 angle = event.angleDelta().y()
973 if angle > 200:
974 angle = 200
976 if angle < -200:
977 angle = -200
979 self.do_dolly(-angle/100.)
981 def do_rotate(self, x, y, x0, y0, center_x, center_y):
983 dx = x0 - x
984 dy = y0 - y
986 phi = d2r*(self.state.strike - 90.)
987 focp = self.gui_state.focal_point
989 if focp == 'center':
990 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
991 - math.sin(phi) * dx + math.cos(phi) * dy
993 lat = self.state.lat
994 lon = self.state.lon
995 factor = self.state.distance / 10.0
996 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
997 else:
998 lat = 90. - self.state.dip
999 lon = -self.state.strike - 90.
1000 factor = 0.5
1001 factor_lat = 1.0
1003 dlat = dy * factor
1004 dlon = dx * factor * factor_lat
1006 lat = max(min(lat + dlat, 90.), -90.)
1007 lon += dlon
1008 lon = (lon + 180.) % 360. - 180.
1010 if focp == 'center':
1011 self.state.lat = float(lat)
1012 self.state.lon = float(lon)
1013 else:
1014 self.state.dip = float(90. - lat)
1015 self.state.strike = float(-(lon + 90.))
1017 def do_dolly(self, v):
1018 self.state.distance *= float(1.0 + 0.1*v)
1020 def key_down_event(self, obj, event):
1021 k = obj.GetKeyCode()
1022 s = obj.GetKeySym()
1023 if k == 'f' or s == 'Control_L':
1024 self.gui_state.next_focal_point()
1026 elif k == 'r':
1027 self.reset_strike_dip()
1029 elif k == 'p':
1030 print(self.state)
1032 elif k == 'i':
1033 for elem in self.state.elements:
1034 if isinstance(elem, elements.IcosphereState):
1035 elem.visible = not elem.visible
1037 elif k == 'c':
1038 for elem in self.state.elements:
1039 if isinstance(elem, elements.CoastlinesState):
1040 elem.visible = not elem.visible
1042 elif k == 't':
1043 if not any(
1044 isinstance(elem, elements.TopoState)
1045 for elem in self.state.elements):
1047 self.state.elements.append(elements.TopoState())
1048 else:
1049 for elem in self.state.elements:
1050 if isinstance(elem, elements.TopoState):
1051 elem.visible = not elem.visible
1053 elif k == ' ':
1054 self.toggle_panel_visibility()
1056 def key_up_event(self, obj, event):
1057 s = obj.GetKeySym()
1058 if s == 'Control_L':
1059 self.gui_state.next_focal_point()
1061 def _state_bind(self, *args, **kwargs):
1062 vstate.state_bind(self, self.state, *args, **kwargs)
1064 def _gui_state_bind(self, *args, **kwargs):
1065 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1067 def controls_navigation(self):
1068 frame = qw.QFrame(self)
1069 frame.setSizePolicy(
1070 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1071 layout = qw.QGridLayout()
1072 frame.setLayout(layout)
1074 # lat, lon, depth
1076 layout.addWidget(
1077 qw.QLabel('Location'), 0, 0, 1, 2)
1079 le = qw.QLineEdit()
1080 le.setStatusTip(
1081 'Latitude, Longitude, Depth [km] or city name: '
1082 'Focal point location.')
1083 layout.addWidget(le, 1, 0, 1, 1)
1085 def lat_lon_depth_to_lineedit(state, widget):
1086 sel = str(widget.selectedText()) == str(widget.text())
1087 widget.setText('%g, %g, %g' % (
1088 state.lat, state.lon, state.depth / km))
1090 if sel:
1091 widget.selectAll()
1093 def lineedit_to_lat_lon_depth(widget, state):
1094 s = str(widget.text())
1095 choices = location_to_choices(s)
1096 if len(choices) > 0:
1097 self.state.lat, self.state.lon, self.state.depth = \
1098 choices[0].get_lat_lon_depth()
1099 else:
1100 raise NoLocationChoices(s)
1102 self._state_bind(
1103 ['lat', 'lon', 'depth'],
1104 lineedit_to_lat_lon_depth,
1105 le, [le.editingFinished, le.returnPressed],
1106 lat_lon_depth_to_lineedit)
1108 self.lat_lon_lineedit = le
1110 self.lat_lon_lineedit.returnPressed.connect(
1111 lambda *args: self.lat_lon_lineedit.selectAll())
1113 # focal point
1115 cb = qw.QCheckBox('Fix')
1116 cb.setStatusTip(
1117 'Fix location. Orbit focal point without pressing %s.'
1118 % g_modifier_key)
1119 layout.addWidget(cb, 1, 1, 1, 1)
1121 def focal_point_to_checkbox(state, widget):
1122 widget.blockSignals(True)
1123 widget.setChecked(self.gui_state.focal_point != 'center')
1124 widget.blockSignals(False)
1126 def checkbox_to_focal_point(widget, state):
1127 self.gui_state.focal_point = \
1128 'target' if widget.isChecked() else 'center'
1130 self._gui_state_bind(
1131 ['focal_point'], checkbox_to_focal_point,
1132 cb, [cb.toggled], focal_point_to_checkbox)
1134 self.focal_point_checkbox = cb
1136 self.talkie_connect(
1137 self.gui_state, 'focal_point', self.update_focal_point)
1139 self.update_focal_point()
1141 # strike, dip
1143 layout.addWidget(
1144 qw.QLabel('View Plane'), 2, 0, 1, 2)
1146 le = qw.QLineEdit()
1147 le.setStatusTip(
1148 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1149 'direction.')
1150 layout.addWidget(le, 3, 0, 1, 1)
1152 def strike_dip_to_lineedit(state, widget):
1153 sel = widget.selectedText() == widget.text()
1154 widget.setText('%g, %g' % (state.strike, state.dip))
1155 if sel:
1156 widget.selectAll()
1158 def lineedit_to_strike_dip(widget, state):
1159 s = str(widget.text())
1160 string_to_strike_dip = {
1161 'east': (0., 90.),
1162 'west': (180., 90.),
1163 'south': (90., 90.),
1164 'north': (270., 90.),
1165 'top': (90., 0.),
1166 'bottom': (90., 180.)}
1168 if s in string_to_strike_dip:
1169 state.strike, state.dip = string_to_strike_dip[s]
1171 s = s.replace(',', ' ')
1172 try:
1173 state.strike, state.dip = map(float, s.split())
1174 except Exception:
1175 raise ValueError('need two numerical values: <strike>, <dip>')
1177 self._state_bind(
1178 ['strike', 'dip'], lineedit_to_strike_dip,
1179 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1181 self.strike_dip_lineedit = le
1182 self.strike_dip_lineedit.returnPressed.connect(
1183 lambda *args: self.strike_dip_lineedit.selectAll())
1185 but = qw.QPushButton('Reset')
1186 but.setStatusTip('Reset to north-up map view.')
1187 but.clicked.connect(self.reset_strike_dip)
1188 layout.addWidget(but, 3, 1, 1, 1)
1190 # crosshair
1192 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1193 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1195 # camera bindings
1196 self.talkie_connect(
1197 self.state,
1198 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1199 self.update_camera)
1201 self.talkie_connect(
1202 self.gui_state, 'panels_visible', self.update_panel_visibility)
1204 return frame
1206 def controls_time(self):
1207 frame = qw.QFrame(self)
1208 frame.setSizePolicy(
1209 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1211 layout = qw.QGridLayout()
1212 frame.setLayout(layout)
1214 layout.addWidget(qw.QLabel('Min'), 0, 0)
1215 le_tmin = qw.QLineEdit()
1216 layout.addWidget(le_tmin, 0, 1)
1218 layout.addWidget(qw.QLabel('Max'), 1, 0)
1219 le_tmax = qw.QLineEdit()
1220 layout.addWidget(le_tmax, 1, 1)
1222 label_tcursor = qw.QLabel()
1224 label_tcursor.setSizePolicy(
1225 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1227 layout.addWidget(label_tcursor, 2, 1)
1228 self._label_tcursor = label_tcursor
1230 def time_to_lineedit(state, attribute, widget):
1231 sel = widget.selectedText() == widget.text() \
1232 and widget.text() != ''
1234 widget.setText(
1235 common.time_or_none_to_str(getattr(state, attribute)))
1237 if sel:
1238 widget.selectAll()
1240 def lineedit_to_time(widget, state, attribute):
1241 from pyrocko.util import str_to_time_fillup
1243 s = str(widget.text())
1244 if not s.strip():
1245 setattr(state, attribute, None)
1246 else:
1247 try:
1248 setattr(state, attribute, str_to_time_fillup(s))
1249 except Exception:
1250 raise ValueError(
1251 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1253 self._state_bind(
1254 ['tmin'], lineedit_to_time, le_tmin,
1255 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1256 attribute='tmin')
1257 self._state_bind(
1258 ['tmax'], lineedit_to_time, le_tmax,
1259 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1260 attribute='tmax')
1262 self.tmin_lineedit = le_tmin
1263 self.tmax_lineedit = le_tmax
1265 range_edit = RangeEdit()
1266 range_edit.set_data_provider(self)
1267 range_edit.set_data_name('time')
1269 xblock = [False]
1271 def range_to_range_edit(state, widget):
1272 if not xblock[0]:
1273 widget.blockSignals(True)
1274 widget.set_focus(state.tduration, state.tposition)
1275 widget.set_range(state.tmin, state.tmax)
1276 widget.blockSignals(False)
1278 def range_edit_to_range(widget, state):
1279 xblock[0] = True
1280 self.state.tduration, self.state.tposition = widget.get_focus()
1281 self.state.tmin, self.state.tmax = widget.get_range()
1282 xblock[0] = False
1284 self._state_bind(
1285 ['tmin', 'tmax', 'tduration', 'tposition'],
1286 range_edit_to_range,
1287 range_edit,
1288 [range_edit.rangeChanged, range_edit.focusChanged],
1289 range_to_range_edit)
1291 def handle_tcursor_changed():
1292 self.gui_state.tcursor = range_edit.get_tcursor()
1294 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1296 layout.addWidget(range_edit, 3, 0, 1, 2)
1298 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1299 le_focus = qw.QLineEdit()
1300 layout.addWidget(le_focus, 4, 1)
1302 def focus_to_lineedit(state, widget):
1303 sel = widget.selectedText() == widget.text() \
1304 and widget.text() != ''
1306 if state.tduration is None:
1307 widget.setText('')
1308 else:
1309 widget.setText('%s, %g' % (
1310 guts.str_duration(state.tduration),
1311 state.tposition))
1313 if sel:
1314 widget.selectAll()
1316 def lineedit_to_focus(widget, state):
1317 s = str(widget.text())
1318 w = [x.strip() for x in s.split(',')]
1319 try:
1320 if len(w) == 0 or not w[0]:
1321 state.tduration = None
1322 state.tposition = 0.0
1323 else:
1324 state.tduration = guts.parse_duration(w[0])
1325 if len(w) > 1:
1326 state.tposition = float(w[1])
1327 else:
1328 state.tposition = 0.0
1330 except Exception:
1331 raise ValueError('need two values: <duration>, <position>')
1333 self._state_bind(
1334 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1335 [le_focus.editingFinished, le_focus.returnPressed],
1336 focus_to_lineedit)
1338 label_effective_tmin = qw.QLabel()
1339 label_effective_tmax = qw.QLabel()
1341 label_effective_tmin.setSizePolicy(
1342 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1343 label_effective_tmax.setSizePolicy(
1344 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1345 label_effective_tmin.setMinimumSize(
1346 qg.QFontMetrics(label_effective_tmin.font()).width(
1347 '0000-00-00 00:00:00.000 '), 0)
1349 layout.addWidget(label_effective_tmin, 5, 1)
1350 layout.addWidget(label_effective_tmax, 6, 1)
1352 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1353 self.talkie_connect(
1354 self.state, var, self.update_effective_time_labels)
1356 self._label_effective_tmin = label_effective_tmin
1357 self._label_effective_tmax = label_effective_tmax
1359 self.talkie_connect(
1360 self.gui_state, 'tcursor', self.update_tcursor)
1362 return frame
1364 def controls_appearance(self):
1365 frame = qw.QFrame(self)
1366 frame.setSizePolicy(
1367 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1368 layout = qw.QGridLayout()
1369 frame.setLayout(layout)
1371 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1373 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1374 layout.addWidget(cb, 0, 1)
1375 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1377 self.talkie_connect(
1378 self.state, 'lighting', self.update_render_settings)
1380 # background
1382 layout.addWidget(qw.QLabel('Background'), 1, 0)
1384 cb = common.strings_to_combobox(
1385 ['black', 'white', 'skyblue1 - white'])
1387 layout.addWidget(cb, 1, 1)
1388 vstate.state_bind_combobox_background(
1389 self, self.state, 'background', cb)
1391 self.talkie_connect(
1392 self.state, 'background', self.update_render_settings)
1394 return frame
1396 def controls_snapshots(self):
1397 return snapshots_mod.SnapshotsPanel(self)
1399 def update_effective_time_labels(self, *args):
1400 tmin = self.state.tmin_effective
1401 tmax = self.state.tmax_effective
1403 stmin = common.time_or_none_to_str(tmin)
1404 stmax = common.time_or_none_to_str(tmax)
1406 self._label_effective_tmin.setText(stmin)
1407 self._label_effective_tmax.setText(stmax)
1409 def update_tcursor(self, *args):
1410 tcursor = self.gui_state.tcursor
1411 stcursor = common.time_or_none_to_str(tcursor)
1412 self._label_tcursor.setText(stcursor)
1414 def reset_strike_dip(self, *args):
1415 self.state.strike = 90.
1416 self.state.dip = 0
1417 self.gui_state.focal_point = 'center'
1419 def get_camera_geometry(self):
1421 def rtp2xyz(rtp):
1422 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1424 radius = 1.0 - self.state.depth / self.planet_radius
1426 cam_rtp = num.array([
1427 radius+self.state.distance,
1428 self.state.lat * d2r + 0.5*num.pi,
1429 self.state.lon * d2r])
1430 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1431 cam, up, foc = \
1432 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1434 foc_rtp = num.array([
1435 radius,
1436 self.state.lat * d2r + 0.5*num.pi,
1437 self.state.lon * d2r])
1439 foc = rtp2xyz(foc_rtp)
1441 rot_world = pmt.euler_to_matrix(
1442 -(self.state.lat-90.)*d2r,
1443 (self.state.lon+90.)*d2r,
1444 0.0*d2r).T
1446 rot_cam = pmt.euler_to_matrix(
1447 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1449 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1451 cam = foc + num.dot(rot, cam - foc)
1452 up = num.dot(rot, up)
1453 return cam, up, foc
1455 def update_camera(self, *args):
1456 cam, up, foc = self.get_camera_geometry()
1457 camera = self.ren.GetActiveCamera()
1458 camera.SetPosition(*cam)
1459 camera.SetFocalPoint(*foc)
1460 camera.SetViewUp(*up)
1462 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1464 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1465 self.feature_radius_min / self.planet_radius)**2))
1467 # if horizon == 0.0:
1468 # horizon = 2.0 + self.state.distance
1470 # clip_dist = max(min(self.state.distance*5., max(
1471 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1472 # , math.sqrt(num.sum(cam**2)))
1473 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1474 # clip_dist = feature_horizon
1476 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1478 self.camera_params = (
1479 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1481 self.update_view()
1483 def add_panel(
1484 self, title_label, panel,
1485 visible=False,
1486 # volatile=False,
1487 tabify=True,
1488 where=qc.Qt.RightDockWidgetArea,
1489 remove=None,
1490 title_controls=[]):
1492 dockwidget = common.MyDockWidget(
1493 self, title_label, title_controls=title_controls)
1495 if not visible:
1496 dockwidget.hide()
1498 if not self.gui_state.panels_visible:
1499 dockwidget.block()
1501 dockwidget.setWidget(panel)
1503 panel.setParent(dockwidget)
1505 dockwidgets = self.findChildren(common.MyDockWidget)
1506 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1508 self.addDockWidget(where, dockwidget)
1510 nwrap = 4
1511 if dws and len(dws) >= nwrap and tabify:
1512 self.tabifyDockWidget(
1513 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1515 mitem = dockwidget.toggleViewAction()
1517 def update_label(*args):
1518 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1519 self.update_slug_abbreviated_lengths()
1521 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1522 dockwidget.titlebar._title_label.title_changed.connect(
1523 self.update_slug_abbreviated_lengths)
1525 update_label()
1527 self._panel_togglers[dockwidget] = mitem
1528 self.panels_menu.addAction(mitem)
1529 if visible:
1530 dockwidget.setVisible(True)
1531 dockwidget.setFocus()
1532 dockwidget.raise_()
1534 def update_slug_abbreviated_lengths(self):
1535 dockwidgets = self.findChildren(common.MyDockWidget)
1536 title_labels = []
1537 for dw in dockwidgets:
1538 title_labels.append(dw.titlebar._title_label)
1540 by_title = defaultdict(list)
1541 for tl in title_labels:
1542 by_title[tl.get_title()].append(tl)
1544 for group in by_title.values():
1545 slugs = [tl.get_slug() for tl in group]
1547 n = max(len(slug) for slug in slugs)
1548 nunique = len(set(slugs))
1550 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1551 n -= 1
1553 if n > 0:
1554 n = max(3, n)
1556 for tl in group:
1557 tl.set_slug_abbreviated_length(n)
1559 def raise_panel(self, panel):
1560 dockwidget = panel.parent()
1561 dockwidget.setVisible(True)
1562 dockwidget.setFocus()
1563 dockwidget.raise_()
1565 def toggle_panel_visibility(self):
1566 self.gui_state.panels_visible = not self.gui_state.panels_visible
1568 def update_panel_visibility(self, *args):
1569 self.setUpdatesEnabled(False)
1570 mbar = self.menuBar()
1571 dockwidgets = self.findChildren(common.MyDockWidget)
1573 mbar.setVisible(self.gui_state.panels_visible)
1574 for dockwidget in dockwidgets:
1575 dockwidget.setBlocked(not self.gui_state.panels_visible)
1577 self.setUpdatesEnabled(True)
1579 def remove_panel(self, panel):
1580 dockwidget = panel.parent()
1581 self.removeDockWidget(dockwidget)
1582 dockwidget.setParent(None)
1583 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1585 def register_data_provider(self, provider):
1586 if provider not in self.data_providers:
1587 self.data_providers.append(provider)
1589 def unregister_data_provider(self, provider):
1590 if provider in self.data_providers:
1591 self.data_providers.remove(provider)
1593 def iter_data(self, name):
1594 for provider in self.data_providers:
1595 for data in provider.iter_data(name):
1596 yield data
1598 def closeEvent(self, event):
1599 self.attach()
1600 event.accept()
1601 self.closing = True
1602 common.get_app().set_main_window(None)
1604 def is_closing(self):
1605 return self.closing
1608class SparrowApp(qw.QApplication):
1609 def __init__(self):
1610 qw.QApplication.__init__(self, ['Sparrow'])
1611 self.lastWindowClosed.connect(self.myQuit)
1612 self._main_window = None
1613 self.setApplicationDisplayName('Sparrow')
1614 self.setDesktopFileName('Sparrow')
1616 def install_sigint_handler(self):
1617 self._old_signal_handler = signal.signal(
1618 signal.SIGINT, self.myCloseAllWindows)
1620 def uninstall_sigint_handler(self):
1621 signal.signal(signal.SIGINT, self._old_signal_handler)
1623 def myQuit(self, *args):
1624 self.quit()
1626 def myCloseAllWindows(self, *args):
1627 self.closeAllWindows()
1629 def set_main_window(self, win):
1630 self._main_window = win
1632 def get_main_window(self):
1633 return self._main_window
1635 def get_progressbars(self):
1636 if self._main_window:
1637 return self._main_window.progressbars
1638 else:
1639 return None
1641 def status(self, message, duration=None):
1642 win = self.get_main_window()
1643 if not win:
1644 return
1646 win.statusBar().showMessage(
1647 message, int((duration or 0) * 1000))
1650def main(*args, **kwargs):
1652 from pyrocko import util
1653 from pyrocko.gui import util as gui_util
1654 util.setup_logging('sparrow', 'info')
1656 global win
1658 if gui_util.app is None:
1659 gui_util.app = SparrowApp()
1661 # try:
1662 # from qt_material import apply_stylesheet
1663 #
1664 # apply_stylesheet(app, theme='dark_teal.xml')
1665 #
1666 #
1667 # import qdarkgraystyle
1668 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1669 # import qdarkstyle
1670 #
1671 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1672 #
1673 #
1674 # except ImportError:
1675 # logger.info(
1676 # 'Module qdarkgraystyle not available.\n'
1677 # 'If wanted, install qdarkstyle with "pip install '
1678 # 'qdarkgraystyle".')
1679 #
1680 win = SparrowViewer(*args, **kwargs)
1682 gui_util.app.install_sigint_handler()
1683 gui_util.app.exec_()
1684 gui_util.app.uninstall_sigint_handler()
1686 del win
1688 gc.collect()
1690 del gui_util.app