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()