Source code for vdat.gui.tabs.ifu_viewer
# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2015, 2016, 2017, 2018 "The HETDEX collaboration"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from collections import namedtuple
from functools import partial
import logging
from os.path import basename
import time
import traceback
from astropy.io import fits
# TODO: remove if not necessary
# import ginga.toolkit
# ginga.toolkit.use('qt4')
from ginga import AstroImage
from ginga.qtw.ImageViewCanvasQt import ImageViewCanvas
from ginga.misc.Settings import SettingGroup
import numpy as np
from qtpy import QtCore, QtGui, QtWidgets
from vdat.gui.menus_actions import QuitAction, HelpMenu
from vdat.gui.utils import wait_cursor
from vdat.utilities import grouper
try: # pragma: no cover
import pyds9
except Exception as e: # pragma: no cover
pyds9 = None
pyds9_error = (str(e), traceback.format_exc())
# if no pyds9 is available, do not the signal and decorator are not set
if pyds9:
ds9_types = [pyds9.DS9]
else:
ds9_types = []
[docs]class FitsViewerTitleTooltipError(WindowViewerError, TypeError):
'''Exception raised when the titles and/or tooltips for the fits viewer are
malformed'''
pass
[docs]class DistViewerFileNumberError(FitsViewerTitleTooltipError):
'''Exception raised when the number of files, the titles and/or tooltips
for the dist viewer are malformed'''
pass
[docs]class DS9Menu(QtWidgets.QMenu):
'''Menu item with the ``ds9`` actions.
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_ds9`
- :class:`pyds9.DS9`
- emitted, with the ds9 instance to use, when clicking on a ds9 menu
option
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`populate_ds9_menu`
-
- dynamically create the ds9 menu
* - :meth:`pyds9_error_popup`
-
- launch an pop up window with the explanation of the pysd9 import
failure
* - :meth:`launch_ds9`
- :class:`PyQt5.QtWidgets.QAction`, optional
- Launch DS9. If no action is passed, create a new window, otherwise
add a new frame into an existing window; then emit :attr:`sig_ds9`
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`aboutToShow`
- :meth:`populate_ds9_menu`
* - 'There was a problem loading pyds9.' menu entry
- :meth:`pyds9_error_popup`
* - Open in DS9 menu entries
- :meth:`launch_ds9`
Parameters
----------
title : string, optional
name of the menu
new_ds9_name : list of strings, optional
list of string that are concatenated to create a name for a new DS9
window. If nothing is provided, a name will be created
new_ds9_menu : bool, optional
add a menu to create a new DS9 window
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
sig_ds9
'''
sig_ds9 = QtCore.Signal(*ds9_types)
def __init__(self, title='ds9', new_ds9_name=None, new_ds9_menu=True,
parent=None):
super(DS9Menu, self).__init__(title, parent=parent)
self._new_ds9_name = new_ds9_name
self._new_ds9_menu = new_ds9_menu
self.aboutToShow.connect(self.populate_ds9_menu)
self.error_box = QtWidgets.QErrorMessage(parent=self)
[docs] @QtCore.Slot()
def pyds9_error_popup(self):
'''Create an error popup with the pyds9 message.
This method is also a PyQt slot.
'''
self.error_popup(pyds9_error)
[docs] def error_popup(self, error):
"""Create a popup reporting the error.
Parameters
----------
error : list
error[0]: short error message, set as informative text;
error[1]: full traceback, set as detailed text
"""
box = QtWidgets.QMessageBox(parent=self)
text = ("There has been a problem while importing pyds9:\n"
" {}\n"
"However, VDAT will continue working but you cannot push the"
" selected fits files to ds9.")
box.setText(text.format(error[0]))
informative_text = ('To install pyds9 run either one of the following'
' commands:\n'
' pip install vdat[ds9]\n'
' pip install pyds9\n')
box.setInformativeText(informative_text)
box.setIcon(QtWidgets.QMessageBox.Warning)
box.setDetailedText(error[1])
box.exec_()
[docs] @QtCore.Slot()
@QtCore.Slot(QtWidgets.QAction)
def launch_ds9(self, action=None):
"""Send files to DS9. If action is specified send to the DS9 window
associated to it.
Otherwise, create a new DS9 window with an automatically generated
name.
Parameters
----------
action : :class:`PyQt5.QtWidgets.QAction`, optional
action clicked; if provided its text is used as the DS9 instance
name
"""
name = self._ds9_name(action)
# launch DS9
try:
ds9 = pyds9.DS9(name)
except Exception as e:
log = logging.getLogger('logger')
log.error("Cannot launch ds9: {:}".format(e))
# create a error box to report the error
msg = ("An error has being detected while trying to create or"
" connect to the ds9 instance: {}<br><br>".format(name))
msg += "The error says:<br>{}<br>".format(e)
msg += "<br>"
msg += traceback.format_exc().replace('\n', '<br>')
self.error_box.showMessage(msg, "ds9-{}".format(type(e)))
return
self.sig_ds9.emit(ds9)
[docs] def _ds9_name(self, action):
'''Get the name from the action or create a new name, if action is
``None``.
Parameters
----------
action : :class:`PyQt5.QtWidgets.QAction`, optional
action clicked; if provided its text is used as the DS9 instance
name
Returns
-------
name : string
name of the ds9 windows
'''
if action:
name = action.text()
else:
if self._new_ds9_name:
name = '{}'.format('-'.join(self._new_ds9_name))
else:
name = 'vdat-fits-viewer-window'
# if the DS9 instance already exists, append a number to the name
i = 1
targets = pyds9.ds9_targets()
if targets:
tname = name
while any(tname in s for s in targets):
tname = name + "-{:d}".format(i)
i = i + 1
name = tname
return name
[docs]class FitsViewerTab(QtWidgets.QWidget):
'''Widget that display a fits file using Ginga in the upper part and the
file header on the lower part.
Parameters
----------
fname : string
name of the file to show
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
fname : string
name of the file displayed in the tab
'''
def __init__(self, fname, tab_dict, parent=None):
super(FitsViewerTab, self).__init__(parent=parent)
self.fname = fname
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical, parent=self)
layout.addWidget(splitter)
# Ginga viewer
ginga_widget = GingaWidget(fname, tab_dict, parent=self)
header_widget = HeaderWidget(fname, tab_dict, parent=self)
# header keywords
splitter.addWidget(ginga_widget)
splitter.addWidget(header_widget)
splitter.setSizes([500, 100])
splitter.setStretchFactor(splitter.indexOf(ginga_widget), 1)
splitter.setStretchFactor(splitter.indexOf(header_widget), 0)
[docs]class FitsViewerWindow(QtWidgets.QMainWindow):
'''Display the given files using a ginga based viewer. Show the header
information and allow to open files in DS9.
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`send_to_ds9`
- :class:`pyds9.DS9`
- Push the file to the ds9 window passed as argument
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`DS9Menu.sig_ds9`
- :meth:`send_to_ds9`
Parameters
----------
file_names : list
list of files to display
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
new_ds9_name : list of strings, optional
list of string that are concatenated to create a name for a new DS9
window. If nothing is provided, a name will be created
titles, tooltips : list of strings, optional
if given they are used as title page and tooltip, respectively, for
each of the tabs shown. If ``None`` the file names are used. Otherwise
they must be a list with the same length of ``file_names``
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
ds9_menu : :class:`PyQt5.QtWidgets.QMenu`
menu containing the ds9 buttons
tab_dict : dictionary
same as input
current_tab
'''
def __init__(self, file_names, tab_dict, new_ds9_name=None, titles=None,
tooltips=None, parent=None):
super(FitsViewerWindow, self).__init__(parent=parent)
# this variable is saved here and used in :meth:`make_menubar`
self._new_ds9_name = new_ds9_name
self.tab_dict = tab_dict
menu_bar = self.make_menubar()
self.setMenuBar(menu_bar)
main_widget = self.make_main_widget(file_names, tab_dict, titles,
tooltips)
self.setCentralWidget(main_widget)
@QtCore.Property(FitsViewerTab, doc='''Tab currently shown''')
def current_tab(self):
return self.centralWidget().currentWidget()
[docs] @QtCore.Slot()
def show_ginga_help(self):
'''Open the ginga url into a browser.
Ths method is also a PyQt slot
'''
url = "http://ginga.readthedocs.io/en/latest/quickref.html"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
[docs] @QtCore.Slot(*ds9_types)
def send_to_ds9(self, ds9):
"""Send files to DS9. If action is specified send to the DS9 window
associated to it.
Otherwise, create a new DS9 window with an automatically generated
name.
Parameters
----------
ds9 : :class:`pyds9.DS9`
ds9 instance to where the data are sent
"""
# file to open in DS9
file_name = self.current_tab.fname
# If the extension is present in the ``tab_dict``, send only the
# extension to DS9
if 'ext' in self.tab_dict:
file_name += '[{e}]'.format(e=self.tab_dict['ext'])
# send selected files to DS9
if ds9.get('file'):
ds9.set('frame new')
ds9.set("file {:s}".format(file_name))
ds9.set("scale mode zscale")
ds9.set("tile yes grid mode")
[docs] def make_main_widget(self, file_names, tab_dict, titles, tooltips):
'''Create a tab widget and add :class:`FitsViewerTab` classes as tabs.
Parameters
----------
file_names : list of strings
files to display
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
titles, tooltips : list of strings or None
if None, use the file names, otherwise check the length is the same
of ``file_names``
Returns
-------
tab_widget : :class:`PyQt5.QtWidgets.QTabWidget`
widget to plug as main in the window
'''
tab_widget = QtWidgets.QTabWidget(parent=self)
fnames, titles, tooltips = self._make_title_tooltip(file_names, titles,
tooltips)
for fn, title, tt in zip(fnames, titles, tooltips):
tab = FitsViewerTab(fn, tab_dict, parent=self)
index = tab_widget.addTab(tab, title)
tab_widget.setTabToolTip(index, tt)
return tab_widget
[docs] def _make_title_tooltip(self, file_names, titles, tooltips):
'''Prepare the titles and tooltips for the tabs
Parameters
----------
file_names : list of strings
files to display
titles, tooltips : list of strings or None
if None, use the file names, otherwise check the length is the same
of ``file_names``
Returns
-------
file_names, titles, tooltips : list of strings
if titles and tooltips are ``None`` return lists with default
values
Raises
------
FitsViewerTitleTooltipError
when titles or tooltips is malformed
'''
if titles is None:
titles = [basename(fn) for fn in file_names]
ext = self.tab_dict.get('ext')
if ext is not None:
titles = [t + '[{e}]'.format(e=ext) for t in titles]
else:
if len(file_names) != len(titles):
msg = ('``titles`` must be either ``None`` or'
' a list with the same length of ``file_names``')
raise FitsViewerTitleTooltipError(msg)
if tooltips is None:
tooltips = file_names[:]
else:
if len(file_names) != len(tooltips):
msg = ('``tooltips`` must be either ``None`` or'
' a list with the same length of ``file_names``')
raise FitsViewerTitleTooltipError(msg)
return file_names, titles, tooltips
[docs]class GingaWidget(QtWidgets.QWidget):
'''Widget that display a fits file using Ginga.
Parameters
----------
fname : string
name of the file to show
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
'''
def __init__(self, fname, tab_dict, parent=None):
super(GingaWidget, self).__init__(parent=parent)
self.setMinimumSize(500, 500)
self._first_shown = True
self.tab_dict = tab_dict
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.fits_viewer = self.ginga_panel(fname)
fits_widget = self.fits_viewer.get_widget()
layout.addWidget(fits_widget)
[docs] def ginga_panel(self, fn):
"""Setups up a Ginga fits viewer panel.
Parameters
----------
fn : string
the filename of the fits file to view
Returns
-------
fitsview : :class:`~ginga.qtw.ImageViewCanvasQt.ImageViewCanvasQt`
"""
glog = logging.getLogger('ginga')
ginga_settings = SettingGroup(logger=glog)
# Set up a Ginja fits viewer
fitsview = ImageViewCanvas(glog, render='widget',
settings=ginga_settings)
fitsview.enable_autocuts('on')
fitsview.set_autocut_params('zscale')
fitsview.enable_autozoom('override')
# load the fits image
image = self.load_fits_image(fn, glog)
fitsview.set_window_size(*image.get_size())
fitsview.set_image(image)
fitsview.image_filename = fn
fitsview.ui_setActive(True)
# enable all the bells and whistles (keyboard/mouse shortcuts)
bd = fitsview.get_bindings()
bd.enable_all(True)
return fitsview
[docs] def showEvent(self, event):
'''When the tab becomes visible, this method is triggered by Qt.
If it is the first time it is shown, zoom the image to fit.
Parameters
----------
event : :class:`PyQt5.QtGui.QShowEvent`
the tab is show
'''
if self._first_shown:
self.fits_viewer.zoom_fit()
self._first_shown = False
[docs] def load_fits_image(self, fn, log):
"""Load a FITS file as a Ginga AstroImage and return it
Parameters
----------
fn : String
the filename of the image to load
log :
a logger
Returns
-------
image : :class:`~ginga.AstroImage.AstroImage` instance
"""
image = AstroImage.AstroImage(logger=log)
ext = self.tab_dict.get('ext', 0)
with fits.open(fn, memmap=False) as hdul:
hdu = hdul[ext]
# if the underlying image is a cube, show the median of the given
# slice as done in the focal plane
if hdu.data.ndim == 3:
data = hdu.data
z_indx = self.tab_dict.get('z_indx', (None, None))
data = np.nanmedian(data[z_indx[0]:z_indx[1], ...], axis=0)
hdu = fits.PrimaryHDU(data)
image.load_hdu(hdu)
log.debug("Loaded image {:}".format(fn))
return image
[docs]class HeaderWidget(QtWidgets.QTextEdit):
'''Widget that display a fits file headers.
Parameters
----------
fname : string
name of the file to show
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
'''
def __init__(self, fname, tab_dict, parent=None):
super(HeaderWidget, self).__init__(parent=parent)
self.setReadOnly(True)
self.setText(self.parse_fits_header(fname, tab_dict))
self.setMinimumHeight(100)
[docs] def parse_fits_header(self, fn, tab_dict):
"""Convert a FITS header to a string and return it.
The method uses directly the following keys of the ``tab_dict``
argument:
* ``header_keys`` (optional): list of strings. Header keywords to show
on top of the others in the fits viewer window.
Parameters
----------
fn : string
the filename of the fits file
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
Returns
-------
ostr : string
The header information
"""
header = fits.getheader(fn)
ostr = header.tostring(sep='\n', endcard=False, padding=False).strip()
header_keys = tab_dict.get('header_keys', [])
if header_keys:
ostr = self._order_header_keys(ostr, header_keys)
return ostr
[docs] def _order_header_keys(self, istr, header_keys):
'''Split the incoming string on new lines and move to the beginning all
the lines starting with a keyword in header_keys.
Parameters
----------
istr : string
string with the full headers
header_keys : list of strings
header keywords to put at the beginning
Returns
-------
ostr : string
string reorganized
'''
move_line = namedtuple('move_line', 'line, line_index, key_index')
header_keys = [i.upper() for i in header_keys]
lines = istr.split('\n')
to_move = []
for i, l in enumerate(lines):
key = l.split('=')[0].strip()
if key in header_keys:
index = header_keys.index(key)
to_move.append(move_line(line=l, line_index=i,
key_index=index))
to_move.sort(key=lambda x: x.key_index)
on_top = [i.line for i in to_move]
for i in to_move:
lines.pop(i.line_index)
if on_top and lines:
on_top.append('-'*len(on_top[-1]))
ostr = on_top + lines
ostr = '\n'.join(ostr)
return ostr
[docs]class FileEditorWindow(QtWidgets.QMainWindow):
'''Simple single file editor window.
This is loosely based on
https://www.binpress.com/tutorial/building-a-text-editor-with-pyqt-part-one/143
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_saved`
- str
- emitted when a file is emitted
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`set_title`
- str
- set the window title with the given file name
* - :meth:`mark_not_modified`
- str, optional
- mark the window as not modified, input parameters ignored
* - :meth:`save_file`
- str, optional
- save the document
* - :meth:`save_file_as`
-
- save the document as a new file
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`vdat.gui.menus_actions.QuitAction.triggered`
- :meth:`close`
* - :attr:`save_action.triggered`
- :meth:`save_file`
* - :attr:`save_as_action.triggered`
- :meth:`save_file_as`
* - :attr:`sig_saved`
- :meth:`set_title`
* - :attr:`sig_saved`
- :meth:`mark_not_modified`
Parameters
----------
file_names : string
file to display
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
file_name : string
name of the file to display
text_edit : :class:`PyQt5.QtWidgets.QTextEdit`
display the text
save_action : :class:`PyQt5.QtWidgets.QAction`
action to save the document
save_as_action : :class:`PyQt5.QtWidgets.QAction`
action to save the document under a new name
'''
sig_saved = QtCore.Signal(str)
def __init__(self, file_name, tab_dict, parent=None):
super(FileEditorWindow, self).__init__(parent=parent)
self.file_name = file_name
self.setMinimumSize(600, 500)
self.set_title(file_name)
self.make_common_actions()
self.setMenuBar(self.make_menubar())
self.addToolBar(self.make_toolbar())
self.text_edit = self.make_text_edit()
self.setCentralWidget(self.text_edit)
self.sig_saved.connect(self.mark_not_modified)
self.sig_saved.connect(self.set_title)
[docs] def make_text_edit(self):
'''Create a QTextEdit edit and returns it
'''
text_edit = QtWidgets.QTextEdit(self)
text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
with open(self.file_name) as f:
file_content = f.read()
text_edit.setPlainText(file_content)
text_edit.textChanged.connect(partial(self.setWindowModified, True))
return text_edit
[docs] def make_common_actions(self):
'''Create a number of actions than are plugged into the task and menu
bars'''
save_icon = QtGui.QIcon.fromTheme('document-save')
self.save_action = QtWidgets.QAction(save_icon, 'Save', self)
self.save_action.setStatusTip('Save document')
self.save_action.setShortcut('Ctrl+S')
self.save_action.triggered.connect(self.save_file)
save_icon = QtGui.QIcon.fromTheme('document-save-as')
self.save_as_action = QtWidgets.QAction(save_icon, 'Save as', self)
self.save_as_action.setStatusTip('Save document under a new name')
self.save_as_action.setShortcut('Ctrl+Shift+S')
self.save_as_action.triggered.connect(self.save_file_as)
[docs] def make_toolbar(self):
'''Create and return the toolbar'''
tool_bar = QtWidgets.QToolBar('Options', self)
tool_bar.addAction(self.save_action)
tool_bar.addAction(self.save_as_action)
tool_bar.addSeparator()
return tool_bar
[docs] @QtCore.Slot(str)
def set_title(self, file_name):
'''Set the title of the window.
This method is also a PyQt slot.
Parameters
----------
file_name : string
name of the file
'''
# Qt automatically put a * when the widget is modified, if the title
# contains a [*]:
# https://doc.qt.io/qt-4.8/qwidget.html#windowModified-prop
self.setWindowTitle('{}[*] -- VDAT'.format(basename(file_name)))
[docs] @QtCore.Slot()
@QtCore.Slot(str)
def mark_not_modified(self, _=None):
'''Mark the window as not modified. The parameter is ignored
This method is also a PyQt slot.
'''
self.setWindowModified(False)
[docs] @QtCore.Slot()
@QtCore.Slot(str)
def save_file(self, fname=None):
'''Get the text and save it to file. If ``fname`` is ``None``,
overwrite the input file.
This method is also a PyQt slot
Parameters
----------
fname : string, optional
name of the file to use
'''
if not fname:
fname = self.file_name
with open(fname, 'w') as f:
f.write(self.text_edit.toPlainText())
self.sig_saved.emit(fname)
[docs] @QtCore.Slot()
def save_file_as(self):
'''Ask for a new file name and save there the file. If no file name is
given, nothing happens.
This method is also a PyQt slot
Parameters
----------
fname : string, optional
name of the file to use
'''
filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save File')
if not filename:
return
self.file_name = filename
self.save_file(fname=filename)
[docs] def closeEvent(self, event):
'''If the file is modified, ask what to do before closing. If
``Cancel`` is pressed, the window is not closed.
.. note::
from the `documentation
<https://doc.qt.io/qt-5/qwidget.html#close>`_ it seems that it's
preferable to reimplement :meth:`closeEvent` and the :meth:`close`
method.
Parameters
----------
event : :class:`PyQt5.QtGui.QCloseEvent`
event emitted when a close attempt is made
'''
accept = True
if self.isWindowModified():
msg_box = QtWidgets.QMessageBox(self)
msg_box.setText("The document has been modified.")
msg_box.setInformativeText("Do you want to save your changes?")
msg_box.setStandardButtons(QtWidgets.QMessageBox.Save |
QtWidgets.QMessageBox.Discard |
QtWidgets.QMessageBox.Cancel)
msg_box.setDefaultButton(QtWidgets.QMessageBox.Save)
reply = msg_box.exec_()
# if "Cancel" is pressed, discard the close event
accept = (reply != QtWidgets.QMessageBox.Cancel)
if reply == QtWidgets.QMessageBox.Save:
self.save_file()
if accept:
event.accept()
else:
event.ignore()
[docs]class DistTab(QtWidgets.QWidget):
'''Widget that display a distortion solution and the region
Parameters
----------
dist_name : string
name of the distortion file to show
reg_name : string
name of the region file attached to ``dist_name``
fits_names : list of strings
name of the fits file that can be sent to DS9
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
dist_name : string
same as input
reg_name : string
same as input
fits_names : list of strings
same as input
'''
def __init__(self, dist_name, reg_name, fits_names, tab_dict, parent=None):
super(DistTab, self).__init__(parent=parent)
self.dist_name = dist_name
self.reg_name = reg_name
self.fits_names = fits_names
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
text_display = self.dist_text_widget()
layout.addWidget(text_display)
# splitter = QtGui.QSplitter(QtCore.Qt.Vertical, parent=self)
# layout.addWidget(splitter)
# splitter.setSizes([500, 100])
# splitter.setStretchFactor(splitter.indexOf(ginga_widget), 1)
# splitter.setStretchFactor(splitter.indexOf(header_widget), 0)
[docs] def dist_text_widget(self):
'''Create a read-only text area displaying the distortion file
Returns
-------
text_display : :class:`PyQt5.QtWidgets.QTextEdit`
widget showing the dist file
'''
text_display = QtWidgets.QTextEdit(self)
text_display.setReadOnly(True)
with open(self.dist_name) as f:
file_content = f.read()
text_display.setPlainText(file_content)
return text_display
[docs]class DistWindow(QtWidgets.QMainWindow):
'''Show the distortion file and the region file and add button to send the
distortion region to DS9.
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`send_dist_fits_to_ds9`
- :class:`pyds9.DS9`
- Push the fits files and the regions to the ds9 window passed as
argument
* - :meth:`send_dist_to_ds9`
- :class:`pyds9.DS9`
- Push the regions to the ds9 window passed as argument
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`DS9Menu.sig_ds9` (menu 'Distortion and fits')
- :meth:`send_dist_fits_to_ds9`
* - :attr:`DS9Menu.sig_ds9` (menu 'Distortion only')
- :meth:`send_dist_to_ds9`
Parameters
----------
dist_files : list of strings
distortion files to display
reg_files : list of strings
region files to display, must have the same size of ``dist_files``
fits_names : 2d list of strings
list of list of fits files, the first dimension must have the same size
of ``dist_files``
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
new_ds9_name : list of strings, optional
list of string that are concatenated to create a name for a new DS9
window. If nothing is provided, a name will be created
titles, tooltips : list of strings, optional
if given they are used as title page and tooltip, respectively, for
each of the tabs shown. If ``None`` the file names are used. Otherwise
they must be a list with the same length of ``dist_files``
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
Attributes
----------
ds9_menu : :class:`PyQt5.QtWidgets.QMenu`
menu containing the ds9 buttons
current_tab
'''
def __init__(self, dist_files, reg_files, fits_files, tab_dict,
new_ds9_name=None, titles=None, tooltips=None, parent=None):
super(DistWindow, self).__init__(parent=parent)
# this variable is saved here and used in :meth:`make_menubar`
self._new_ds9_name = new_ds9_name
menu_bar = self.make_menubar()
self.setMenuBar(menu_bar)
main_widget = self.make_main_widget(dist_files, reg_files, fits_files,
tab_dict, titles, tooltips)
self.setCentralWidget(main_widget)
self.setMinimumSize(600, 500)
@QtCore.Property(DistTab, doc='''Tab currently shown''')
def current_tab(self):
return self.centralWidget().currentWidget()
[docs] def make_main_widget(self, dist_files, reg_files, fits_files, tab_dict,
titles, tooltips):
'''Create a tab widget and add :class:`DistTab` classes as tabs.
Parameters
----------
dist_files : list of strings
distortion files to display
reg_files : list of strings
region files to display, must have the same size of ``dist_files``
fits_names : 2d list of strings
list of list of fits files, the first dimension must have the same
size of ``dist_files``
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
titles, tooltips : list of strings, optional
if given they are used as title page and tooltip, respectively, for
each of the tabs shown. If ``None`` the file names are used.
Otherwise they must be a list with the same length of
``dist_files``
Returns
-------
tab_widget : :class:`PyQt5.QtWidgets.QTabWidget`
widget to plug as main in the window
'''
if len(reg_files) != len(dist_files):
msg = ('``reg_files`` must the same length of ``dist_files``')
raise DistViewerFileNumberError(msg)
if len(fits_files) != len(dist_files):
msg = ('``fits_files`` must the same length of ``dist_files``')
raise DistViewerFileNumberError(msg)
fnames, titles, tooltips = self._make_title_tooltip(dist_files, titles,
tooltips)
tab_widget = QtWidgets.QTabWidget(parent=self)
for fn, reg_fn, fits_fn, title, tt in zip(fnames, reg_files,
fits_files, titles,
tooltips):
tab = DistTab(fn, reg_fn, fits_fn, tab_dict, parent=self)
index = tab_widget.addTab(tab, title)
tab_widget.setTabToolTip(index, tt)
return tab_widget
[docs] def _make_title_tooltip(self, file_names, titles, tooltips):
'''Prepare the titles and tooltips for the tabs
Parameters
----------
file_names : list of strings
files to display
titles, tooltips : list of strings or None
if None, use the file names, otherwise check the length is the same
of ``file_names``
Returns
-------
file_names, titles, tooltips : list of strings
if titles and tooltips are ``None`` return lists with default
values
Raises
------
DistViewerFileNumberError
when titles or tooltips is malformed
'''
if titles is None:
titles = [basename(fn) for fn in file_names]
else:
if len(file_names) != len(titles):
msg = ('``titles`` must be either ``None`` or'
' a list with the same length of ``dist_files``')
raise DistViewerFileNumberError(msg)
if tooltips is None:
tooltips = file_names[:]
else:
if len(file_names) != len(tooltips):
msg = ('``tooltips`` must be either ``None`` or'
' a list with the same length of ``dist_files``')
raise DistViewerFileNumberError(msg)
return file_names, titles, tooltips
[docs] @QtCore.Slot(*ds9_types)
def send_dist_fits_to_ds9(self, ds9):
"""Send the fits files to DS9 and overplot the region files.
This method is also a PyQt slot.
Parameters
----------
ds9 : :class:`pyds9.DS9`
ds9 instance to where the data are sent
"""
# file to open in DS9
with wait_cursor():
current_tab = self.current_tab
for file_name in current_tab.fits_names:
# send selected files to DS9
if ds9.get('file'):
ds9.set('frame new')
# send a fits file
ds9.set("file {:s}".format(file_name))
ds9.set("scale mode zscale")
ds9.set("tile yes grid mode")
self.send_region_to_ds9(ds9, current_tab.reg_name)
[docs] @QtCore.Slot(*ds9_types)
def send_dist_to_ds9(self, ds9):
"""Send the region file to a DS9 window.
This method is also a PyQt slot.
Parameters
----------
ds9 : :class:`pyds9.DS9`
ds9 instance to where the data are sent
"""
# file to open in DS9
with wait_cursor():
current_tab = self.current_tab
self.send_region_to_ds9(ds9, current_tab.reg_name)
[docs] def send_region_to_ds9(self, ds9, regions_file, chunk_size=50):
"""Send the regions from the region file to DS9.
This method assumes that each line is a region command. Because of this
it cannot simply load the file into DS9, but have to send the regions
in chunks of size ``chunk_size``.
Parameters
----------
ds9 : :class:`pyds9.DS9`
ds9 instance to where the data are sent
regions_file : string
name of the file containing the regions
chunk_size : int, optional
number of region elements to send in one go to DS9
"""
# file to open in DS9
with open(regions_file) as f:
regions = f.read()
# split on new lines
regions = regions.splitlines()
for r in grouper(regions, n=chunk_size):
exc_counter = 0
while True:
try:
ds9.set(';'.join([i for i in r if i]))
except ValueError:
if exc_counter > 3:
raise
else:
exc_counter += 2
# if too many stuff are sent to XPA, sometimes it
# crashes. Try again and the go ahead
time.sleep(0.2)
else:
break
# the following code can be used to test the windows in isolation
# def main():
# import sys
#
# app = QtGui.QApplication(sys.argv)
#
# main = FileEditorWindow(sys.argv[1], {})
# main.show()
#
# sys.exit(app.exec_())
#
#
# if __name__ == "__main__":
# main()