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 Subtitle', elements.HudState(
323 template='Subtitle')),
324 ('HUD (tmax_effective)', elements.HudState(
325 template='tmax: {tmax_effective|date}',
326 position='top-left')),
327 ('Volcanoes', elements.VolcanoesState()),
328 ('Faults', elements.ActiveFaultsState()),
329 ('Plate bounds', elements.PlatesBoundsState()),
330 ('InSAR Surface Displacements', elements.KiteState()),
331 ('Geometry', elements.GeometryState()),
332 ('Spheroid', elements.SpheroidState()),
333 ('Rays', elements.RaysState())]:
335 def wrap_add_element(estate):
336 def add_element(*args):
337 new_element = guts.clone(estate)
338 new_element.element_id = elements.random_id()
339 self.state.elements.append(new_element)
340 self.state.sort_elements()
342 return add_element
344 mitem = qw.QAction(name, self)
346 mitem.triggered.connect(wrap_add_element(estate))
348 menu.addAction(mitem)
350 self.data_providers = []
351 self.elements = {}
353 self.detached_window = None
355 self.main_frame = qw.QFrame()
356 self.main_frame.setFrameShape(qw.QFrame.NoFrame)
358 self.vtk_frame = CenteringScrollArea()
360 self.vtk_widget = QVTKWidget(self, self)
361 self.vtk_frame.setWidget(self.vtk_widget)
363 self.main_layout = qw.QVBoxLayout()
364 self.main_layout.setContentsMargins(0, 0, 0, 0)
365 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter)
367 pb = Progressbars(self)
368 self.progressbars = pb
369 self.main_layout.addWidget(pb)
371 self.main_frame.setLayout(self.main_layout)
373 self.vtk_frame_substitute = None
375 self.add_panel(
376 'Navigation',
377 self.controls_navigation(), visible=True,
378 where=qc.Qt.LeftDockWidgetArea)
380 self.add_panel(
381 'Time',
382 self.controls_time(), visible=True,
383 where=qc.Qt.LeftDockWidgetArea)
385 self.add_panel(
386 'Appearance',
387 self.controls_appearance(), visible=True,
388 where=qc.Qt.LeftDockWidgetArea)
390 snapshots_panel = self.controls_snapshots()
391 self.add_panel(
392 'Snapshots',
393 snapshots_panel, visible=False,
394 where=qc.Qt.LeftDockWidgetArea)
396 menu = mbar.addMenu('Snapshots')
397 menu.addAction(
398 'Next',
399 snapshots_panel.transition_to_next_snapshot,
400 qg.QKeySequence(qc.Qt.Key_PageDown)).setShortcutContext(
401 qc.Qt.ApplicationShortcut)
403 menu.addAction(
404 'Previous',
405 snapshots_panel.transition_to_previous_snapshot,
406 qg.QKeySequence(qc.Qt.Key_PageUp)).setShortcutContext(
407 qc.Qt.ApplicationShortcut)
409 self.setCentralWidget(self.main_frame)
411 self.mesh = None
413 ren = vtk.vtkRenderer()
415 # ren.SetBackground(0.15, 0.15, 0.15)
416 # ren.SetBackground(0.0, 0.0, 0.0)
417 # ren.TwoSidedLightingOn()
418 # ren.SetUseShadows(1)
420 self._lighting = None
421 self._background = None
423 self.ren = ren
424 self.update_render_settings()
425 self.update_camera()
427 renwin = self.vtk_widget.GetRenderWindow()
429 if self._use_depth_peeling:
430 renwin.SetAlphaBitPlanes(1)
431 renwin.SetMultiSamples(0)
433 ren.SetUseDepthPeeling(1)
434 ren.SetMaximumNumberOfPeels(100)
435 ren.SetOcclusionRatio(0.1)
437 ren.SetUseFXAA(1)
438 # ren.SetUseHiddenLineRemoval(1)
439 # ren.SetBackingStore(1)
441 self.renwin = renwin
443 # renwin.LineSmoothingOn()
444 # renwin.PointSmoothingOn()
445 # renwin.PolygonSmoothingOn()
447 renwin.AddRenderer(ren)
449 iren = renwin.GetInteractor()
450 iren.LightFollowCameraOn()
451 iren.SetInteractorStyle(None)
453 iren.AddObserver('LeftButtonPressEvent', self.button_event)
454 iren.AddObserver('LeftButtonReleaseEvent', self.button_event)
455 iren.AddObserver('MiddleButtonPressEvent', self.button_event)
456 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event)
457 iren.AddObserver('RightButtonPressEvent', self.button_event)
458 iren.AddObserver('RightButtonReleaseEvent', self.button_event)
459 iren.AddObserver('MouseMoveEvent', self.mouse_move_event)
460 iren.AddObserver('KeyPressEvent', self.key_down_event)
461 iren.AddObserver('KeyReleaseEvent', self.key_up_event)
462 iren.AddObserver('ModifiedEvent', self.check_vtk_resize)
464 renwin.Render()
466 iren.Initialize()
468 self.iren = iren
470 self.rotating = False
472 self._elements = {}
473 self._elements_active = {}
475 self.talkie_connect(
476 self.state, 'elements', self.update_elements)
478 self.state.elements.append(elements.IcosphereState(
479 element_id='icosphere',
480 level=4,
481 smooth=True,
482 opacity=0.5,
483 ambient=0.1))
485 self.state.elements.append(elements.GridState(
486 element_id='grid'))
487 self.state.elements.append(elements.CoastlinesState(
488 element_id='coastlines'))
489 self.state.elements.append(elements.CrosshairState(
490 element_id='crosshair'))
492 # self.state.elements.append(elements.StationsState())
493 # self.state.elements.append(elements.SourceState())
494 # self.state.elements.append(
495 # elements.CatalogState(
496 # selection=elements.FileCatalogSelection(paths=['japan.dat'])))
497 # selection=elements.FileCatalogSelection(paths=['excerpt.dat'])))
499 if events:
500 self.state.elements.append(
501 elements.CatalogState(
502 selection=elements.MemoryCatalogSelection(events=events)))
504 self.state.sort_elements()
506 if snapshots:
507 snapshots_ = []
508 for obj in snapshots:
509 if isinstance(obj, str):
510 snapshots_.extend(snapshots_mod.load_snapshots(obj))
511 else:
512 snapshots_.append(obj)
514 snapshots_panel.add_snapshots(snapshots_)
515 self.raise_panel(snapshots_panel)
516 snapshots_panel.goto_snapshot(1)
518 self.timer = qc.QTimer(self)
519 self.timer.timeout.connect(self.periodical)
520 self.timer.setInterval(1000)
521 self.timer.start()
523 self._animation_saver = None
525 self.closing = False
526 self.vtk_widget.setFocus()
528 self.update_detached()
530 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0)
531 common.get_app().status('Let\'s fly.', 2.0)
533 self.show()
534 self.windowHandle().showMaximized()
536 self.talkie_connect(
537 self.gui_state, 'fixed_size', self.update_vtk_widget_size)
539 self.update_vtk_widget_size()
541 def _add_vtk_widget_size_menu_entries(self, menu):
543 group = qw.QActionGroup(menu)
544 group.setExclusive(True)
546 def set_variable_size():
547 self.gui_state.fixed_size = False
549 variable_size_action = menu.addAction('Fit Window Size')
550 variable_size_action.setCheckable(True)
551 variable_size_action.setActionGroup(group)
552 variable_size_action.triggered.connect(set_variable_size)
554 fixed_size_items = []
555 for nx, ny, label in [
556 (None, None, 'Aspect 16:9 (e.g. for YouTube)'),
557 (426, 240, ''),
558 (640, 360, ''),
559 (854, 480, '(FWVGA)'),
560 (1280, 720, '(HD)'),
561 (1920, 1080, '(Full HD)'),
562 (2560, 1440, '(Quad HD)'),
563 (3840, 2160, '(4K UHD)'),
564 (3840*2, 2160*2, '',),
565 (None, None, 'Aspect 4:3'),
566 (640, 480, '(VGA)'),
567 (800, 600, '(SVGA)'),
568 (None, None, 'Other'),
569 (512, 512, ''),
570 (1024, 1024, '')]:
572 if None in (nx, ny):
573 menu.addSection(label)
574 else:
575 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '')
576 action = menu.addAction(name)
577 action.setCheckable(True)
578 action.setActionGroup(group)
579 fixed_size_items.append((action, (nx, ny)))
581 def make_set_fixed_size(nx, ny):
582 def set_fixed_size():
583 self.gui_state.fixed_size = (float(nx), float(ny))
585 return set_fixed_size
587 action.triggered.connect(make_set_fixed_size(nx, ny))
589 def update_widget(*args):
590 for action, (nx, ny) in fixed_size_items:
591 action.blockSignals(True)
592 action.setChecked(
593 bool(self.gui_state.fixed_size and (nx, ny) == tuple(
594 int(z) for z in self.gui_state.fixed_size)))
595 action.blockSignals(False)
597 variable_size_action.blockSignals(True)
598 variable_size_action.setChecked(not self.gui_state.fixed_size)
599 variable_size_action.blockSignals(False)
601 update_widget()
602 self.talkie_connect(
603 self.gui_state, 'fixed_size', update_widget)
605 def update_vtk_widget_size(self, *args):
606 if self.gui_state.fixed_size:
607 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size)
608 wanted_size = qc.QSize(nx, ny)
609 else:
610 wanted_size = qc.QSize(
611 self.vtk_frame.window().width(), self.vtk_frame.height())
613 current_size = self.vtk_widget.size()
615 if current_size.width() != wanted_size.width() \
616 or current_size.height() != wanted_size.height():
618 self.vtk_widget.setFixedSize(wanted_size)
620 self.vtk_frame.recenter()
621 self.check_vtk_resize()
623 def update_focal_point(self, *args):
624 if self.gui_state.focal_point == 'center':
625 self.vtk_widget.setStatusTip(
626 'Click and drag: change location. %s-click and drag: '
627 'change view plane orientation.' % g_modifier_key)
628 else:
629 self.vtk_widget.setStatusTip(
630 '%s-click and drag: change location. Click and drag: '
631 'change view plane orientation. Uncheck "Navigation: Fix" to '
632 'reverse sense.' % g_modifier_key)
634 def update_detached(self, *args):
636 if self.gui_state.detached and not self.detached_window: # detach
637 logger.debug('Detaching VTK view.')
639 self.main_layout.removeWidget(self.vtk_frame)
640 self.detached_window = DetachedViewer(self, self.vtk_frame)
641 self.detached_window.show()
642 self.vtk_widget.setFocus()
644 screens = common.get_app().screens()
645 if len(screens) > 1:
646 for screen in screens:
647 if screen is not self.screen():
648 self.detached_window.windowHandle().setScreen(screen)
649 # .setScreen() does not work reliably,
650 # therefore trying also with .move()...
651 p = screen.geometry().topLeft()
652 self.detached_window.move(p.x() + 50, p.y() + 50)
653 # ... but also does not work in notion window manager.
655 self.detached_window.windowHandle().showMaximized()
657 frame = qw.QFrame()
658 # frame.setFrameShape(qw.QFrame.NoFrame)
659 # frame.setBackgroundRole(qg.QPalette.Mid)
660 # frame.setAutoFillBackground(True)
661 frame.setSizePolicy(
662 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
664 layout = qw.QGridLayout()
665 frame.setLayout(layout)
666 self.main_layout.insertWidget(0, frame)
668 self.state_editor = StateEditor(self)
670 layout.addWidget(self.state_editor, 0, 0)
672 # attach_button = qw.QPushButton('Attach View')
673 # attach_button.clicked.connect(self.attach)
674 # layout.addWidget(
675 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter)
677 self.vtk_frame_substitute = frame
679 if not self.gui_state.detached and self.detached_window: # attach
680 logger.debug('Attaching VTK view.')
681 self.detached_window.hide()
682 self.vtk_frame.setParent(self)
683 if self.vtk_frame_substitute:
684 self.main_layout.removeWidget(self.vtk_frame_substitute)
685 self.state_editor.unbind_state()
686 self.vtk_frame_substitute = None
688 self.main_layout.insertWidget(0, self.vtk_frame)
689 self.detached_window = None
690 self.vtk_widget.setFocus()
692 def attach(self):
693 self.gui_state.detached = False
695 def export_image(self):
697 caption = 'Export Image'
698 fn_out, _ = qw.QFileDialog.getSaveFileName(
699 self, caption, 'image.png',
700 options=common.qfiledialog_options)
702 if fn_out:
703 self.save_image(fn_out)
705 def save_image(self, path):
707 original_fixed_size = self.gui_state.fixed_size
708 if original_fixed_size is None:
709 self.gui_state.fixed_size = (1920., 1080.)
711 wif = vtk.vtkWindowToImageFilter()
712 wif.SetInput(self.renwin)
713 wif.SetInputBufferTypeToRGBA()
714 wif.SetScale(1, 1)
715 wif.ReadFrontBufferOff()
716 writer = vtk.vtkPNGWriter()
717 writer.SetInputConnection(wif.GetOutputPort())
719 self.renwin.Render()
720 wif.Modified()
721 writer.SetFileName(path)
722 writer.Write()
724 self.gui_state.fixed_size = original_fixed_size
726 def update_render_settings(self, *args):
727 if self._lighting is None or self._lighting != self.state.lighting:
728 self.ren.RemoveAllLights()
729 for li in light.get_lights(self.state.lighting):
730 self.ren.AddLight(li)
732 self._lighting = self.state.lighting
734 if self._background is None \
735 or self._background != self.state.background:
737 self.state.background.vtk_apply(self.ren)
738 self._background = self.state.background
740 self.update_view()
742 def start_animation(self, interpolator, output_path=None):
743 self._animation = interpolator
744 if output_path is None:
745 self._animation_tstart = time.time()
746 self._animation_iframe = None
747 else:
748 self._animation_iframe = 0
749 self.showFullScreen()
750 self.update_view()
751 self.gui_state.panels_visible = False
752 self.update_view()
754 self._animation_timer = qc.QTimer(self)
755 self._animation_timer.timeout.connect(self.next_animation_frame)
756 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.)))
757 self._animation_timer.start()
758 if output_path is not None:
759 original_fixed_size = self.gui_state.fixed_size
760 if original_fixed_size is None:
761 self.gui_state.fixed_size = (1920., 1080.)
763 wif = vtk.vtkWindowToImageFilter()
764 wif.SetInput(self.renwin)
765 wif.SetInputBufferTypeToRGBA()
766 wif.SetScale(1, 1)
767 wif.ReadFrontBufferOff()
768 writer = vtk.vtkPNGWriter()
769 temp_path = tempfile.mkdtemp()
770 self._animation_saver = (
771 wif, writer, temp_path, output_path, original_fixed_size)
772 writer.SetInputConnection(wif.GetOutputPort())
774 def next_animation_frame(self):
776 ani = self._animation
777 if not ani:
778 return
780 if self._animation_iframe is not None:
781 state = ani(
782 ani.tmin
783 + self._animation_iframe * ani.dt)
785 self._animation_iframe += 1
786 else:
787 tnow = time.time()
788 state = ani(min(
789 ani.tmax,
790 ani.tmin + (tnow - self._animation_tstart)))
792 self.set_state(state)
793 self.renwin.Render()
794 if self._animation_saver:
795 wif, writer, temp_path, _, _ = self._animation_saver
796 wif.Modified()
797 fn = os.path.join(temp_path, 'f%09i.png')
798 writer.SetFileName(fn % self._animation_iframe)
799 writer.Write()
801 if self._animation_iframe is not None:
802 t = self._animation_iframe * ani.dt
803 else:
804 t = tnow - self._animation_tstart
806 if t > ani.tmax - ani.tmin:
807 self.stop_animation()
809 def stop_animation(self):
810 if self._animation_timer:
811 self._animation_timer.stop()
813 if self._animation_saver:
815 wif, writer, temp_path, output_path, original_fixed_size \
816 = self._animation_saver
817 self.gui_state.fixed_size = original_fixed_size
819 fn_path = os.path.join(temp_path, 'f%09d.png')
820 check_call([
821 'ffmpeg', '-y',
822 '-i', fn_path,
823 '-c:v', 'libx264',
824 '-preset', 'slow',
825 '-crf', '17',
826 '-vf', 'format=yuv420p,fps=%i' % (
827 int(round(1.0/self._animation.dt))),
828 output_path])
829 shutil.rmtree(temp_path)
831 self._animation_saver = None
832 self._animation_saver
834 self.showNormal()
835 self.gui_state.panels_visible = True
837 self._animation_tstart = None
838 self._animation_iframe = None
839 self._animation = None
841 def set_state(self, state):
842 self._update_elements_enabled = False
843 self.setUpdatesEnabled(False)
844 self.state.diff_update(state)
845 self.state.sort_elements()
846 self.setUpdatesEnabled(True)
847 self._update_elements_enabled = True
848 self.update_elements()
850 def periodical(self):
851 pass
853 def request_quit(self):
854 app = common.get_app()
855 app.myQuit()
857 def check_vtk_resize(self, *args):
858 render_window_size = self.renwin.GetSize()
859 if self._render_window_size != render_window_size:
860 self._render_window_size = render_window_size
861 self.resize_event(*render_window_size)
863 def update_elements(self, *_):
864 if not self._update_elements_enabled:
865 return
867 if self._in_update_elements:
868 return
870 self._in_update_elements = True
871 for estate in self.state.elements:
872 if estate.element_id not in self._elements:
873 new_element = estate.create()
874 logger.debug('Creating "%s" ("%s").' % (
875 type(new_element).__name__,
876 estate.element_id))
877 self._elements[estate.element_id] = new_element
879 element = self._elements[estate.element_id]
881 if estate.element_id not in self._elements_active:
882 logger.debug('Adding "%s" ("%s")' % (
883 type(element).__name__,
884 estate.element_id))
885 element.bind_state(estate)
886 element.set_parent(self)
887 self._elements_active[estate.element_id] = element
889 state_element_ids = [el.element_id for el in self.state.elements]
890 deactivate = []
891 for element_id, element in self._elements_active.items():
892 if element_id not in state_element_ids:
893 logger.debug('Removing "%s" ("%s").' % (
894 type(element).__name__,
895 element_id))
896 element.unset_parent()
897 deactivate.append(element_id)
899 for element_id in deactivate:
900 del self._elements_active[element_id]
902 self._update_crosshair_bindings()
904 self._in_update_elements = False
906 def _update_crosshair_bindings(self):
908 def get_crosshair_element():
909 for element in self.state.elements:
910 if element.element_id == 'crosshair':
911 return element
913 return None
915 crosshair = get_crosshair_element()
916 if crosshair is None or crosshair.is_connected:
917 return
919 def to_checkbox(state, widget):
920 widget.blockSignals(True)
921 widget.setChecked(state.visible)
922 widget.blockSignals(False)
924 def to_state(widget, state):
925 state.visible = widget.isChecked()
927 cb = self._crosshair_checkbox
928 vstate.state_bind(
929 self, crosshair, ['visible'], to_state,
930 cb, [cb.toggled], to_checkbox)
932 crosshair.is_connected = True
934 def add_actor_2d(self, actor):
935 if actor not in self._actors_2d:
936 self.ren.AddActor2D(actor)
937 self._actors_2d.add(actor)
939 def remove_actor_2d(self, actor):
940 if actor in self._actors_2d:
941 self.ren.RemoveActor2D(actor)
942 self._actors_2d.remove(actor)
944 def add_actor(self, actor):
945 if actor not in self._actors:
946 self.ren.AddActor(actor)
947 self._actors.add(actor)
949 def add_actor_list(self, actorlist):
950 for actor in actorlist:
951 self.add_actor(actor)
953 def remove_actor(self, actor):
954 if actor in self._actors:
955 self.ren.RemoveActor(actor)
956 self._actors.remove(actor)
958 def update_view(self):
959 self.vtk_widget.update()
961 def resize_event(self, size_x, size_y):
962 self.gui_state.size = (size_x, size_y)
964 def button_event(self, obj, event):
965 if event == "LeftButtonPressEvent":
966 self.rotating = True
967 elif event == "LeftButtonReleaseEvent":
968 self.rotating = False
970 def mouse_move_event(self, obj, event):
971 x0, y0 = self.iren.GetLastEventPosition()
972 x, y = self.iren.GetEventPosition()
974 size_x, size_y = self.renwin.GetSize()
975 center_x = size_x / 2.0
976 center_y = size_y / 2.0
978 if self.rotating:
979 self.do_rotate(x, y, x0, y0, center_x, center_y)
981 def myWheelEvent(self, event):
983 angle = event.angleDelta().y()
985 if angle > 200:
986 angle = 200
988 if angle < -200:
989 angle = -200
991 self.do_dolly(-angle/100.)
993 def do_rotate(self, x, y, x0, y0, center_x, center_y):
995 dx = x0 - x
996 dy = y0 - y
998 phi = d2r*(self.state.strike - 90.)
999 focp = self.gui_state.focal_point
1001 if focp == 'center':
1002 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \
1003 - math.sin(phi) * dx + math.cos(phi) * dy
1005 lat = self.state.lat
1006 lon = self.state.lon
1007 factor = self.state.distance / 10.0
1008 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance))
1009 else:
1010 lat = 90. - self.state.dip
1011 lon = -self.state.strike - 90.
1012 factor = 0.5
1013 factor_lat = 1.0
1015 dlat = dy * factor
1016 dlon = dx * factor * factor_lat
1018 lat = max(min(lat + dlat, 90.), -90.)
1019 lon += dlon
1020 lon = (lon + 180.) % 360. - 180.
1022 if focp == 'center':
1023 self.state.lat = float(lat)
1024 self.state.lon = float(lon)
1025 else:
1026 self.state.dip = float(90. - lat)
1027 self.state.strike = float(-(lon + 90.))
1029 def do_dolly(self, v):
1030 self.state.distance *= float(1.0 + 0.1*v)
1032 def key_down_event(self, obj, event):
1033 k = obj.GetKeyCode()
1034 s = obj.GetKeySym()
1035 if k == 'f' or s == 'Control_L':
1036 self.gui_state.next_focal_point()
1038 elif k == 'r':
1039 self.reset_strike_dip()
1041 elif k == 'p':
1042 print(self.state)
1044 elif k == 'i':
1045 for elem in self.state.elements:
1046 if isinstance(elem, elements.IcosphereState):
1047 elem.visible = not elem.visible
1049 elif k == 'c':
1050 for elem in self.state.elements:
1051 if isinstance(elem, elements.CoastlinesState):
1052 elem.visible = not elem.visible
1054 elif k == 't':
1055 if not any(
1056 isinstance(elem, elements.TopoState)
1057 for elem in self.state.elements):
1059 self.state.elements.append(elements.TopoState())
1060 else:
1061 for elem in self.state.elements:
1062 if isinstance(elem, elements.TopoState):
1063 elem.visible = not elem.visible
1065 elif k == ' ':
1066 self.toggle_panel_visibility()
1068 def key_up_event(self, obj, event):
1069 s = obj.GetKeySym()
1070 if s == 'Control_L':
1071 self.gui_state.next_focal_point()
1073 def _state_bind(self, *args, **kwargs):
1074 vstate.state_bind(self, self.state, *args, **kwargs)
1076 def _gui_state_bind(self, *args, **kwargs):
1077 vstate.state_bind(self, self.gui_state, *args, **kwargs)
1079 def controls_navigation(self):
1080 frame = qw.QFrame(self)
1081 frame.setSizePolicy(
1082 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1083 layout = qw.QGridLayout()
1084 frame.setLayout(layout)
1086 # lat, lon, depth
1088 layout.addWidget(
1089 qw.QLabel('Location'), 0, 0, 1, 2)
1091 le = qw.QLineEdit()
1092 le.setStatusTip(
1093 'Latitude, Longitude, Depth [km] or city name: '
1094 'Focal point location.')
1095 layout.addWidget(le, 1, 0, 1, 1)
1097 def lat_lon_depth_to_lineedit(state, widget):
1098 sel = str(widget.selectedText()) == str(widget.text())
1099 widget.setText('%g, %g, %g' % (
1100 state.lat, state.lon, state.depth / km))
1102 if sel:
1103 widget.selectAll()
1105 def lineedit_to_lat_lon_depth(widget, state):
1106 s = str(widget.text())
1107 choices = location_to_choices(s)
1108 if len(choices) > 0:
1109 self.state.lat, self.state.lon, self.state.depth = \
1110 choices[0].get_lat_lon_depth()
1111 else:
1112 raise NoLocationChoices(s)
1114 self._state_bind(
1115 ['lat', 'lon', 'depth'],
1116 lineedit_to_lat_lon_depth,
1117 le, [le.editingFinished, le.returnPressed],
1118 lat_lon_depth_to_lineedit)
1120 self.lat_lon_lineedit = le
1122 self.lat_lon_lineedit.returnPressed.connect(
1123 lambda *args: self.lat_lon_lineedit.selectAll())
1125 # focal point
1127 cb = qw.QCheckBox('Fix')
1128 cb.setStatusTip(
1129 'Fix location. Orbit focal point without pressing %s.'
1130 % g_modifier_key)
1131 layout.addWidget(cb, 1, 1, 1, 1)
1133 def focal_point_to_checkbox(state, widget):
1134 widget.blockSignals(True)
1135 widget.setChecked(self.gui_state.focal_point != 'center')
1136 widget.blockSignals(False)
1138 def checkbox_to_focal_point(widget, state):
1139 self.gui_state.focal_point = \
1140 'target' if widget.isChecked() else 'center'
1142 self._gui_state_bind(
1143 ['focal_point'], checkbox_to_focal_point,
1144 cb, [cb.toggled], focal_point_to_checkbox)
1146 self.focal_point_checkbox = cb
1148 self.talkie_connect(
1149 self.gui_state, 'focal_point', self.update_focal_point)
1151 self.update_focal_point()
1153 # strike, dip
1155 layout.addWidget(
1156 qw.QLabel('View Plane'), 2, 0, 1, 2)
1158 le = qw.QLineEdit()
1159 le.setStatusTip(
1160 'Strike, Dip [deg]: View plane orientation, perpendicular to view '
1161 'direction.')
1162 layout.addWidget(le, 3, 0, 1, 1)
1164 def strike_dip_to_lineedit(state, widget):
1165 sel = widget.selectedText() == widget.text()
1166 widget.setText('%g, %g' % (state.strike, state.dip))
1167 if sel:
1168 widget.selectAll()
1170 def lineedit_to_strike_dip(widget, state):
1171 s = str(widget.text())
1172 string_to_strike_dip = {
1173 'east': (0., 90.),
1174 'west': (180., 90.),
1175 'south': (90., 90.),
1176 'north': (270., 90.),
1177 'top': (90., 0.),
1178 'bottom': (90., 180.)}
1180 if s in string_to_strike_dip:
1181 state.strike, state.dip = string_to_strike_dip[s]
1183 s = s.replace(',', ' ')
1184 try:
1185 state.strike, state.dip = map(float, s.split())
1186 except Exception:
1187 raise ValueError('need two numerical values: <strike>, <dip>')
1189 self._state_bind(
1190 ['strike', 'dip'], lineedit_to_strike_dip,
1191 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit)
1193 self.strike_dip_lineedit = le
1194 self.strike_dip_lineedit.returnPressed.connect(
1195 lambda *args: self.strike_dip_lineedit.selectAll())
1197 but = qw.QPushButton('Reset')
1198 but.setStatusTip('Reset to north-up map view.')
1199 but.clicked.connect(self.reset_strike_dip)
1200 layout.addWidget(but, 3, 1, 1, 1)
1202 # crosshair
1204 self._crosshair_checkbox = qw.QCheckBox('Crosshair')
1205 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2)
1207 # camera bindings
1208 self.talkie_connect(
1209 self.state,
1210 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'],
1211 self.update_camera)
1213 self.talkie_connect(
1214 self.gui_state, 'panels_visible', self.update_panel_visibility)
1216 return frame
1218 def controls_time(self):
1219 frame = qw.QFrame(self)
1220 frame.setSizePolicy(
1221 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1223 layout = qw.QGridLayout()
1224 frame.setLayout(layout)
1226 layout.addWidget(qw.QLabel('Min'), 0, 0)
1227 le_tmin = qw.QLineEdit()
1228 layout.addWidget(le_tmin, 0, 1)
1230 layout.addWidget(qw.QLabel('Max'), 1, 0)
1231 le_tmax = qw.QLineEdit()
1232 layout.addWidget(le_tmax, 1, 1)
1234 label_tcursor = qw.QLabel()
1236 label_tcursor.setSizePolicy(
1237 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1239 layout.addWidget(label_tcursor, 2, 1)
1240 self._label_tcursor = label_tcursor
1242 def time_to_lineedit(state, attribute, widget):
1243 sel = widget.selectedText() == widget.text() \
1244 and widget.text() != ''
1246 widget.setText(
1247 common.time_or_none_to_str(getattr(state, attribute)))
1249 if sel:
1250 widget.selectAll()
1252 def lineedit_to_time(widget, state, attribute):
1253 from pyrocko.util import str_to_time_fillup
1255 s = str(widget.text())
1256 if not s.strip():
1257 setattr(state, attribute, None)
1258 else:
1259 try:
1260 setattr(state, attribute, str_to_time_fillup(s))
1261 except Exception:
1262 raise ValueError(
1263 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')
1265 self._state_bind(
1266 ['tmin'], lineedit_to_time, le_tmin,
1267 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit,
1268 attribute='tmin')
1269 self._state_bind(
1270 ['tmax'], lineedit_to_time, le_tmax,
1271 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit,
1272 attribute='tmax')
1274 self.tmin_lineedit = le_tmin
1275 self.tmax_lineedit = le_tmax
1277 range_edit = RangeEdit()
1278 range_edit.set_data_provider(self)
1279 range_edit.set_data_name('time')
1281 xblock = [False]
1283 def range_to_range_edit(state, widget):
1284 if not xblock[0]:
1285 widget.blockSignals(True)
1286 widget.set_focus(state.tduration, state.tposition)
1287 widget.set_range(state.tmin, state.tmax)
1288 widget.blockSignals(False)
1290 def range_edit_to_range(widget, state):
1291 xblock[0] = True
1292 self.state.tduration, self.state.tposition = widget.get_focus()
1293 self.state.tmin, self.state.tmax = widget.get_range()
1294 xblock[0] = False
1296 self._state_bind(
1297 ['tmin', 'tmax', 'tduration', 'tposition'],
1298 range_edit_to_range,
1299 range_edit,
1300 [range_edit.rangeChanged, range_edit.focusChanged],
1301 range_to_range_edit)
1303 def handle_tcursor_changed():
1304 self.gui_state.tcursor = range_edit.get_tcursor()
1306 range_edit.tcursorChanged.connect(handle_tcursor_changed)
1308 layout.addWidget(range_edit, 3, 0, 1, 2)
1310 layout.addWidget(qw.QLabel('Focus'), 4, 0)
1311 le_focus = qw.QLineEdit()
1312 layout.addWidget(le_focus, 4, 1)
1314 def focus_to_lineedit(state, widget):
1315 sel = widget.selectedText() == widget.text() \
1316 and widget.text() != ''
1318 if state.tduration is None:
1319 widget.setText('')
1320 else:
1321 widget.setText('%s, %g' % (
1322 guts.str_duration(state.tduration),
1323 state.tposition))
1325 if sel:
1326 widget.selectAll()
1328 def lineedit_to_focus(widget, state):
1329 s = str(widget.text())
1330 w = [x.strip() for x in s.split(',')]
1331 try:
1332 if len(w) == 0 or not w[0]:
1333 state.tduration = None
1334 state.tposition = 0.0
1335 else:
1336 state.tduration = guts.parse_duration(w[0])
1337 if len(w) > 1:
1338 state.tposition = float(w[1])
1339 else:
1340 state.tposition = 0.0
1342 except Exception:
1343 raise ValueError('need two values: <duration>, <position>')
1345 self._state_bind(
1346 ['tduration', 'tposition'], lineedit_to_focus, le_focus,
1347 [le_focus.editingFinished, le_focus.returnPressed],
1348 focus_to_lineedit)
1350 label_effective_tmin = qw.QLabel()
1351 label_effective_tmax = qw.QLabel()
1353 label_effective_tmin.setSizePolicy(
1354 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1355 label_effective_tmax.setSizePolicy(
1356 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1357 label_effective_tmin.setMinimumSize(
1358 qg.QFontMetrics(label_effective_tmin.font()).width(
1359 '0000-00-00 00:00:00.000 '), 0)
1361 layout.addWidget(label_effective_tmin, 5, 1)
1362 layout.addWidget(label_effective_tmax, 6, 1)
1364 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
1365 self.talkie_connect(
1366 self.state, var, self.update_effective_time_labels)
1368 self._label_effective_tmin = label_effective_tmin
1369 self._label_effective_tmax = label_effective_tmax
1371 self.talkie_connect(
1372 self.gui_state, 'tcursor', self.update_tcursor)
1374 return frame
1376 def controls_appearance(self):
1377 frame = qw.QFrame(self)
1378 frame.setSizePolicy(
1379 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed)
1380 layout = qw.QGridLayout()
1381 frame.setLayout(layout)
1383 layout.addWidget(qw.QLabel('Lighting'), 0, 0)
1385 cb = common.string_choices_to_combobox(vstate.LightingChoice)
1386 layout.addWidget(cb, 0, 1)
1387 vstate.state_bind_combobox(self, self.state, 'lighting', cb)
1389 self.talkie_connect(
1390 self.state, 'lighting', self.update_render_settings)
1392 # background
1394 layout.addWidget(qw.QLabel('Background'), 1, 0)
1396 cb = common.strings_to_combobox(
1397 ['black', 'white', 'skyblue1 - white'])
1399 layout.addWidget(cb, 1, 1)
1400 vstate.state_bind_combobox_background(
1401 self, self.state, 'background', cb)
1403 self.talkie_connect(
1404 self.state, 'background', self.update_render_settings)
1406 return frame
1408 def controls_snapshots(self):
1409 return snapshots_mod.SnapshotsPanel(self)
1411 def update_effective_time_labels(self, *args):
1412 tmin = self.state.tmin_effective
1413 tmax = self.state.tmax_effective
1415 stmin = common.time_or_none_to_str(tmin)
1416 stmax = common.time_or_none_to_str(tmax)
1418 self._label_effective_tmin.setText(stmin)
1419 self._label_effective_tmax.setText(stmax)
1421 def update_tcursor(self, *args):
1422 tcursor = self.gui_state.tcursor
1423 stcursor = common.time_or_none_to_str(tcursor)
1424 self._label_tcursor.setText(stcursor)
1426 def reset_strike_dip(self, *args):
1427 self.state.strike = 90.
1428 self.state.dip = 0
1429 self.gui_state.focal_point = 'center'
1431 def get_camera_geometry(self):
1433 def rtp2xyz(rtp):
1434 return geometry.rtp2xyz(rtp[num.newaxis, :])[0]
1436 radius = 1.0 - self.state.depth / self.planet_radius
1438 cam_rtp = num.array([
1439 radius+self.state.distance,
1440 self.state.lat * d2r + 0.5*num.pi,
1441 self.state.lon * d2r])
1442 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.])
1443 cam, up, foc = \
1444 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.])
1446 foc_rtp = num.array([
1447 radius,
1448 self.state.lat * d2r + 0.5*num.pi,
1449 self.state.lon * d2r])
1451 foc = rtp2xyz(foc_rtp)
1453 rot_world = pmt.euler_to_matrix(
1454 -(self.state.lat-90.)*d2r,
1455 (self.state.lon+90.)*d2r,
1456 0.0*d2r).T
1458 rot_cam = pmt.euler_to_matrix(
1459 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T
1461 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T))
1463 cam = foc + num.dot(rot, cam - foc)
1464 up = num.dot(rot, up)
1465 return cam, up, foc
1467 def update_camera(self, *args):
1468 cam, up, foc = self.get_camera_geometry()
1469 camera = self.ren.GetActiveCamera()
1470 camera.SetPosition(*cam)
1471 camera.SetFocalPoint(*foc)
1472 camera.SetViewUp(*up)
1474 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0))
1476 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - (
1477 self.feature_radius_min / self.planet_radius)**2))
1479 # if horizon == 0.0:
1480 # horizon = 2.0 + self.state.distance
1482 # clip_dist = max(min(self.state.distance*5., max(
1483 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon)
1484 # , math.sqrt(num.sum(cam**2)))
1485 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2)))
1486 # clip_dist = feature_horizon
1488 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist)
1490 self.camera_params = (
1491 cam, up, foc, planet_horizon, feature_horizon, clip_dist)
1493 self.update_view()
1495 def add_panel(
1496 self, title_label, panel,
1497 visible=False,
1498 # volatile=False,
1499 tabify=True,
1500 where=qc.Qt.RightDockWidgetArea,
1501 remove=None,
1502 title_controls=[]):
1504 dockwidget = common.MyDockWidget(
1505 self, title_label, title_controls=title_controls)
1507 if not visible:
1508 dockwidget.hide()
1510 if not self.gui_state.panels_visible:
1511 dockwidget.block()
1513 dockwidget.setWidget(panel)
1515 panel.setParent(dockwidget)
1517 dockwidgets = self.findChildren(common.MyDockWidget)
1518 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where]
1520 self.addDockWidget(where, dockwidget)
1522 nwrap = 4
1523 if dws and len(dws) >= nwrap and tabify:
1524 self.tabifyDockWidget(
1525 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget)
1527 mitem = dockwidget.toggleViewAction()
1529 def update_label(*args):
1530 mitem.setText(dockwidget.titlebar._title_label.get_full_title())
1531 self.update_slug_abbreviated_lengths()
1533 dockwidget.titlebar._title_label.title_changed.connect(update_label)
1534 dockwidget.titlebar._title_label.title_changed.connect(
1535 self.update_slug_abbreviated_lengths)
1537 update_label()
1539 self._panel_togglers[dockwidget] = mitem
1540 self.panels_menu.addAction(mitem)
1541 if visible:
1542 dockwidget.setVisible(True)
1543 dockwidget.setFocus()
1544 dockwidget.raise_()
1546 def update_slug_abbreviated_lengths(self):
1547 dockwidgets = self.findChildren(common.MyDockWidget)
1548 title_labels = []
1549 for dw in dockwidgets:
1550 title_labels.append(dw.titlebar._title_label)
1552 by_title = defaultdict(list)
1553 for tl in title_labels:
1554 by_title[tl.get_title()].append(tl)
1556 for group in by_title.values():
1557 slugs = [tl.get_slug() for tl in group]
1559 n = max(len(slug) for slug in slugs)
1560 nunique = len(set(slugs))
1562 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique:
1563 n -= 1
1565 if n > 0:
1566 n = max(3, n)
1568 for tl in group:
1569 tl.set_slug_abbreviated_length(n)
1571 def raise_panel(self, panel):
1572 dockwidget = panel.parent()
1573 dockwidget.setVisible(True)
1574 dockwidget.setFocus()
1575 dockwidget.raise_()
1577 def toggle_panel_visibility(self):
1578 self.gui_state.panels_visible = not self.gui_state.panels_visible
1580 def update_panel_visibility(self, *args):
1581 self.setUpdatesEnabled(False)
1582 mbar = self.menuBar()
1583 sbar = self.statusBar()
1584 dockwidgets = self.findChildren(common.MyDockWidget)
1586 mbar.setVisible(self.gui_state.panels_visible)
1587 sbar.setVisible(self.gui_state.panels_visible)
1588 for dockwidget in dockwidgets:
1589 dockwidget.setBlocked(not self.gui_state.panels_visible)
1591 self.setUpdatesEnabled(True)
1593 def remove_panel(self, panel):
1594 dockwidget = panel.parent()
1595 self.removeDockWidget(dockwidget)
1596 dockwidget.setParent(None)
1597 self.panels_menu.removeAction(self._panel_togglers[dockwidget])
1599 def register_data_provider(self, provider):
1600 if provider not in self.data_providers:
1601 self.data_providers.append(provider)
1603 def unregister_data_provider(self, provider):
1604 if provider in self.data_providers:
1605 self.data_providers.remove(provider)
1607 def iter_data(self, name):
1608 for provider in self.data_providers:
1609 for data in provider.iter_data(name):
1610 yield data
1612 def closeEvent(self, event):
1613 self.attach()
1614 event.accept()
1615 self.closing = True
1616 common.get_app().set_main_window(None)
1618 def is_closing(self):
1619 return self.closing
1622class SparrowApp(qw.QApplication):
1623 def __init__(self):
1624 qw.QApplication.__init__(self, ['Sparrow'])
1625 self.lastWindowClosed.connect(self.myQuit)
1626 self._main_window = None
1627 self.setApplicationDisplayName('Sparrow')
1628 self.setDesktopFileName('Sparrow')
1630 def install_sigint_handler(self):
1631 self._old_signal_handler = signal.signal(
1632 signal.SIGINT, self.myCloseAllWindows)
1634 def uninstall_sigint_handler(self):
1635 signal.signal(signal.SIGINT, self._old_signal_handler)
1637 def myQuit(self, *args):
1638 self.quit()
1640 def myCloseAllWindows(self, *args):
1641 self.closeAllWindows()
1643 def set_main_window(self, win):
1644 self._main_window = win
1646 def get_main_window(self):
1647 return self._main_window
1649 def get_progressbars(self):
1650 if self._main_window:
1651 return self._main_window.progressbars
1652 else:
1653 return None
1655 def status(self, message, duration=None):
1656 win = self.get_main_window()
1657 if not win:
1658 return
1660 win.statusBar().showMessage(
1661 message, int((duration or 0) * 1000))
1664def main(*args, **kwargs):
1666 from pyrocko import util
1667 from pyrocko.gui import util as gui_util
1668 util.setup_logging('sparrow', 'info')
1670 global win
1672 if gui_util.app is None:
1673 gui_util.app = SparrowApp()
1675 # try:
1676 # from qt_material import apply_stylesheet
1677 #
1678 # apply_stylesheet(app, theme='dark_teal.xml')
1679 #
1680 #
1681 # import qdarkgraystyle
1682 # app.setStyleSheet(qdarkgraystyle.load_stylesheet())
1683 # import qdarkstyle
1684 #
1685 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
1686 #
1687 #
1688 # except ImportError:
1689 # logger.info(
1690 # 'Module qdarkgraystyle not available.\n'
1691 # 'If wanted, install qdarkstyle with "pip install '
1692 # 'qdarkgraystyle".')
1693 #
1694 win = SparrowViewer(*args, **kwargs)
1696 gui_util.app.install_sigint_handler()
1697 gui_util.app.exec_()
1698 gui_util.app.uninstall_sigint_handler()
1700 del win
1702 gc.collect()
1704 del gui_util.app