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.ReadFrontBufferOff()
703 writer = vtk.vtkPNGWriter()
704 writer.SetInputConnection(wif.GetOutputPort())
706 self.renwin.Render()
707 wif.Modified()
708 writer.SetFileName(path)
709 writer.Write()
711 self.vtk_widget.setFixedSize(
712 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
714 self.gui_state.fixed_size = original_fixed_size
716 def update_render_settings(self, *args):
717 if self._lighting is None or self._lighting != self.state.lighting:
718 self.ren.RemoveAllLights()
719 for li in light.get_lights(self.state.lighting):
720 self.ren.AddLight(li)
722 self._lighting = self.state.lighting
724 if self._background is None \
725 or self._background != self.state.background:
727 self.state.background.vtk_apply(self.ren)
728 self._background = self.state.background
730 self.update_view()
732 def start_animation(self, interpolator, output_path=None):
733 self._animation = interpolator
734 if output_path is None:
735 self._animation_tstart = time.time()
736 self._animation_iframe = None
737 else:
738 self._animation_iframe = 0
739 self.showFullScreen()
740 self.update_view()
741 self.gui_state.panels_visible = False
742 self.update_view()
744 self._animation_timer = qc.QTimer(self)
745 self._animation_timer.timeout.connect(self.next_animation_frame)
746 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
747 self._animation_timer.start()
748 if output_path is not None:
749 self.vtk_widget.setFixedSize(qc.QSize(1920, 1080))
750 # self.vtk_widget.setFixedSize(qc.QSize(960, 540))
752 wif = vtk.vtkWindowToImageFilter()
753 wif.SetInput(self.renwin)
754 wif.SetInputBufferTypeToRGBA()
755 wif.SetScale(1, 1)
756 wif.ReadFrontBufferOff()
757 writer = vtk.vtkPNGWriter()
758 temp_path = tempfile.mkdtemp()
759 self._animation_saver = (wif, writer, temp_path, output_path)
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:
802 self.vtk_widget.setFixedSize(
803 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX)
805 wif, writer, temp_path, output_path = self._animation_saver
806 fn_path = os.path.join(temp_path, 'f%09d.png')
807 check_call([
808 'ffmpeg', '-y',
809 '-i', fn_path,
810 '-c:v', 'libx264',
811 '-preset', 'slow',
812 '-crf', '17',
813 '-vf', 'format=yuv420p,fps=%i' % (
814 int(round(1.0/self._animation.dt))),
815 output_path])
816 shutil.rmtree(temp_path)
818 self._animation_saver = None
819 self._animation_saver
821 self.showNormal()
822 self.gui_state.panels_visible = True
824 self._animation_tstart = None
825 self._animation_iframe = None
826 self._animation = None
828 def set_state(self, state):
829 self._update_elements_enabled = False
830 self.setUpdatesEnabled(False)
831 self.state.diff_update(state)
832 self.state.sort_elements()
833 self.setUpdatesEnabled(True)
834 self._update_elements_enabled = True
835 self.update_elements()
837 def periodical(self):
838 pass
840 def request_quit(self):
841 app = common.get_app()
842 app.myQuit()
844 def check_vtk_resize(self, *args):
845 render_window_size = self.renwin.GetSize()
846 if self._render_window_size != render_window_size:
847 self._render_window_size = render_window_size
848 self.resize_event(*render_window_size)
850 def update_elements(self, *_):
851 if not self._update_elements_enabled:
852 return
854 if self._in_update_elements:
855 return
857 self._in_update_elements = True
858 for estate in self.state.elements:
859 if estate.element_id not in self._elements:
860 new_element = estate.create()
861 logger.debug('Creating "%s" ("%s").' % (
862 type(new_element).__name__,
863 estate.element_id))
864 self._elements[estate.element_id] = new_element
866 element = self._elements[estate.element_id]
868 if estate.element_id not in self._elements_active:
869 logger.debug('Adding "%s" ("%s")' % (
870 type(element).__name__,
871 estate.element_id))
872 element.bind_state(estate)
873 element.set_parent(self)
874 self._elements_active[estate.element_id] = element
876 state_element_ids = [el.element_id for el in self.state.elements]
877 deactivate = []
878 for element_id, element in self._elements_active.items():
879 if element_id not in state_element_ids:
880 logger.debug('Removing "%s" ("%s").' % (
881 type(element).__name__,
882 element_id))
883 element.unset_parent()
884 deactivate.append(element_id)
886 for element_id in deactivate:
887 del self._elements_active[element_id]
889 self._update_crosshair_bindings()
891 self._in_update_elements = False
893 def _update_crosshair_bindings(self):
895 def get_crosshair_element():
896 for element in self.state.elements:
897 if element.element_id == 'crosshair':
898 return element
900 return None
902 crosshair = get_crosshair_element()
903 if crosshair is None or crosshair.is_connected:
904 return
906 def to_checkbox(state, widget):
907 widget.blockSignals(True)
908 widget.setChecked(state.visible)
909 widget.blockSignals(False)
911 def to_state(widget, state):
912 state.visible = widget.isChecked()
914 cb = self._crosshair_checkbox
915 vstate.state_bind(
916 self, crosshair, ['visible'], to_state,
917 cb, [cb.toggled], to_checkbox)
919 crosshair.is_connected = True
921 def add_actor_2d(self, actor):
922 if actor not in self._actors_2d:
923 self.ren.AddActor2D(actor)
924 self._actors_2d.add(actor)
926 def remove_actor_2d(self, actor):
927 if actor in self._actors_2d:
928 self.ren.RemoveActor2D(actor)
929 self._actors_2d.remove(actor)
931 def add_actor(self, actor):
932 if actor not in self._actors:
933 self.ren.AddActor(actor)
934 self._actors.add(actor)
936 def add_actor_list(self, actorlist):
937 for actor in actorlist:
938 self.add_actor(actor)
940 def remove_actor(self, actor):
941 if actor in self._actors:
942 self.ren.RemoveActor(actor)
943 self._actors.remove(actor)
945 def update_view(self):
946 self.vtk_widget.update()
948 def resize_event(self, size_x, size_y):
949 self.gui_state.size = (size_x, size_y)
951 def button_event(self, obj, event):
952 if event == "LeftButtonPressEvent":
953 self.rotating = True
954 elif event == "LeftButtonReleaseEvent":
955 self.rotating = False
957 def mouse_move_event(self, obj, event):
958 x0, y0 = self.iren.GetLastEventPosition()
959 x, y = self.iren.GetEventPosition()
961 size_x, size_y = self.renwin.GetSize()
962 center_x = size_x / 2.0
963 center_y = size_y / 2.0
965 if self.rotating:
966 self.do_rotate(x, y, x0, y0, center_x, center_y)
968 def myWheelEvent(self, event):
970 angle = event.angleDelta().y()
972 if angle > 200:
973 angle = 200
975 if angle < -200:
976 angle = -200
978 self.do_dolly(-angle/100.)
980 def do_rotate(self, x, y, x0, y0, center_x, center_y):
982 dx = x0 - x
983 dy = y0 - y
985 phi = d2r*(self.state.strike - 90.)
986 focp = self.gui_state.focal_point
988 if focp == 'center':
989 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
990 - math.sin(phi) * dx + math.cos(phi) * dy
992 lat = self.state.lat
993 lon = self.state.lon
994 factor = self.state.distance / 10.0
995 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
996 else:
997 lat = 90. - self.state.dip
998 lon = -self.state.strike - 90.
999 factor = 0.5
1000 factor_lat = 1.0
1002 dlat = dy * factor
1003 dlon = dx * factor * factor_lat
1005 lat = max(min(lat + dlat, 90.), -90.)
1006 lon += dlon
1007 lon = (lon + 180.) % 360. - 180.
1009 if focp == 'center':
1010 self.state.lat = float(lat)
1011 self.state.lon = float(lon)
1012 else:
1013 self.state.dip = float(90. - lat)
1014 self.state.strike = float(-(lon + 90.))
1016 def do_dolly(self, v):
1017 self.state.distance *= float(1.0 + 0.1*v)
1019 def key_down_event(self, obj, event):
1020 k = obj.GetKeyCode()
1021 s = obj.GetKeySym()
1022 if k == 'f' or s == 'Control_L':
1023 self.gui_state.next_focal_point()
1025 elif k == 'r':
1026 self.reset_strike_dip()
1028 elif k == 'p':
1029 print(self.state)
1031 elif k == 'i':
1032 for elem in self.state.elements:
1033 if isinstance(elem, elements.IcosphereState):
1034 elem.visible = not elem.visible
1036 elif k == 'c':
1037 for elem in self.state.elements:
1038 if isinstance(elem, elements.CoastlinesState):
1039 elem.visible = not elem.visible
1041 elif k == 't':
1042 if not any(
1043 isinstance(elem, elements.TopoState)
1044 for elem in self.state.elements):
1046 self.state.elements.append(elements.TopoState())
1047 else:
1048 for elem in self.state.elements:
1049 if isinstance(elem, elements.TopoState):
1050 elem.visible = not elem.visible
1052 elif k == ' ':
1053 self.toggle_panel_visibility()
1055 def key_up_event(self, obj, event):
1056 s = obj.GetKeySym()
1057 if s == 'Control_L':
1058 self.gui_state.next_focal_point()
1060 def _state_bind(self, *args, **kwargs):
1061 vstate.state_bind(self, self.state, *args, **kwargs)
1063 def _gui_state_bind(self, *args, **kwargs):
1064 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1066 def controls_navigation(self):
1067 frame = qw.QFrame(self)
1068 frame.setSizePolicy(
1069 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1070 layout = qw.QGridLayout()
1071 frame.setLayout(layout)
1073 # lat, lon, depth
1075 layout.addWidget(
1076 qw.QLabel('Location'), 0, 0, 1, 2)
1078 le = qw.QLineEdit()
1079 le.setStatusTip(
1080 'Latitude, Longitude, Depth [km] or city name: '
1081 'Focal point location.')
1082 layout.addWidget(le, 1, 0, 1, 1)
1084 def lat_lon_depth_to_lineedit(state, widget):
1085 sel = str(widget.selectedText()) == str(widget.text())
1086 widget.setText('%g, %g, %g' % (
1087 state.lat, state.lon, state.depth / km))
1089 if sel:
1090 widget.selectAll()
1092 def lineedit_to_lat_lon_depth(widget, state):
1093 s = str(widget.text())
1094 choices = location_to_choices(s)
1095 if len(choices) > 0:
1096 self.state.lat, self.state.lon, self.state.depth = \
1097 choices[0].get_lat_lon_depth()
1098 else:
1099 raise NoLocationChoices(s)
1101 self._state_bind(
1102 ['lat', 'lon', 'depth'],
1103 lineedit_to_lat_lon_depth,
1104 le, [le.editingFinished, le.returnPressed],
1105 lat_lon_depth_to_lineedit)
1107 self.lat_lon_lineedit = le
1109 self.lat_lon_lineedit.returnPressed.connect(
1110 lambda *args: self.lat_lon_lineedit.selectAll())
1112 # focal point
1114 cb = qw.QCheckBox('Fix')
1115 cb.setStatusTip(
1116 'Fix location. Orbit focal point without pressing %s.'
1117 % g_modifier_key)
1118 layout.addWidget(cb, 1, 1, 1, 1)
1120 def focal_point_to_checkbox(state, widget):
1121 widget.blockSignals(True)
1122 widget.setChecked(self.gui_state.focal_point != 'center')
1123 widget.blockSignals(False)
1125 def checkbox_to_focal_point(widget, state):
1126 self.gui_state.focal_point = \
1127 'target' if widget.isChecked() else 'center'
1129 self._gui_state_bind(
1130 ['focal_point'], checkbox_to_focal_point,
1131 cb, [cb.toggled], focal_point_to_checkbox)
1133 self.focal_point_checkbox = cb
1135 self.talkie_connect(
1136 self.gui_state, 'focal_point', self.update_focal_point)
1138 self.update_focal_point()
1140 # strike, dip
1142 layout.addWidget(
1143 qw.QLabel('View Plane'), 2, 0, 1, 2)
1145 le = qw.QLineEdit()
1146 le.setStatusTip(
1147 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1148 'direction.')
1149 layout.addWidget(le, 3, 0, 1, 1)
1151 def strike_dip_to_lineedit(state, widget):
1152 sel = widget.selectedText() == widget.text()
1153 widget.setText('%g, %g' % (state.strike, state.dip))
1154 if sel:
1155 widget.selectAll()
1157 def lineedit_to_strike_dip(widget, state):
1158 s = str(widget.text())
1159 string_to_strike_dip = {
1160 'east': (0., 90.),
1161 'west': (180., 90.),
1162 'south': (90., 90.),
1163 'north': (270., 90.),
1164 'top': (90., 0.),
1165 'bottom': (90., 180.)}
1167 if s in string_to_strike_dip:
1168 state.strike, state.dip = string_to_strike_dip[s]
1170 s = s.replace(',', ' ')
1171 try:
1172 state.strike, state.dip = map(float, s.split())
1173 except Exception:
1174 raise ValueError('need two numerical values: <strike>, <dip>')
1176 self._state_bind(
1177 ['strike', 'dip'], lineedit_to_strike_dip,
1178 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1180 self.strike_dip_lineedit = le
1181 self.strike_dip_lineedit.returnPressed.connect(
1182 lambda *args: self.strike_dip_lineedit.selectAll())
1184 but = qw.QPushButton('Reset')
1185 but.setStatusTip('Reset to north-up map view.')
1186 but.clicked.connect(self.reset_strike_dip)
1187 layout.addWidget(but, 3, 1, 1, 1)
1189 # crosshair
1191 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1192 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1194 # camera bindings
1195 self.talkie_connect(
1196 self.state,
1197 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1198 self.update_camera)
1200 self.talkie_connect(
1201 self.gui_state, 'panels_visible', self.update_panel_visibility)
1203 return frame
1205 def controls_time(self):
1206 frame = qw.QFrame(self)
1207 frame.setSizePolicy(
1208 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1210 layout = qw.QGridLayout()
1211 frame.setLayout(layout)
1213 layout.addWidget(qw.QLabel('Min'), 0, 0)
1214 le_tmin = qw.QLineEdit()
1215 layout.addWidget(le_tmin, 0, 1)
1217 layout.addWidget(qw.QLabel('Max'), 1, 0)
1218 le_tmax = qw.QLineEdit()
1219 layout.addWidget(le_tmax, 1, 1)
1221 label_tcursor = qw.QLabel()
1223 label_tcursor.setSizePolicy(
1224 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1226 layout.addWidget(label_tcursor, 2, 1)
1227 self._label_tcursor = label_tcursor
1229 def time_to_lineedit(state, attribute, widget):
1230 sel = widget.selectedText() == widget.text() \
1231 and widget.text() != ''
1233 widget.setText(
1234 common.time_or_none_to_str(getattr(state, attribute)))
1236 if sel:
1237 widget.selectAll()
1239 def lineedit_to_time(widget, state, attribute):
1240 from pyrocko.util import str_to_time_fillup
1242 s = str(widget.text())
1243 if not s.strip():
1244 setattr(state, attribute, None)
1245 else:
1246 try:
1247 setattr(state, attribute, str_to_time_fillup(s))
1248 except Exception:
1249 raise ValueError(
1250 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1252 self._state_bind(
1253 ['tmin'], lineedit_to_time, le_tmin,
1254 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1255 attribute='tmin')
1256 self._state_bind(
1257 ['tmax'], lineedit_to_time, le_tmax,
1258 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1259 attribute='tmax')
1261 self.tmin_lineedit = le_tmin
1262 self.tmax_lineedit = le_tmax
1264 range_edit = RangeEdit()
1265 range_edit.set_data_provider(self)
1266 range_edit.set_data_name('time')
1268 xblock = [False]
1270 def range_to_range_edit(state, widget):
1271 if not xblock[0]:
1272 widget.blockSignals(True)
1273 widget.set_focus(state.tduration, state.tposition)
1274 widget.set_range(state.tmin, state.tmax)
1275 widget.blockSignals(False)
1277 def range_edit_to_range(widget, state):
1278 xblock[0] = True
1279 self.state.tduration, self.state.tposition = widget.get_focus()
1280 self.state.tmin, self.state.tmax = widget.get_range()
1281 xblock[0] = False
1283 self._state_bind(
1284 ['tmin', 'tmax', 'tduration', 'tposition'],
1285 range_edit_to_range,
1286 range_edit,
1287 [range_edit.rangeChanged, range_edit.focusChanged],
1288 range_to_range_edit)
1290 def handle_tcursor_changed():
1291 self.gui_state.tcursor = range_edit.get_tcursor()
1293 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1295 layout.addWidget(range_edit, 3, 0, 1, 2)
1297 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1298 le_focus = qw.QLineEdit()
1299 layout.addWidget(le_focus, 4, 1)
1301 def focus_to_lineedit(state, widget):
1302 sel = widget.selectedText() == widget.text() \
1303 and widget.text() != ''
1305 if state.tduration is None:
1306 widget.setText('')
1307 else:
1308 widget.setText('%s, %g' % (
1309 guts.str_duration(state.tduration),
1310 state.tposition))
1312 if sel:
1313 widget.selectAll()
1315 def lineedit_to_focus(widget, state):
1316 s = str(widget.text())
1317 w = [x.strip() for x in s.split(',')]
1318 try:
1319 if len(w) == 0 or not w[0]:
1320 state.tduration = None
1321 state.tposition = 0.0
1322 else:
1323 state.tduration = guts.parse_duration(w[0])
1324 if len(w) > 1:
1325 state.tposition = float(w[1])
1326 else:
1327 state.tposition = 0.0
1329 except Exception:
1330 raise ValueError('need two values: <duration>, <position>')
1332 self._state_bind(
1333 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1334 [le_focus.editingFinished, le_focus.returnPressed],
1335 focus_to_lineedit)
1337 label_effective_tmin = qw.QLabel()
1338 label_effective_tmax = qw.QLabel()
1340 label_effective_tmin.setSizePolicy(
1341 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1342 label_effective_tmax.setSizePolicy(
1343 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1344 label_effective_tmin.setMinimumSize(
1345 qg.QFontMetrics(label_effective_tmin.font()).width(
1346 '0000-00-00 00:00:00.000 '), 0)
1348 layout.addWidget(label_effective_tmin, 5, 1)
1349 layout.addWidget(label_effective_tmax, 6, 1)
1351 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1352 self.talkie_connect(
1353 self.state, var, self.update_effective_time_labels)
1355 self._label_effective_tmin = label_effective_tmin
1356 self._label_effective_tmax = label_effective_tmax
1358 self.talkie_connect(
1359 self.gui_state, 'tcursor', self.update_tcursor)
1361 return frame
1363 def controls_appearance(self):
1364 frame = qw.QFrame(self)
1365 frame.setSizePolicy(
1366 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1367 layout = qw.QGridLayout()
1368 frame.setLayout(layout)
1370 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1372 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1373 layout.addWidget(cb, 0, 1)
1374 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1376 self.talkie_connect(
1377 self.state, 'lighting', self.update_render_settings)
1379 # background
1381 layout.addWidget(qw.QLabel('Background'), 1, 0)
1383 cb = common.strings_to_combobox(
1384 ['black', 'white', 'skyblue1 - white'])
1386 layout.addWidget(cb, 1, 1)
1387 vstate.state_bind_combobox_background(
1388 self, self.state, 'background', cb)
1390 self.talkie_connect(
1391 self.state, 'background', self.update_render_settings)
1393 return frame
1395 def controls_snapshots(self):
1396 return snapshots_mod.SnapshotsPanel(self)
1398 def update_effective_time_labels(self, *args):
1399 tmin = self.state.tmin_effective
1400 tmax = self.state.tmax_effective
1402 stmin = common.time_or_none_to_str(tmin)
1403 stmax = common.time_or_none_to_str(tmax)
1405 self._label_effective_tmin.setText(stmin)
1406 self._label_effective_tmax.setText(stmax)
1408 def update_tcursor(self, *args):
1409 tcursor = self.gui_state.tcursor
1410 stcursor = common.time_or_none_to_str(tcursor)
1411 self._label_tcursor.setText(stcursor)
1413 def reset_strike_dip(self, *args):
1414 self.state.strike = 90.
1415 self.state.dip = 0
1416 self.gui_state.focal_point = 'center'
1418 def get_camera_geometry(self):
1420 def rtp2xyz(rtp):
1421 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1423 radius = 1.0 - self.state.depth / self.planet_radius
1425 cam_rtp = num.array([
1426 radius+self.state.distance,
1427 self.state.lat * d2r + 0.5*num.pi,
1428 self.state.lon * d2r])
1429 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1430 cam, up, foc = \
1431 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1433 foc_rtp = num.array([
1434 radius,
1435 self.state.lat * d2r + 0.5*num.pi,
1436 self.state.lon * d2r])
1438 foc = rtp2xyz(foc_rtp)
1440 rot_world = pmt.euler_to_matrix(
1441 -(self.state.lat-90.)*d2r,
1442 (self.state.lon+90.)*d2r,
1443 0.0*d2r).T
1445 rot_cam = pmt.euler_to_matrix(
1446 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1448 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1450 cam = foc + num.dot(rot, cam - foc)
1451 up = num.dot(rot, up)
1452 return cam, up, foc
1454 def update_camera(self, *args):
1455 cam, up, foc = self.get_camera_geometry()
1456 camera = self.ren.GetActiveCamera()
1457 camera.SetPosition(*cam)
1458 camera.SetFocalPoint(*foc)
1459 camera.SetViewUp(*up)
1461 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1463 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1464 self.feature_radius_min / self.planet_radius)**2))
1466 # if horizon == 0.0:
1467 # horizon = 2.0 + self.state.distance
1469 # clip_dist = max(min(self.state.distance*5., max(
1470 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1471 # , math.sqrt(num.sum(cam**2)))
1472 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1473 # clip_dist = feature_horizon
1475 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1477 self.camera_params = (
1478 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1480 self.update_view()
1482 def add_panel(
1483 self, title_label, panel,
1484 visible=False,
1485 # volatile=False,
1486 tabify=True,
1487 where=qc.Qt.RightDockWidgetArea,
1488 remove=None,
1489 title_controls=[]):
1491 dockwidget = common.MyDockWidget(
1492 self, title_label, title_controls=title_controls)
1494 if not visible:
1495 dockwidget.hide()
1497 if not self.gui_state.panels_visible:
1498 dockwidget.block()
1500 dockwidget.setWidget(panel)
1502 panel.setParent(dockwidget)
1504 dockwidgets = self.findChildren(common.MyDockWidget)
1505 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1507 self.addDockWidget(where, dockwidget)
1509 nwrap = 4
1510 if dws and len(dws) >= nwrap and tabify:
1511 self.tabifyDockWidget(
1512 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1514 mitem = dockwidget.toggleViewAction()
1516 def update_label(*args):
1517 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1518 self.update_slug_abbreviated_lengths()
1520 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1521 dockwidget.titlebar._title_label.title_changed.connect(
1522 self.update_slug_abbreviated_lengths)
1524 update_label()
1526 self._panel_togglers[dockwidget] = mitem
1527 self.panels_menu.addAction(mitem)
1528 if visible:
1529 dockwidget.setVisible(True)
1530 dockwidget.setFocus()
1531 dockwidget.raise_()
1533 def update_slug_abbreviated_lengths(self):
1534 dockwidgets = self.findChildren(common.MyDockWidget)
1535 title_labels = []
1536 for dw in dockwidgets:
1537 title_labels.append(dw.titlebar._title_label)
1539 by_title = defaultdict(list)
1540 for tl in title_labels:
1541 by_title[tl.get_title()].append(tl)
1543 for group in by_title.values():
1544 slugs = [tl.get_slug() for tl in group]
1546 n = max(len(slug) for slug in slugs)
1547 nunique = len(set(slugs))
1549 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1550 n -= 1
1552 if n > 0:
1553 n = max(3, n)
1555 for tl in group:
1556 tl.set_slug_abbreviated_length(n)
1558 def raise_panel(self, panel):
1559 dockwidget = panel.parent()
1560 dockwidget.setVisible(True)
1561 dockwidget.setFocus()
1562 dockwidget.raise_()
1564 def toggle_panel_visibility(self):
1565 self.gui_state.panels_visible = not self.gui_state.panels_visible
1567 def update_panel_visibility(self, *args):
1568 self.setUpdatesEnabled(False)
1569 mbar = self.menuBar()
1570 dockwidgets = self.findChildren(common.MyDockWidget)
1572 mbar.setVisible(self.gui_state.panels_visible)
1573 for dockwidget in dockwidgets:
1574 dockwidget.setBlocked(not self.gui_state.panels_visible)
1576 self.setUpdatesEnabled(True)
1578 def remove_panel(self, panel):
1579 dockwidget = panel.parent()
1580 self.removeDockWidget(dockwidget)
1581 dockwidget.setParent(None)
1582 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1584 def register_data_provider(self, provider):
1585 if provider not in self.data_providers:
1586 self.data_providers.append(provider)
1588 def unregister_data_provider(self, provider):
1589 if provider in self.data_providers:
1590 self.data_providers.remove(provider)
1592 def iter_data(self, name):
1593 for provider in self.data_providers:
1594 for data in provider.iter_data(name):
1595 yield data
1597 def closeEvent(self, event):
1598 self.attach()
1599 event.accept()
1600 self.closing = True
1601 common.get_app().set_main_window(None)
1603 def is_closing(self):
1604 return self.closing
1607class SparrowApp(qw.QApplication):
1608 def __init__(self):
1609 qw.QApplication.__init__(self, ['Sparrow'])
1610 self.lastWindowClosed.connect(self.myQuit)
1611 self._main_window = None
1612 self.setApplicationDisplayName('Sparrow')
1613 self.setDesktopFileName('Sparrow')
1615 def install_sigint_handler(self):
1616 self._old_signal_handler = signal.signal(
1617 signal.SIGINT, self.myCloseAllWindows)
1619 def uninstall_sigint_handler(self):
1620 signal.signal(signal.SIGINT, self._old_signal_handler)
1622 def myQuit(self, *args):
1623 self.quit()
1625 def myCloseAllWindows(self, *args):
1626 self.closeAllWindows()
1628 def set_main_window(self, win):
1629 self._main_window = win
1631 def get_main_window(self):
1632 return self._main_window
1634 def get_progressbars(self):
1635 if self._main_window:
1636 return self._main_window.progressbars
1637 else:
1638 return None
1640 def status(self, message, duration=None):
1641 win = self.get_main_window()
1642 if not win:
1643 return
1645 win.statusBar().showMessage(
1646 message, int((duration or 0) * 1000))
1649def main(*args, **kwargs):
1651 from pyrocko import util
1652 from pyrocko.gui import util as gui_util
1653 util.setup_logging('sparrow', 'info')
1655 global win
1657 if gui_util.app is None:
1658 gui_util.app = SparrowApp()
1660 # try:
1661 # from qt_material import apply_stylesheet
1662 #
1663 # apply_stylesheet(app, theme='dark_teal.xml')
1664 #
1665 #
1666 # import qdarkgraystyle
1667 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1668 # import qdarkstyle
1669 #
1670 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1671 #
1672 #
1673 # except ImportError:
1674 # logger.info(
1675 # 'Module qdarkgraystyle not available.\n'
1676 # 'If wanted, install qdarkstyle with "pip install '
1677 # 'qdarkgraystyle".')
1678 #
1679 win = SparrowViewer(*args, **kwargs)
1681 gui_util.app.install_sigint_handler()
1682 gui_util.app.exec_()
1683 gui_util.app.uninstall_sigint_handler()
1685 del win
1687 gc.collect()
1689 del gui_util.app