Source code for vdat.gui.tabs.ifu_widget
# 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/>.
"""Widgets showing the IFU in the focal plane"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import copy
from distutils.spawn import find_executable
import hashlib
import itertools as it
import logging
import os
import subprocess as sp
from astropy.io import fits
from astropy.visualization import ZScaleInterval
from pyhetdex.het.fplane import IFU
from pyhetdex.tools.files.file_tools import prefix_filename
import qimage2ndarray
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtWidgets import QFrame
from .ifu_viewer import FitsViewerWindow, FileEditorWindow, DistWindow
import vdat.gui.utils as gui_utils
import vdat.config as vconf
from vdat import exceptions
[docs]def update_painter_font_size(painter, rect, string):
'''If the string is too large, reduce the font. The code is taken from
https://stackoverflow.com/questions/2202717/for-qt-4-6-x-how-to-auto-size-text-to-fit-in-a-specified-width
Parameters
----------
painter : :class:`PyQt5.QtGui.QPainter`
painter object
rect : :class:`PyQt5.QtCore.QRectF`
rectangle in which the string is embedded
string : string
string to paint
Returns
-------
painter : :class:`PyQt5.QtGui.QPainter`
updated painter
'''
while (painter.fontMetrics().width(string) > rect.width()):
newsize = painter.font().pointSize() - 1
painter.setFont(QtGui.QFont(painter.font().family(), newsize))
return painter
[docs]class BaseItem(object):
'''An editable version of :class:`collections.namedtuple`. The name and
number of attributes is fixed and the values are set to None by default.
Derived classes must only define ``__slots__`` which should only contain
names of any additional slots. For more information see the `documenatation
<https://docs.python.org/3/reference/datamodel.html#slots>`_
Attributes
----------
n_col, n_row : int
index of the column and of the row of the item
col_name, row_name : string
name associated to the column and row
format_dict : dict
dictionary with the fields and values that can be replaced in file
names
image : :class:`PyQt5.QtGui.QImage` or ``None``
the thumbnail to display; set it to ``None`` if there is nothing to
display
'''
__slots__ = ['n_col', 'n_row', 'col_name', 'row_name', 'format_dict',
'image']
def __init__(self, **kwargs):
for i in self.__slots__:
setattr(self, i, None)
for k, v in kwargs.items():
setattr(self, k, v)
[docs]class FitsItem(BaseItem):
'''Items used in the widgets showing fits file
Attributes
----------
fname : str
full file name of the file to show
data : nd.array
array representing the image
zmin, zmax : float
minimum and maximum value of data after applying zscaling
ctime : float
time of creation of the thumbnail stored in ``image``
'''
__slots__ = ['fname', 'data', 'zmin', 'zmax', 'ctime']
[docs]class DistItem(BaseItem):
'''Items used when showing the distortion. :attr:`fits_names` is
initialized to an empty list.
Attributes
----------
fname : str
full file name of the distortion file
reg_fname : str
full name of the region file
fits_names : list strings
name of the fits files to load into DS9 to use as base to display the
regions
'''
__slots__ = ['fname', 'reg_fname', 'fits_names']
def __init__(self, **kwargs):
super(DistItem, self).__init__(**kwargs)
if self.fits_names is None:
self.fits_names = []
# Mixing Qt classes with other classes can be tricky. See:
# http://python.6.x6.nabble.com/Issue-with-multiple-inheritance-td5207771.html
# http://pyqt.sourceforge.net/Docs/PyQt5/multiinheritance.html#support-for-cooperative-multi-inheritance
# for some reason this does not affect PyQt4 (probably because needs to cope
# with old style classes.
[docs]class BaseIFUWidget(IFU, QtWidgets.QLabel):
'''Base class representing one IFU in the focal plane.
This implement the basic functionalities that other IFU widgets have and
all the hooks needed to make this work together with
:class:`~vdat.gui.tabs.tab_widget.BaseFplanePanel`.
Upon initialization it:
* creates tooltip and whatsThis
* sets the default style
* customizes single and double click behaviour
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_ifuToggled`
- str, bool
- emitted when a user selects/deselects the current IFU; the
parameters are the SLOTID of the IFU and whether the IFU has been
selected
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`ifuSelected`
- str, bool
- selects (second argument ``True``)/deselects (second argument
``False``) the IFU, if its ``SLOTID`` is the one given in the first
argument.
Parameters
----------
ifuslot : string
id of the slot in the focal plane
x, y : string or float
x and y position of the ifuslot in the focal plane
specid, specslot: int
id of the spectrograph where the ifu is plugged into and of the slot of
the spectrograph
ifuid : string
id of the ifu
platescl : float
focal plane plate scale at the position in the IHMP
parent : :class:`QWidget` or derived instance, optional
the parent of the current widget
Attributes
----------
hw_size
selected
empty_img : :class:`PyQt5.QtGui.QImage`
empty image, black with a white cross
current_img : :class:`PyQt5.QtGui.QImage`
image to show in the gui. The desired image should be assigned to this
variable in :meth:`prepare_image` and shown in :meth:`show_image`.
default style attibutes
see :meth:`_default_aspect`
'''
sig_ifuToggled = QtCore.Signal(str, bool)
def __init__(self, ifuslot, x, y, specid, specslot,
ifuid, ifurot, platescl, parent=None):
QtWidgets.QLabel.__init__(self, parent=parent)
IFU.__init__(self, ifuslot, x, y, specid, specslot,
ifuid, ifurot, platescl)
self._selected = False # by default the IFU is not selected
# setup a timer that started on mouse release and stopped on double
# click. If it times out the single click action is executed
self._release_timer = QtCore.QTimer(parent=self)
self._release_timer.setSingleShot(True)
self._release_timer.setInterval(QtWidgets.QApplication
.doubleClickInterval())
self._release_timer.timeout.connect(self._on_timer_timout)
# When a double click happens, the variable is set to True and switched
# back on the following mouse release event
self._is_double_click = False
self._default_aspect()
self._default_infos()
self._create_empty_image()
# force initialization of ``current_img`` to the empty image
self.current_img = self.empty_img
[docs] def _default_aspect(self):
'''Set the default aspect of the Widget.
Attributes
----------
border_style_selected, border_style_unselected : int
defaultsize : int
default to 42
'''
self.border_style_selected = QFrame.Box | QFrame.Raised
self.border_style_unselected = 0
self.defaultsize = 42
# Set the basic look of the widget
self.setScaledContents(True)
self.setLineWidth(3)
self.setMidLineWidth(3)
# Set the widget colour scheme button colour, background colour
palette = QtGui.QPalette(QtGui.QColor.fromRgb(0, 0, 255),
QtGui.QColor.fromRgb(0, 0, 0))
self.setPalette(palette)
self.setFrameStyle(self.border_style_unselected)
# Minimum size of widget in pixels
self.setMinimumWidth(self.defaultsize)
self.setMinimumHeight(self.defaultsize)
# Size policy.
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
qsp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred)
qsp.setHeightForWidth(True)
self.setSizePolicy(qsp)
[docs] def _default_infos(self):
'''Create and set the default tooltip and whatsThis message '''
# Add some info for the user when they hover a mouse over the button
self.setToolTip("""<html><head/><body><p>
<strong>IHMP:</strong>{:s},
<strong>IFUID:</strong>{:s}
<strong>SPECID:</strong>{:02d}
</p></body></html>""".format(self.ifuslot,
self.ifuid,
self.specid))
self.setWhatsThis("<html><head/><body><p>An IFU</p></body></html>")
[docs] def _create_empty_image(self):
'''Create an empty image, a white cross on black, and save it into the
:attr:`empty` attribute.'''
self.empty_img = QtGui.QImage(self.defaultsize, self.defaultsize,
QtGui.QImage.Format_ARGB32)
self.empty_img.fill(QtCore.Qt.black)
p = QtGui.QPainter()
p.begin(self.empty_img)
p.fillRect(QtCore.QRectF(0.0, 0.0, self.defaultsize,
self.defaultsize), QtCore.Qt.black)
# Sets a pen with a width of 3 pixels
p.setPen(QtGui.QPen(QtGui.QBrush(QtCore.Qt.white), 3))
# Draws the line to form a X
p.drawLine(self.defaultsize*0.1,
self.defaultsize*0.1,
self.defaultsize-self.defaultsize*0.1,
self.defaultsize-self.defaultsize*0.1)
p.drawLine(self.defaultsize*0.1,
self.defaultsize-self.defaultsize*0.1,
self.defaultsize-self.defaultsize*0.1,
self.defaultsize*0.1)
p.end()
@QtCore.Property(int, doc='Return the smaller between width and'
' height of the widget')
def hw_size(self):
return min(self.width(), self.height())
[docs] def mouseDoubleClickEvent(self, event):
"""Make sure that a double click is not interpreted as a two single
clicks. Beside this doesn't do anything else.
Qt don't deal with single and double click, so we do it here. If you
overrides either of :meth:`mouseReleaseEvent` or
:meth:`mouseDoubleClickEvent`, make sure to execute the parent class
implementation as first thing to avoid unexpected results.
Parameters
----------
event : :class:`PyQt5.QtGui.QMouseEvent`
"""
self._release_timer.stop()
self._is_double_click = True
[docs] def mouseReleaseEvent(self, event):
"""On a user clicking on the button, swap the logical value of the
:attr:`selected` attribute and change the look of the button to reflect
this.
Qt don't deal with single and double click, so we do it here. If you
overrides either of :meth:`mouseReleaseEvent` or
:meth:`mouseDoubleClickEvent`, make sure to execute the parent class
implementation as first thing to avoid unexpected results.
Parameters
----------
event : :class:`PyQt5.QtGui.QMouseEvent`
"""
if self._is_double_click:
self._is_double_click = False
return
self._release_timer.start()
[docs] @QtCore.Slot()
def _on_timer_timout(self):
'''Selected or deselect the IFU.
This method is also a PyQt slot.
'''
self.selected = not self.selected
self.sig_ifuToggled.emit(self.ifuslot, self.selected)
@QtCore.Property(bool, doc='Whether the IFU is selected or not.')
def selected(self):
return self._selected
@selected.setter
def selected(self, isSel):
self._selected = isSel
if isSel:
self.setFrameStyle(self.border_style_selected)
else:
self.setFrameStyle(self.border_style_unselected)
[docs] def prepare_image(self):
'''Prepare the image to show in the gui saving it into the
:attr:`current_img`. The base implementation saves the empty image
:attr:`empty_img` into :attr:`current_img`.
Subclasses can override this method and should call it to make sure
that sensible defaults are set
'''
self.current_img = self.empty_img
[docs] def show_image(self):
'''Create pixmap using the :attr:`current_img`, save it under
:attr:`pixmap_img` and set it. Then update the widget to show it.
'''
self.pixmap_img = QtGui.QPixmap.fromImage(self.current_img)
self.setPixmap(self.pixmap_img)
self.update()
[docs] def cleanup(self):
'''Cleanup method. Create the empty image and paint it.'''
self.current_img = self.empty_img
self.show_image()
[docs]class IFUSplitWidget(BaseIFUWidget):
'''Base IFU widget for all the cases in which the image displayed is
composed of multiple elements.
This class implements boilerplate code to setup a list of items to show,
with their position, and to display them.
Parameters
----------
all
same as :class:`BaseIFUWidget`
Attributes
----------
all
same as :class:`BaseIFUWidget`
thumb_items : list
list of :attr:`th_item`, initialized to an empty list by :meth:`setup`
target_dir : string
the selected directory
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
n_rows, n_cols : int
number of rows and columns to show in the IFU
th_item
rect_thumbnail
'''
def __init__(self, *args, **kwargs):
super(IFUSplitWidget, self).__init__(*args, **kwargs)
self.thumb_items = []
@property
def th_item(self):
'''Class representing one of the items to display in the IFU.
Returns
-------
:class:`BaseItem`
thumbnail item
'''
return BaseItem
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Set :attr:`target_dir`, :attr:`tab_dict`, :attr:`n_cols` and
:attr:`n_rows` and fill the :attr:`thumb_items` list.
The method uses directly the following keys of the ``tab_dict``
argument:
* ``cols``, ``rows`` (optional): list of objects, typically strings.
The thumbnail gets divided into len(cols)*len(rows) quadrants and
each one shows one file. If not given they default to a list with one
empty string. :attr:`n_cols` and :attr:`n_rows` attributes are set to
the number of elements in the two variables.
Each element of the :attr:`thumb_items` list is an instance of
:attr:`th_item` with:
* ``n_row`` and ``n_col`` set to the position of the item;
* ``row_name`` and ``col_name`` set to the names of the row and
column (from the configuration as described above) associated to the
element;
* ``image`` is the image of the item to show in the quadrant and is
initialized to ``None``;
* ``format_dict`` is a dictionary containing the following entries:
* all the elements in the input parameter ``format_dict``
* ``ifuslot``, ``ifuid``, ``specid``: ID of the slot, of the IFU
bundle and of the spectrograph it is connected to.
* ``col``, ``row``: replaced with each of the elements in the
``cols`` and ``rows`` configuration options.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : dictionary
dictionary with extra fields and values that can be used to e.g.
format file names
'''
# save the relevant information
self.target_dir = target_dir
self.tab_dict = tab_dict
cols = tab_dict.get('cols', ['', ])
rows = tab_dict.get('rows', ['', ])
self.n_cols, self.n_rows = len(cols), len(rows)
_format_dict = copy.copy(format_dict)
_format_dict.update(ifuslot=self.ifuslot, ifuid=self.ifuid,
specid=self.specid)
# Precompile the things needed to make and place the thumbnails
for (i, col), (j, row) in it.product(enumerate(cols),
enumerate(rows)):
_fd = copy.copy(_format_dict)
_fd.update(col=col, row=row)
self.thumb_items.append(self.th_item(n_col=i, n_row=j,
format_dict=_fd, col_name=col,
row_name=row))
@QtCore.Property(QtCore.QRectF, doc='Rectangle used to add the'
' thumbnails to the IFU. It uses the values of'
' :attr:`n_cols` and :attr:`n_rows` set in'
' :meth:`setup` to divide the widget in equal areas.')
def rect_thumbnail(self):
return QtCore.QRectF(0, 0,
self.hw_size / self.n_cols,
self.hw_size / self.n_rows)
[docs] def upper_left_qpoint(self, i, j):
'''Return the upper left point to use to add thumbnails to the IFU
widget given :attr:`n_cols` columns and :attr:`n_rows` rows.
Parameters
----------
i, j : int
column/row index of the image to add
Returns
-------
:class:`PyQt5.QtCore.QPointF`
position of the upper left corner of the rectangle
'''
return QtCore.QPointF(i * self.hw_size / self.n_cols,
j * self.hw_size / self.n_rows)
[docs] def load_thumbnail(self, item):
'''Create the thumbnail to be displayed in the IFU. If no image can be
created, set ``item.image`` to ``None``. Otherwise create a
:class:`PyQt5.QtGui.QImage` and assign it.
This implementation returns the input ``item`` unmodified.
Parameters
----------
item : :class:`BaseItem` or child
item representing one file to display
Returns
-------
item : :class:`BaseItem` or child
updated item with the ``image``
'''
return item
[docs] def prepare_image(self):
'''Create the images to show in the GUI.
Invoke the parent class method, to create empty images, then
loop through the elements of :attr:`thumb_items`, for each calls
:meth:`load_thumbnail`. Then filter the items for which the
:attr:`th_item.image` is ``None``. If nothing remains, do nothing,
otherwise add the images in the IFU and replace the empty image.
The size of the displayed image is :attr:`rect_thumbnail` and the
position is given by :meth:`upper_left_qpoint`.
If some image is missing the corresponding quadrant it is left black.
Derived classes probably don't need to override this method, but the
ones called from here, notably :meth:`load_thumbnail`.
'''
super(IFUSplitWidget, self).prepare_image()
# load the thumbnails
self.thumb_items = [self.load_thumbnail(i) for i in self.thumb_items]
# filter the thumb_items containing data
to_show = [i for i in self.thumb_items if i.image is not None]
# check if there are any data. If not, just return
if not to_show:
return
rect = self.rect_thumbnail
thumbnails = QtGui.QImage(self.hw_size, self.hw_size,
QtGui.QImage.Format_RGB16)
thumbnails.fill(QtCore.Qt.black)
p = QtGui.QPainter()
p.begin(thumbnails)
for item in to_show:
top_left_position = self.upper_left_qpoint(item.n_col, item.n_row)
rect.moveTopLeft(top_left_position)
p.drawImage(rect, item.image)
p.end()
self.current_img = thumbnails
[docs] def cleanup(self):
'''Remove the thumbnail from the gui and clear the :attr:`thumb_items`
list'''
super(IFUSplitWidget, self).cleanup()
self.thumb_items = []
[docs]class IFUFitsWidget(IFUSplitWidget):
"""A custom class designed to contain one or more fits files for the IFU.
Parameters
----------
all
same as :class:`IFUSplitWidget`
Attributes
----------
all
same as :class:`IFUSplitWidget`
zmin_ifu
zmax_ifu
zmin_global, zmax_global : float
global zmin and zmax. If they are both not ``None``, they are used
instead of the individual zscale values. On cleanup they are reset to
``None``
files_for_window
titles_for_window
tooltips_for_window
"""
def __init__(self, *args, **kwargs):
super(IFUFitsWidget, self).__init__(*args, **kwargs)
self.zmin_global = self.zmax_global = None
@property
def th_item(self):
'''Item to display in the IFU.
Returns
-------
:class:`FitsItem`
thumbnail item
'''
return FitsItem
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the images to show.
After executing the corresponding method of the base class, fills the
:attr:`th_item.fname` attributes using :attr:`th_item.format_dict`.
It also ensures that the thumbnail directory gets created (with
:meth:`ensure_thumb_dir`)
Besides what the parent method needs, this method uses directly the
following keys of the ``tab_dict`` argument:
* ``file_name`` (mandatory): name of the file(s) to show. It is
possible to format the file name using the `python formatting syntax
<https://docs.python.org/3/library/string.html#formatspec>`_.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
super(IFUFitsWidget, self).setup(target_dir, tab_dict, format_dict)
file_name = tab_dict['file_name']
for item in self.thumb_items:
fname = file_name.format(**item.format_dict)
item.fname = os.path.join(target_dir, fname)
self.ensure_thumb_dir(target_dir)
[docs] def ensure_thumb_dir(self, target_dir):
'''If necessary creates the thumbnail directory
:attr:`~vdat.gui.utils.THUMB_DIR` in ``target_dir``
Parameters
----------
target_dir : string
directory selected by the user
'''
try:
os.mkdir(os.path.join(target_dir, gui_utils.THUMB_DIR))
except OSError:
pass
[docs] def load_thumbnail(self, item):
'''Create, if necessary, and load the thumbnail. To avoid reloading it
multiple times, store the data into ``item.data`` and its min/max into
``item.zmin`` and ``item.zmax``.
Steps:
* :meth:`get_thumb`: if possible creates the thumbnail and returns its
name; if not possible, return None and set the above elements of
``item`` are set to ``None``;
* if the file name is returned and the file is newer than the stored
version, reloads the thumbnail and recompute ``zmin`` and ``zmax``;
* if there is a thumbnail, create a :class:`PyQt5.QtGui.QImage` with
the data, using either the image or the global ``zmin``/``zmax``, and
store it into ``item.image``
Derived classes probably don't need to override this method, but the
ones called from here, notably :meth:`get_thumb` and
:meth:`file_ctime`.
Parameters
----------
item : :attr:`th_item`
item representing one file to display
Returns
-------
item : :attr:`th_item`
updated item with the ``data``, ``image``, ``zmin`` and ``zmax``
attributes filled
'''
thumb_name = self.get_thumb(item.fname)
if thumb_name is None:
data = None
else:
thumb_ctime = self.file_ctime(thumb_name)
if item.ctime is None or item.ctime < thumb_ctime:
load_data = True
else:
load_data = False
if load_data:
with fits.open(thumb_name, memmap=False) as hdu:
data = hdu[0].data
item.zmin, item.zmax = ZScaleInterval().get_limits(data)
else:
data = item.data
if data is None:
item.data = item.image = item.zmin = item.zmax = item.ctime = None
else:
item.data = data
zmin, zmax = item.zmin, item.zmax
item.ctime = thumb_ctime
# convert the data array to a QImage
if self.zmin_global is not None and self.zmax_global is not None:
zmin = self.zmin_global
zmax = self.zmax_global
fimg = qimage2ndarray.gray2qimage(data, (zmin, zmax))
fimg = fimg.mirrored(False, True)
item.image = fimg
return item
[docs] def get_thumb(self, fname):
'''Create the thumbnail from the file name.
It gets the name of the thumbnail file using :meth:`thumb_name` and the
creation times of the input file and the thumbnail using
:meth:`file_ctime`. If ``fname`` does not exist, it makes sure that
there is also no thumbnail file, to make sure that files are not shown
after being removed.
If ``fname`` exists and/or is newer than the thumbnail (re)create it
using :meth:`create_thumb`
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
Returns
-------
thumb_name : string
name of the thumbnail file, or None if no thumbnail is created
'''
thumb_name = self.thumb_name(fname)
fname_ctime = self.file_ctime(fname)
thumb_ctime = self.file_ctime(thumb_name)
if fname_ctime is None:
if thumb_ctime is not None:
os.remove(thumb_name)
thumb_name = None
else:
if thumb_ctime is None or (fname_ctime > thumb_ctime):
try:
self.create_thumb(fname, thumb_name)
except Exception:
thumb_name = None
log = logging.getLogger('logger')
log.critical('Failed to create the thumbnail for'
' file "%s". Please report this to the'
' developers if you think it is a bug.',
fname, exc_info=True)
return thumb_name
[docs] def create_thumb(self, fname, thumb_name):
'''Create the thumbnail from ``fname``.
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
thumb_name : string
name of the file where to save the thumbnail
'''
gui_utils.rebin_fits(fname, outfile=thumb_name)
[docs] def thumb_name(self, fname):
'''Create the name to use for the thumbnail, prepending
:attr:`~vdat.gui.utils.THUMB_DIR`/:attr:`~vdat.gui.utils.THUMB_PREFIX`
to the file name
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
Returns
-------
string
name of the thumbnail file
'''
thumb_prefix = os.path.join(gui_utils.THUMB_DIR,
gui_utils.THUMB_PREFIX)
thumb_fname = prefix_filename(fname, thumb_prefix)
return thumb_fname
[docs] def file_ctime(self, fname):
'''Get the ctime from the file.
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
Returns
-------
float
time of creation of a file or None if the file does not exist
'''
try:
return os.path.getctime(fname)
except OSError: # fname does not exist
return None
[docs] def cleanup(self):
'''Remove the thumbnail from the gui and clear the :attr:`thumb_items`
list'''
super(IFUFitsWidget, self).cleanup()
self.zmin_global = self.zmax_global = None
@property
def zmin_ifu(self):
'''Returns the minimum zmin for the fits files shown in the widget,
or None if no file is shown'''
try:
return min(i.zmin for i in self.thumb_items if i.zmin)
except ValueError:
return None
@property
def zmax_ifu(self):
'''Returns the maximum zmax for the fits files shown in the widget,
or None if no file is shown'''
try:
return max(i.zmax for i in self.thumb_items if i.zmax)
except ValueError:
return None
[docs] def mouseDoubleClickEvent(self, event):
'''On double click open a popup window of type
:class:`.ifu_viewer.FitsViewerWindow` with details on the selected IFU.
The list of files passed to the window is taken from
:attr:`files_for_window`. If the list is empty no window will be
shown. The new window tab titles and tooltips are taken from
:attr:`titles_for_window` and :attr:`tooltips_for_window`. Derived
classes can overrides these three properties to give the appropriate
file and tabs names and tooltips.
Parameters
----------
event : :class:`PyQt5.QtGui.QMouseEvent`
'''
super(IFUFitsWidget, self).mouseDoubleClickEvent(event)
flist = self.files_for_window
if flist:
# get the redux dir from the selected directory
redux_dir = vconf.get_config('main')['general']['redux_dir']
relpath = os.path.relpath(self.target_dir, redux_dir)
name = [relpath, self.ifuslot]
fits_window = FitsViewerWindow(flist, self.tab_dict,
new_ds9_name=name, parent=self,
titles=self.titles_for_window,
tooltips=self.tooltips_for_window)
fits_window.show()
@property
def files_for_window(self):
'''Return the name of the files to pass to the fits viewer window. The
file names are created in :meth:`setup`, stored in :attr:`thumb_items`
and returned only if they exist.
Returns
-------
flist : list of string
file names to plug display in the fits viewer window
'''
flist = [f.fname for f in self.thumb_items if os.path.exists(f.fname)]
return flist
@property
def titles_for_window(self):
'''Return the tab titles for the files passes to the fits viewer window.
It must return either ``None`` or a list of strings with the same
length of :attr:`files_for_window`.
Returns
-------
``None``
'''
return None
@property
def tooltips_for_window(self):
'''Return the tab tool tips for the files passes to the fits viewer
window.
It should return either ``None`` or a list of strings with the same
length of :attr:`files_for_window`.
Returns
-------
``None``
'''
return None
[docs]class IFUQuickReconWidget(IFUFitsWidget):
'''Create IFU reconstructed images
Parameters
----------
all
same as :class:`IFUFitsWidget`
Attributes
----------
all
same as :class:`IFUFitsWidget`
enabled : bool
whether the reconstruction is disabled or not, i.e. if the
reconstructed object returned by
:func:`vdat.gui.utils.get_reconstructed` returns None or not
basenames
'''
def __init__(self, *args, **kwargs):
super(IFUQuickReconWidget, self).__init__(*args, **kwargs)
# self._reconstructed = gui_utils.get_reconstructed()
self.enabled = True
self._basenames = None
@property
def basenames(self):
'''List of strings used as basenames. If the property is present, setup
will loop through ``cols``, ``rows`` and ``basenames`` and replace the
``{basename}`` placeholder in the file names by each of the elements in
this property, shadowing the placeholder in :meth:`setup`'s
``format_dict``, if present. If the property is not set, the loop over
the basenames is not done in :meth:`setup`.'''
if self._basenames:
return self._basenames
else:
msg = "'{}' object has no attribute 'basenames' 'basenames'"
raise AttributeError(msg.format(self.__class__.__name__))
@basenames.setter
def basenames(self, value):
self._basenames = value
@basenames.deleter
def basenames(self):
self._basenames = None
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the reconstructed image to show.
The information needed to build the ifu reconstructed image are stored
into the :attr:`thumb_items`. The ``fname`` attribute from
:attr:`IFUFitsWidget.th_item` is used to store a list of file names
needed for the reconstruction.
The method uses the keys in ``tab_dict`` and provide the formatting
entries as described in :meth:`IFUFitsWidget.setup`. It overrides the
``basename`` formatting entry as described in
:attr:`IFUQuickReconWidget.basenames`. :attr:`n_cols` and
:attr:`n_rows` attributes are set to one.
It also ensures that the thumbnail directory is created.
Parameters
----------
all :
as in :meth:`IFUFitsWidget.setup`
'''
self.target_dir = target_dir
self.tab_dict = tab_dict
_format_dict = copy.copy(format_dict)
self._reconstructed = gui_utils.get_reconstructed()
self.enabled = self._reconstructed is not None
# save the relevant information
file_name = tab_dict['file_name']
cols = tab_dict.get('cols', ['', ])
rows = tab_dict.get('rows', ['', ])
self.n_cols, self.n_rows = 1, 1
# update the format dict
_format_dict.update(ifuslot=self.ifuslot, ifuid=self.ifuid,
specid=self.specid)
# prepare the basenames
add_basename = True # add {basename} to the format dict
try:
_basenames = self.basenames
except AttributeError:
try:
_basenames = [_format_dict['basename'], ]
except KeyError:
_basenames = ['', ]
add_basename = False
file_names = []
for col, row, basename in it.product(cols, rows, _basenames):
if add_basename:
_format_dict['basename'] = basename
fn = file_name.format(col=col, row=row, **_format_dict)
fn = os.path.join(target_dir, fn)
file_names.append(fn)
# save all the relevant information into the thumb_items
self.thumb_items.append(self.th_item(n_col=0, n_row=0,
fname=file_names, col_name=col,
row_name=row))
self.ensure_thumb_dir(target_dir)
[docs] def get_thumb(self, fname):
'''Create the reconstructed image from the file names.
This method is almost identical to :meth:`IFUFitsWidget.get_thumb`
except that it doesn't attempt to create thumbnails if the quick
reconstruction object is not present. However permits to show it if
available.
.. todo::
Replicating code is in general a bad idea, but I haven't found yet
a better solutions. This method implementation needs some
re-thinking.
Parameters
----------
fname : list of string
names of the file for which the reconstructed image is to be
created
Returns
-------
thumb_name : string
name of the thumbnail file, or None if no thumbnail is created
'''
thumb_name = self.thumb_name(fname)
fname_ctime = self.file_ctime(fname)
thumb_ctime = self.file_ctime(thumb_name)
if fname_ctime is None:
if thumb_ctime is not None:
os.remove(thumb_name)
thumb_name = None
else:
if self.enabled and (thumb_ctime is None or
fname_ctime > thumb_ctime):
try:
self.create_thumb(fname, thumb_name)
except Exception:
thumb_name = None
log = logging.getLogger('logger')
log.critical('Failed to create the thumbnail for'
' file "%s". Please report this to the'
' developers if you think it is a bug.',
fname, exc_info=True)
else:
thumb_name = thumb_name if thumb_ctime else None
return thumb_name
[docs] def create_thumb(self, fnames, thumb_name):
'''Create the reconstructed image from the names and save it
Parameters
----------
fnames : list of string
names of the file for which the reconstructed image is to be
created
thumb_name : string
name of the file where to save the thumbnail
'''
self._reconstructed.reconstruct(fnames)
self._reconstructed.write(thumb_name)
[docs] def thumb_name(self, fnames):
'''Alias of :meth:`reconstructed_name`'''
return self.reconstructed_name(fnames)
[docs] def reconstructed_name(self, file_names):
'''Creates the name to use to store the reconstructed image, as a md5
hash of the ``file_names``. This way we can get a unique name for every
combination of files.
Parameters
----------
file_names : list of strings
name of the files to use to create the thumbnail
Returns
-------
string
name of the reconstructed file
'''
recon_prefix = os.path.join(gui_utils.THUMB_DIR,
gui_utils.RECON_PREFIX)
path_ = os.path.dirname(file_names[0])
file_names = ''.join(file_names)
fn_hash = hashlib.md5(file_names.encode()).hexdigest()
fn_hash += '.fits'
fn_hash = os.path.join(path_, fn_hash)
return prefix_filename(fn_hash, recon_prefix)
[docs] def file_ctime(self, fname):
'''Get the ctime from the file. If fname is a list, get the newest time.
Parameters
----------
fname : string or list of strings
name of the file for which the thumbnail is to be created
Returns
-------
float
newest time of creation (ctime) of a file or None if the file does
not exist
'''
super_ = super(IFUQuickReconWidget, self)
try:
return super_.file_ctime(fname)
except TypeError: # it's a list
ctimes = [super_.file_ctime(fn) for fn in fname]
ctimes = [t for t in ctimes if t]
if ctimes:
return max(ctimes)
else:
return None
@property
def files_for_window(self):
'''Return the name of the reconstructed file that should go into the
fits viewer window.
If the file does not exist, try to create it. If it fails, return an
empty list.
Returns
-------
flist : list of string
file name to plug display in the fits viewer window
'''
item = self.load_thumbnail(self.thumb_items[0])
file_name = self.reconstructed_name(item.fname)
if os.path.exists(file_name):
return [file_name, ]
else:
return []
@property
def titles_for_window(self):
'''The title of the reconstructed tab in the fits window is just
"Reconstructed"'''
return ["Reconstructed", ]
@property
def tooltips_for_window(self):
'''The tooltip show all the files making up the reconstructed image'''
fnames = self.thumb_items[0].fname
prefix = 'Reconstructed image made from: '
fnames = ',\n'.join(fnames)
tooltip = '{}{}'.format(prefix, fnames)
return [tooltip, ]
[docs] def cleanup(self):
'''Cleanup the widget as in :meth:`IFUFitsWidget`, plus delete the
basenames'''
super(IFUQuickReconWidget, self).cleanup()
del self.basenames
[docs]class IFUCubeWidget(IFUFitsWidget):
'''Display thumbnails from data cubes instead of from 2D fits files.
This widget is very similar to the :class:`IFUFitsWidget`
Unless documented below, the class inherits signals, slots and connection
from :class:`IFUFitsWidget`.
Parameters
----------
all :
see :class:`IFUFitsWidget`
Attributes
----------
all :
see :class:`IFUFitsWidget`
'''
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the images to show.
The widget uses the same ``tab_dict`` keys as :class:`IFUFitsWidget`
and the same formatting entries. It also accept the following key of
the ``tab_dict`` argument:
* ``z_indx`` (optional): before creating the thumbnail for the data
cubes, the image is compressed along the z-dimension using the
median; if ``z_indx`` is not given, it uses the whole range,
otherwise it uses only the part of the cube in the range [z_indx[0],
z_indx[1])
Parameters
----------
all :
same as :meth:`IFUFitsWidget.setup`
'''
super(IFUCubeWidget, self).setup(target_dir, tab_dict, format_dict)
# indexes to use when collapsing the data cube
self._z_indx = (None, None)
if 'z_indx' in tab_dict:
z_indx = tab_dict['z_indx']
if len(z_indx) < 2:
msg = ('When given, ``z_indx`` must be a tuple or list of at'
' least two elements, not {}.'.format(len(z_indx)))
raise exceptions.VDATValueError(msg)
self._z_indx = tuple(z_indx[:2])
# only rebin by two
self._rebin = 2
[docs] def create_thumb(self, fname, thumb_name):
'''Create the thumbnail from ``fname``.
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
thumb_name : string
name of the file where to save the thumbnail
'''
gui_utils.rebin_fits(fname, outfile=thumb_name, rebin=self._rebin,
z_indx=self._z_indx)
[docs] def thumb_name(self, fname):
'''Create the name to use for the thumbnail, prepending
:attr:`~vdat.gui.utils.THUMB_DIR`/:attr:`~vdat.gui.utils.THUMB_PREFIX`
to the file name. If ``z_indx`` is given, add it to the thumbnail name
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
Returns
-------
string
name of the thumbnail file
'''
th_name = super(IFUCubeWidget, self).thumb_name(fname)
if self._z_indx != (None, None):
root, ext = os.path.splitext(th_name)
th_name = '{root}{sep}{z[0]}{sep}{z[1]}{ext}'
th_name = th_name.format(root=root, sep='-', z=self._z_indx,
ext=ext)
return th_name
[docs]class IFUMultiExtWidget(IFUFitsWidget):
'''Display thumbnails from one of the extensions of a multi-extension fits
file
This widget is very similar to the :class:`IFUFitsWidget`
Unless documented below, the class inherits signals, slots and connection
from :class:`IFUFitsWidget`.
Parameters
----------
all :
see :class:`IFUFitsWidget`
Attributes
----------
all :
see :class:`IFUFitsWidget`
'''
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the images to show.
The widget uses the same ``tab_dict`` keys as :class:`IFUFitsWidget`
and the same formatting entries. It also accept the following key of
the ``tab_dict`` argument:
* ``ext`` (int or string): index or number of the extension to display
in IFU
Parameters
----------
all :
same as :meth:`IFUFitsWidget.setup`
'''
super(IFUMultiExtWidget, self).setup(target_dir, tab_dict, format_dict)
# get the extension
self._ext = tab_dict['ext']
# only rebin by two
self._rebin = 2
[docs] def create_thumb(self, fname, thumb_name):
'''Create the thumbnail from ``fname``.
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
thumb_name : string
name of the file where to save the thumbnail
'''
gui_utils.rebin_fits(fname, outfile=thumb_name, rebin=self._rebin,
ext=self._ext)
[docs] def thumb_name(self, fname):
'''Create the name to use for the thumbnail, prepending
:attr:`~vdat.gui.utils.THUMB_DIR`/:attr:`~vdat.gui.utils.THUMB_PREFIX`
to the file name. If ``z_indx`` is given, add it to the thumbnail name
Parameters
----------
fname : string
name of the file for which the thumbnail is to be created
Returns
-------
string
name of the thumbnail file
'''
th_name = super(IFUMultiExtWidget, self).thumb_name(fname)
root, ext = os.path.splitext(th_name)
th_name = '{root}{sep}{fits_ext}{ext}'
th_name = th_name.format(root=root, sep='-', fits_ext=self._ext,
ext=ext)
return th_name
[docs]class TextFileWidget(BaseIFUWidget):
'''Widget that paints the number of lines in the widget. On double click
open a window to show the file content.
Parameters
----------
all
same as :class:`BaseIFUWidget`
'''
@QtCore.Property(QtCore.QRectF, doc='Rectangle with the size of'
' the thumbnails of the IFU. Use default values')
def rect_thumbnail(self):
return QtCore.QRectF(0, 0, self.defaultsize, self.defaultsize)
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the images to show.
The method uses directly the following keys of the ``tab_dict``
argument:
* ``file_name`` (mandatory): name of the file(s) to show. It is
possible to format the file name using the `python formatting syntax
<https://docs.python.org/3/library/string.html#formatspec>`_.
The following formatting entries are explicitly used in this method:
* ``ifuslot``, ``ifuid``, ``specid``: ID of the slot, of the IFU bundle
and of the spectrograph it is connected to.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
# save the relevant information
self.target_dir = target_dir
self.tab_dict = tab_dict
file_name = tab_dict['file_name']
file_name = file_name.format(ifuslot=self.ifuslot, ifuid=self.ifuid,
specid=self.specid, **format_dict)
self.file_name = os.path.join(target_dir, file_name)
[docs] def prepare_image(self):
'''Create the image to show in the GUI.
If the text file, whose name is created in :meth:`setup`, exists, paint
the number of lines of the file.
'''
super(TextFileWidget, self).prepare_image()
if not os.path.exists(self.file_name): # nothing to do
return
all_lines, lines = 0, 0
with open(self.file_name) as f:
for all_lines, line in enumerate(f, 1):
if not line.startswith('#'):
lines += 1
# prepare a black image
thumbnail = QtGui.QImage(self.defaultsize, self.defaultsize,
QtGui.QImage.Format_RGB16)
thumbnail.fill(QtCore.Qt.black)
# paint the number of lines
p = QtGui.QPainter()
p.begin(thumbnail)
p.setPen(QtGui.QPen(QtGui.QBrush(QtCore.Qt.white), 3))
flags = (QtCore.Qt.AlignCenter | QtCore.Qt.NoClip |
QtCore.Qt.TextWordWrap)
string = '{}\n({})'.format(all_lines, lines)
# if the string is too large, reduce the font. The code is taken from
# https://stackoverflow.com/questions/2202717/for-qt-4-6-x-how-to-auto-size-text-to-fit-in-a-specified-width
# and left here for future reference in case it becomes necessary to
# update the font size automatically
# void checkFontSize(QPainter *painter, const QString& name) {
# while (painter->fontMetrics().width(string) > rect().width()) {
# int newsize = painter->font().pointSize() - 1;
# painter->setFont(QFont(painter->font().family(), newsize));
# }
p.drawText(self.rect_thumbnail, flags, string)
p.end()
self.current_img = thumbnail
[docs] def mouseDoubleClickEvent(self, event):
'''On double click, if the text file exists, open a window of type
:class:`.ifu_viewer.FileEditorWindow`.
Parameters
----------
event : :class:`PyQt5.QtGui.QMouseEvent`
'''
super(TextFileWidget, self).mouseDoubleClickEvent(event)
if os.path.exists(self.file_name):
text_window = FileEditorWindow(self.file_name, self.tab_dict,
parent=self)
text_window.show()
[docs]class DistWidget(IFUSplitWidget):
'''Widget showing the distortion file and the corresponding region file
created with ``$CUREBIN/distview``
Parameters
----------
all
same as :class:`IFUSplitWidget`
Attributes
----------
all
same as :class:`IFUSplitWidget`
'''
def __init__(self, *args, **kwargs):
super(DistWidget, self).__init__(*args, **kwargs)
# find the $CUREBIN/distview executable
PATH = os.environ['PATH']
curebin = os.environ.get('CUREBIN')
if curebin:
PATH = curebin + ':' + PATH
self.dist_view = find_executable('distview', path=PATH)
@property
def th_item(self):
'''Item to display in the IFU.
Returns
-------
:class:`DistItem`
thumbnail item
'''
return DistItem
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations needed to create the images to show.
After executing the corresponding method of the base class, fills the
:attr:`th_item.fname`, :attr:`th_item.reg_fname' and
:attr:`th_item.fits_names` attributes using
:attr:`th_item.format_dict`.
Besides what the parent method needs, this method uses directly the
following keys of the ``tab_dict`` argument:
* ``file_name`` (mandatory): name of the file(s) to show. It is
possible to format the file name using the `python formatting syntax
<https://docs.python.org/3/library/string.html#formatspec>`_.
* ``fits_names`` (mandatory): list of names of the fits files to use
then displaying the distortion in DS9. If the list is empty, it is
not possible to display the data on DS9. Use the same formatting as
``file_name``
``
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
super(DistWidget, self).setup(target_dir, tab_dict, format_dict)
file_name = tab_dict['file_name']
fits_names = tab_dict['fits_names']
for item in self.thumb_items:
fname = file_name.format(**item.format_dict)
item.fname = os.path.join(target_dir, fname)
item.reg_fname = item.fname + '.reg'
for fn in fits_names:
fn = fn.format(**item.format_dict)
item.fits_names.append(os.path.join(target_dir, fn))
[docs] def load_thumbnail(self, item):
'''Create, if necessary, the region file and create the thumbnail to
display.
Steps:
* :meth:`get_region`: if possible creates the region file and returns
whether it exists or not;
* if the distortion and/or the region file exists, create an image with
the number or lines in either file and save it into
:attr:`item.image`
Parameters
----------
item : :attr:`th_item`
item representing one file to display
Returns
-------
item : :attr:`th_item`
updated item with the ``image`` attribute filled
'''
dist_exists, reg_exists = self.get_region(item)
# number of lines in the distortion and the region file
if (not dist_exists) and (not reg_exists):
item.image = None # clear the image
else:
with open(item.fname) as f:
for l in f:
if not l.startswith('#'):
# the first non comment line is the version
dist_version = l.strip().strip('\n')
break
# prepare a black image
rect = self.rect_thumbnail
thumbnail = QtGui.QImage(rect.width(), rect.height(),
QtGui.QImage.Format_RGB16)
thumbnail.fill(QtCore.Qt.black)
# paint the number of lines
p = QtGui.QPainter()
p.begin(thumbnail)
p.setPen(QtGui.QPen(QtGui.QBrush(QtCore.Qt.white), 3))
flags = (QtCore.Qt.AlignCenter | QtCore.Qt.NoClip |
QtCore.Qt.TextWordWrap)
string = 'D: {}\nR: {}'.format(dist_version,
'yes' if reg_exists else 'no')
for line in string.splitlines():
p = update_painter_font_size(p, rect, line)
p.drawText(rect, flags, string)
p.end()
# save the image
item.image = thumbnail
return item
[docs] def get_region(self, item):
'''Create the region file using ``$CUREBIN/distview``.
Steps:
* if the distortion exists, but not the region file, create the latter;
* if the distortion exists and is newer than the region file, recreate
the latter;
* if the distortion file does not exist but the region file does not,
remove the region file;
* if none exists, do nothing.
Parameters
----------
item : :attr:`th_item`
item representing one file to display
Returns
-------
dist_exists, reg_exists : bool
whether the distortion and the region file exist
'''
fname = item.fname
reg_fn = item.reg_fname
dist_exists = os.path.exists(fname)
reg_exists = os.path.exists(reg_fn)
if dist_exists:
if not reg_exists or (os.path.getctime(fname) >
os.path.getctime(reg_fn)):
# (re)create the region file
success = self._run_distview(fname, reg_fn)
if success:
reg_exists = True
else: # something when wrong
reg_exists = False
else:
if reg_exists:
os.remove(reg_fn)
reg_exists = False
return dist_exists, reg_exists
[docs] def _run_distview(self, fname, reg_fname):
'''Run ``$CUREBIN/distview`` to create the region file
Parameters
----------
fname, reg_fname : string
name of the distortion and the region files
Returns
-------
success : bool
whether the operation is successful
'''
with open(reg_fname, 'w') as f:
p = sp.Popen([self.dist_view, fname], stdout=f, stderr=sp.PIPE)
return_code = p.wait()
success = (return_code == 0)
if not success:
os.remove(reg_fname)
return success
[docs] def mouseDoubleClickEvent(self, event):
'''On double click, if the distortion files exist, open a window of
type :class:`.ifu_viewer.DistWindow`.
Parameters
----------
event : :class:`PyQt5.QtGui.QMouseEvent`
'''
super(DistWidget, self).mouseDoubleClickEvent(event)
dist_files, reg_files, fits_files = [], [], []
redux_dir = vconf.get_config('main')['general']['redux_dir']
relpath = os.path.relpath(self.target_dir, redux_dir)
name = [relpath, self.ifuslot]
for item in self.thumb_items:
if os.path.exists(item.fname):
dist_files.append(item.fname)
reg_files.append(item.reg_fname)
fits_files.append(item.fits_names)
if reg_files:
dist_window = DistWindow(dist_files, reg_files, fits_files,
self.tab_dict, new_ds9_name=name,
parent=self)
dist_window.show()