diff --git a/mosviz/__init__.py b/mosviz/__init__.py index fc6d8c9..b36d9e6 100644 --- a/mosviz/__init__.py +++ b/mosviz/__init__.py @@ -18,7 +18,8 @@ def setup(): from .viewers.mos_viewer import MOSVizViewer from glue.config import qt_client - + from .plugins.cutout_tool import nIRSpec_cutout_tool + from .plugins.table_generator import nIRSpec_table_gen qt_client.add(MOSVizViewer) diff --git a/mosviz/data/ui/cutout_tool.ui b/mosviz/data/ui/cutout_tool.ui new file mode 100644 index 0000000..caf65af --- /dev/null +++ b/mosviz/data/ui/cutout_tool.ui @@ -0,0 +1,209 @@ + + + MainWindow + + + + 0 + 0 + 621 + 470 + + + + + 621 + 470 + + + + + 621 + 470 + + + + MainWindow + + + + + + + + + + X: + + + + + + + Y: + + + + + + + Path to Image: + + + + + + + Cutout size in arcsec: + + + + + + + Save cutouts at: + + + + + + + + + + background-color: rgba(255, 255, 255, 0); + + + + + + + Start + + + true + + + + + + + background-color: rgba(255, 255, 255, 203); + + + Qt::ScrollBarAlwaysOn + + + QTextEdit::FixedPixelWidth + + + 585 + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'.SF NS Text'; font-size:13pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600; text-decoration: underline;">Cutout Tool for JWST/NIRSpec MSA Programs</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">MOSViz is a quick-look analysis and visualization tool for multi-object spectroscopy (MOS). It is designed to work with JWST and non-JWST data: spectra and it can be used with associated images. MOSViz can display a “postage stamp” image of the source along with the spectra. These images are usually cutouts from larger images. The MOSViz Cutout Tool for JWST/NIRSpec MSA Programs uses information in the headers of NIRSpec spectra files to generate cutouts. MOSViz uses a special catalog called a MOSViz Table to organize file paths and information. The Cutout Tool will generate a MOSViz Table after the cutouts are saved.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Inputs</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">NIRSpec spectra directory: </span>This is the path to the NIRSpec MSA post-pipeline spectra. These files are saved in the level3 subdirectory of the pipeline output. The files should follow the following naming scheme:</p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">&lt;programName&gt;_&lt;objectName&gt;_&lt;instrument_filter&gt;_ &lt;grating&gt;_&lt;s2d|x1d&gt;.fits</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Image:</span> An image to make cutouts from. You can use NIRCam images as well as images from other sources. If a target's coordinates are out of range, that target will not be assigned a cutout. A list of skipped files will be saved in a text file at the save destination.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="docs-internal-guid-c8f1413b-5cd0-ce16-af7d-b5b1a747d2df"></a><span style=" font-family:'Arial'; font-weight:600; color:#000000; background-color:transparent;">C</span><span style=" font-family:'Arial'; font-weight:600; color:#000000; background-color:transparent;">utout Size:</span><span style=" font-family:'Arial'; color:#000000; background-color:transparent;"> The size of individual cutouts in arcsec</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Arial'; color:#000000;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Arial'; font-weight:600; color:#000000; background-color:transparent;">Save Path (Optional):</span><span style=" font-family:'Arial'; color:#000000; background-color:transparent;"> The Cutout Tool saves a subdirectory of cutouts and a MOSViz Table in the same directory as the spectra files by default. You can change the save destination of these items by clicking the &quot;Change&quot; button. Changing the save destination will generate a MOSViz Table that is unique to your computer (you will not be able to share it).</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Arial'; color:#000000;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Arial'; font-weight:600; color:#000000; background-color:transparent;">Outputs</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Arial'; font-weight:600; color:#000000; background-color:transparent;">Cutouts: </span><span style=" font-family:'Arial'; color:#000000; background-color:transparent;">A directory containing cutouts. The cutout images will follow the naming scheme:</span></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="docs-internal-guid-c8f1413b-5cd1-dca8-4611-362810d62030"></a><span style=" font-family:'Arial'; font-style:italic; color:#000000; background-color:transparent;">&lt;</span><span style=" font-family:'Arial'; font-style:italic; color:#000000; background-color:transparent;">objectName&gt;_&lt;programName&gt;_cutout.fits</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Arial'; color:#000000;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Arial'; font-weight:600; color:#000000;">MOSViz Table: </span><span style=" font-family:'Arial'; color:#000000;">A table containing paths to MSA spectra and cutout images. The MOSViz Table file will be saved in ASCII-Ecsv format. You can import this file into glue and visualize your data using MOSViz. Information contained in the MOSViz Table:</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Object ID. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Right Ascension. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Declination. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to 1d spectrum file. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to 2d spectrum file. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to cutout file, “None” otherwise. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Slit width and length. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Pixel scale.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + 24 + + + + + + + + + + Path to spectra directory: + + + + + + + Change + + + + + + + Browse + + + + + + + Progress: + + + + + + + Browse + + + + + + + + + + Generate MOSViz Table + + + + + + + + + + 0 + 0 + 621 + 22 + + + + + + + diff --git a/mosviz/data/ui/table_generator.ui b/mosviz/data/ui/table_generator.ui new file mode 100644 index 0000000..b0d8b67 --- /dev/null +++ b/mosviz/data/ui/table_generator.ui @@ -0,0 +1,216 @@ + + + MainWindow + + + + 0 + 0 + 621 + 470 + + + + + 621 + 470 + + + + + 621 + 470 + + + + MainWindow + + + + + + + background-color: rgba(255, 255, 255, 203); + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustIgnored + + + QTextEdit::FixedPixelWidth + + + 582 + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'.SF NS Text'; font-size:13pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600; text-decoration: underline;">MOSViz Table Generator for JWST/NIRSpec MSA Programs</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">MOSViz is a quick-look analysis and visualization tool for multi-object spectroscopy (MOS). It is designed to work with JWST and non-JWST data: spectra and it can be used with associated images. MOSViz uses a special catalog called a MOSViz Table to organize file paths and information. For JWST data, the MOSViz table generator uses information in the headers of NIRSpec spectra files to generate a MOSViz Table. There is a separate tool to create a table for non-JWST data.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Inputs</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">NIRSpec spectra directory: </span>This is the path to the NIRSpec MSA post-pipeline spectra. These files are saved in the level3 subdirectory of the pipeline output. The files should follow the naming scheme:</p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">&lt;programName&gt;_&lt;objectName&gt;_&lt;instrument_filter&gt;_ &lt;grating&gt;_&lt;s2d|x1d&gt;.fits</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Cutout Image directory: </span>MOSViz can display a “postage stamp” image of the source along with the spectra. These images are usually cutouts from larger images. You can add cutout images by selecting “Add cutouts” and by providing a path to a directory containing them. If the you would like to generate cutouts from an image, please use the NIRSpec Cutout Tool. You can create and use your own cutouts as well; the cutout images should follow the naming scheme:</p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">&lt;objectName&gt;_&lt;programName&gt;_cutout.fits</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Outputs</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">MOSViz Table: </span>MOSViz Table: A table containing paths to MSA spectra and cutout images will be saved in the same directory as the spectra files. You can import this file into glue and visualize your data using MOSViz. Information contained in MOSViz Table:</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Object ID. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Right Ascension. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Declination. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to 1d spectrum file. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to 2d spectrum file. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Path to cutout file, “None” otherwise. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Slit width and length. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- Pixel scale.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + <html><head/><body><p><span style=" font-family:'.SF NS Text,sans-serif';">You can add cutout images by selecting “Add cutouts” and by providing a path to a directory containing them. The cutout images should follow the following naming scheme: </span><span style=" font-family:'.SF NS Text,sans-serif'; font-style:italic;">&lt;objectName&gt;_&lt;programName&gt;_cutout.fits </span></p></body></html> + + + Browse + + + + + + + <html><head/><body><p>Path to level 3 directory containing the NIRSpec fits files. </p></body></html> + + + Browse + + + + + + + Path to spectra directory: + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + Cutout Image directory: + + + + + + + + + + Generate Table + + + false + + + false + + + true + + + false + + + + + + + Qt::Horizontal + + + + + + + <html><head/><body><p><span style=" font-family:'.SF NS Text,sans-serif';">The cutout images should follow the following naming scheme: </span><span style=" font-family:'.SF NS Text,sans-serif'; font-style:italic;">&lt;objectName&gt;_&lt;programName&gt;_cutout.fits </span></p></body></html> + + + Add Cutouts + + + + + + + <html><head/><body><p><span style=" font-family:'.SF NS Text,sans-serif';">MOSViz can display a “postage stamp” image of the source along with the spectra. If the you would like to automatically generate cutouts from an image, please use the </span><span style=" font-family:'.SF NS Text,sans-serif'; font-weight:600; color:#800080;">NIRSpec Cutout Tool </span><span style=" font-family:'.SF NS Text,sans-serif';">instead to generate the MOSViz Table. </span></p></body></html> + + + No Cutouts + + + true + + + false + + + + + + + Cutout Images: + + + + + + + + + + 0 + 0 + 621 + 22 + + + + + + + diff --git a/mosviz/loaders/loader_selection.py b/mosviz/loaders/loader_selection.py index b52be55..2399739 100644 --- a/mosviz/loaders/loader_selection.py +++ b/mosviz/loaders/loader_selection.py @@ -172,12 +172,13 @@ def _validation_checks(self, *args, **kwargs): for filename in filenames: file_path = os.path.join(path, filename) - - if not os.path.exists(file_path): - self.validate(False, "File '{0}' listed in column '{1}' " - "(currently selected for {2}) does not " - "exist.".format(filename, column_name, column)) - return + base = os.path.basename(file_path) + if base != "None": + if not os.path.exists(file_path): + self.validate(False, "File '{0}' listed in column '{1}' " + "(currently selected for {2}) does not " + "exist.".format(filename, column_name, column)) + return # Check whether the loaders are able to read in the spectra/cutouts. We # can't check whether all spectra/cutouts can be read since this would @@ -193,8 +194,17 @@ def _validation_checks(self, *args, **kwargs): column_name = getattr(self, column) filenames = self.data.get_component(column_name).labels + test_filename = "None" + for filename in filenames: + if os.path.basename(filename) != "None": + test_filename = filename + break + + if test_filename == "None": + continue + try: - loader(filenames[0]) + loader(test_filename) except Exception as e: self.validate(False, "An error occurred when trying to read in " "'{0}' using the loader '{1}' (see terminal for " diff --git a/mosviz/plugins/__init__.py b/mosviz/plugins/__init__.py new file mode 100644 index 0000000..9dc41a8 --- /dev/null +++ b/mosviz/plugins/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import + +from .cutout_tool import * +from .table_generator import * diff --git a/mosviz/plugins/cutout_tool.py b/mosviz/plugins/cutout_tool.py new file mode 100644 index 0000000..723711b --- /dev/null +++ b/mosviz/plugins/cutout_tool.py @@ -0,0 +1,472 @@ +from __future__ import absolute_import, division, print_function + +import sys +import os +from glob import glob +from time import sleep +import numpy as np +import random as rn +from functools import partial + +from qtpy import compat +from qtpy.uic import loadUi +from qtpy.QtWidgets import QMainWindow,QApplication +from qtpy.QtWidgets import QWidget,QMessageBox +from qtpy.QtCore import Qt + +from glue.config import menubar_plugin + +from astropy.table import QTable +import astropy.units as u +from astropy.io import fits +from astropy.wcs import WCS, NoConvergence +from astropy.coordinates import SkyCoord +from astropy.nddata.utils import (Cutout2D, NoOverlapError) +from astropy import log +from astropy.coordinates import Angle + +from .. import UI_DIR + +__all__ = ['CutoutTool','nIRSpec_cutout_tool'] + +class CutoutTool (QMainWindow): + + def __init__ (self, parent=None): + super(CutoutTool,self).__init__(parent,Qt.WindowStaysOnTopHint) + self.title = "NIRSpec Cutout Tool" + self.spec_path = "" + self.img_path = "" + self.save_path = "" + self.cutout_x_size = 0 + self.cutout_y_size = 0 + self.cutout_x_size_default = "" + self.cutout_y_size_default = "" + self.custom_save_path = False + self.imageExt = ['*.fits', '*.FITS', '*.fit', '*.FIT', + '*.fts', '*.FTS', '*.fits.Z', '*.fits.z', '*.fitz', + '*.FITZ', '*.ftz', '*.FTZ', '*.fz', '*.FZ'] + self.initUI() + + def initUI(self): + path = os.path.join(UI_DIR, 'cutout_tool.ui') + loadUi(path, self) + self.setWindowTitle(self.title) + self.statusBar().showMessage("Waiting for user input") + + self.progressBar.reset() + self.inSave.setDisabled(True) + + self.inSpectra.textChanged.connect(self.update_save) + self.start.clicked.connect(self.call_main) + self.savePathButton.clicked.connect(self.custom_path) + self.specBrowseButton.clicked.connect(self.get_spec_path) + self.imageBrowseButton.clicked.connect(self.get_img_path) + + self.xSize.setText(self.cutout_x_size_default) + self.ySize.setText(self.cutout_y_size_default) + self.xSize.selectAll() + self.ySize.selectAll() + + self.table_checkBox.setChecked(True) + + self.show() + + def get_spec_path(self): + self.spec_path = compat.getexistingdirectory() + self.inSpectra.setText(self.spec_path) + self.update_save() + + def get_img_path(self): + self.img_path = compat.getopenfilename(filters=" ".join(self.imageExt))[0] + self.inImage.setText(self.img_path) + + def update_save(self): + if not self.custom_save_path : + self.save_path = self.inSpectra.text() + if self.save_path == "": + self.inSave.setText("") + else: + self.inSave.setText(os.path.join(self.save_path ,"[Name]_cutouts","")) + + def custom_path(self): + """ + User specified save path. Renders paths in output absolute. + Can also revert to default. + """ + if self.savePathButton.text() == "Change": + info = QMessageBox.information(self, "Info", "Changing the save destination will generate a MOSViz Table" + " that is unique to your computer (you will not be able to share it).") + if self.custom_save_path == False: + self.save_path = compat.getexistingdirectory() + if self.save_path == "": + return + self.inSave.setText(os.path.join(self.save_path ,"[Name]_cutouts","")) + self.savePathButton.setText("Revert") + self.custom_save_path = True + else: + self.custom_save_path = False + self.savePathButton.setText("Change") + self.update_save() + + def unique_id(self, ID, IDList): + keys = IDList.keys() + if ID not in keys: + IDList[ID] = 0 + return ID, IDList + + IDList[ID] += 1 + ID = ID+"-%s"%(IDList[ID]) + + return ID, IDList + + def collect_text(self): + """ + Process information in the input boxes. + Checks if user inputs are functional. + + Returns + ------- + userOk : bool + True for success, False otherwise. + + """ + self.statusBar().showMessage("Reading input") + self.spec_path = self.inSpectra.text() + self.img_path = self.inImage.text() + + if not self.custom_save_path : #Just in case + self.save_path = self.spec_path + + if self.xSize.text() != "": + try: + self.cutout_x_size = float(self.xSize.text()) + except ValueError: + self.cutout_x_size = -1 + else: + self.cutout_x_size = -1 + if self.ySize.text() != "": + try: + self.cutout_y_size = float(self.ySize.text()) + except ValueError: + self.cutout_y_size = -1 + else: + self.cutout_y_size = -1 + + + userOk = True #meaning did the user input correctly? + + if self.spec_path == "": + self.inSpectra.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.inSpectra.setStyleSheet("") + + if self.img_path == "": + self.inImage.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.inImage.setStyleSheet("") + + if self.cutout_x_size <= 0: + self.xSize.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.xSize.setStyleSheet("") + + if self.cutout_y_size <= 0: + self.ySize.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.ySize.setStyleSheet("") + + if userOk: + if not os.path.isdir(self.spec_path): + info = QMessageBox.information(self, "Status:", + "Broken path:\n\n"+self.spec_path) + self.inSpectra.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + if not os.path.isfile(self.img_path): + info = QMessageBox.information(self, "Status:", + "Broken path:\n\n"+self.img_path) + self.inImage.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + + + return userOk + + #This function will be modified further to add features. + def make_cutouts(self, imagename, table, image_label, image_ext=0, + clobber=False, verbose=True): + """ + Function to generate cutouts. + """ + from reproject import reproject_interp + + with fits.open(imagename) as pf: + data = pf[image_ext].data + wcs = WCS(pf[image_ext].header) + + # It is more efficient to operate on an entire column at once. + c = SkyCoord(table['ra'], table['dec']) + x = (table['cutout_x_size'] / table['pix_scale']).value # pix + y = (table['cutout_y_size'] / table['pix_scale']).value # pix + pscl = table['pix_scale'].to(u.deg / u.pix) + + apply_rotation = False + + # Sub-directory, relative to working directory. + path = '{0}_cutouts'.format(image_label) + if not os.path.exists(path): + os.mkdir(path) + + cutcls = partial(Cutout2D, data, wcs=wcs, mode='partial') + + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(len(table)-1) + self.progressBar.reset() + counter = 0 + success_counter = 0 + success_table = [False for x in range(len(table['id']))] + print(len(table['id'])) + for position, x_pix, y_pix, pix_scl in zip(c, x, y, pscl): + self.progressBar.setValue(counter) + row = table[counter] + print(row) + print(position) + counter += 1 + self.statusBar().showMessage("Making cutouts (%s/%s)"%(counter, len(success_table))) + QApplication.processEvents() + if apply_rotation: + pix_rot = row['cutout_pa'].to(u.degree).value + + cutout_wcs = WCS(naxis=2) + cutout_wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] + cutout_wcs.wcs.crval = [position.ra.deg, position.dec.deg] + cutout_wcs.wcs.crpix = [(x_pix - 1) * 0.5, (y_pix - 1) * 0.5] + + try: + cutout_wcs.wcs.cd = wcs.wcs.cd + cutout_wcs.rotateCD(-pix_rot) + except AttributeError: + cutout_wcs.wcs.cdelt = wcs.wcs.cdelt + cutout_wcs.wcs.crota = [0, -pix_rot] + + cutout_hdr = cutout_wcs.to_header() + try: + cutout_arr = reproject_interp( + (data, wcs), cutout_hdr, shape_out=(int(np.ceil(y_pix)), int(np.ceil(x_pix))), order=2) + except Exception: + if verbose: + log.info('reproject failed: ' + 'Skipping {0}'.format(row['id'])) + continue + + cutout_arr = cutout_arr[0] # Ignore footprint + cutout_hdr['OBJ_ROT'] = (pix_rot, 'Cutout rotation in degrees') + + else: + try: + cutout = cutcls(position, size=(y_pix, x_pix)) + except NoConvergence: + if verbose: + log.info('WCS solution did not converge: ' + 'Skipping {0}'.format(row['id'])) + continue + except NoOverlapError: + if verbose: + log.info('Cutout is not on image: ' + 'Skipping {0}'.format(row['id'])) + continue + else: + cutout_hdr = cutout.wcs.to_header() + cutout_arr = cutout.data + + if np.array_equiv(cutout_arr, 0): + if verbose: + log.info('No data in cutout: Skipping {0}'.format(row['id'])) + continue + + fname = os.path.join( + path, '{0}_{1}_cutout.fits'.format(row['id'], image_label)) + + # Construct FITS HDU. + hdu = fits.PrimaryHDU(cutout_arr) + hdu.header.update(cutout_hdr) + hdu.header['OBJ_RA'] = (position.ra.deg, 'Cutout object RA in deg') + hdu.header['OBJ_DEC'] = (position.dec.deg, 'Cutout object DEC in deg') + + hdu.writeto(fname, overwrite=clobber) + success_counter += 1 + success_table[counter-1] = True + if verbose: + log.info('Wrote {0}'.format(fname)) + + self.progressBar.setValue(counter) + QApplication.processEvents() + if success_counter != len(success_table): + with open("skipped_cutout_files.txt","w") as f: + for i, x in enumerate(table['id']): + status = success_table[i] + if status == False: + f.write(table["spectrum2d"][i]+"\n") + + + return success_counter, success_table + + def call_main(self): + """ + Calls the main function and handles exceptions. + """ + try: + self.main() + except Exception as e: + info = QMessageBox.critical(self, "Error", str(e)) + self.close() + + def main(self): + """ + Main function that uses information provided + by the user and in the headers of spectra files + to construct a catalog and make postage stamp cutouts. + """ + userOK = self.collect_text() #meaning did the user input ok? + if not userOK: + self.statusBar().showMessage("Please fill in all fields") + return + + self.start.setDisabled(True) + self.statusBar().showMessage("Making a list of files") + + + target_names = [] + fb = [] # File Base + searchPath = os.path.join(self.spec_path,"*s2d.fits") + for fn in glob(searchPath): + name = os.path.basename(fn) + name = name.split("_") #Split up file name + if len(name) != 5: + continue + name = name[-4] # Get the target name from file + target_names.append(name) + fb.append(fn) + + #If no files are found, close the app + if len(fb) == 0: + self.statusBar().showMessage("NIRSpec files not found") + self.start.setDisabled(False) + info = QMessageBox.information(self, "Status:", "No NIRSpec files found in this directory\n" + "File Name Format:\n\n" + "___ _.fits") + return + + #Change working path to save path + cwd = os.getcwd() + os.chdir(self.save_path ) + self.statusBar().showMessage("Making catalog") + + #Setup local catalog. + catalog = [] + IDList = {} #Counter for objects with the same ID + + #Extract info from spectra files and save to catalog. + projectName = os.path.basename(fn).split("_")[0] + for idx, fn in enumerate(fb): #For file name in file base: + row = [] + headx1d = fits.open(fn.replace("s2d.fits", "x1d.fits"))['extract1d'].header + wcs = WCS(headx1d) + w1, w2 = wcs.wcs_pix2world(0., 0., 1) + w1 = w1.tolist() + w2 = w2.tolist() + + head = fits.getheader(fn) + ID = target_names[idx] + ID, IDList = self.unique_id(ID, IDList) + + if self.custom_save_path: + spectrum1d = os.path.abspath(fn.replace("s2d.fits", "x1d.fits")) + spectrum2d = os.path.abspath(fn) + cutout = os.path.join(self.save_path ,projectName+"_cutouts/"+ID+"_"+projectName+"_cutout.fits") + else: + spectrum1d = os.path.join(".",os.path.basename(fn).replace("s2d.fits", "x1d.fits")) + spectrum2d = os.path.join(".",os.path.basename(fn)) + cutout = os.path.join(".",os.path.join(projectName+"_cutouts/"+ID+"_"+projectName+"_cutout.fits")) + + row.append(ID) #id + row.append(w1) #ra + row.append(w2) #dec + row.append(spectrum1d) #spectrum1d + row.append(spectrum2d) #spectrum2d + row.append(cutout) #cutout + row.append(0.2) #slit_width + row.append(3.3) #slit_length + row.append(head["CDELT2"]) #pix_scale (spatial_pixel_scale) + row.append(self.cutout_x_size) #cutout_x_size + row.append(self.cutout_y_size) #cutout_y_size + row.append(head["PA_APER"]) #slit_pa + + catalog.append(row) #Add row to catalog + + #Make MOSViz Table using info in local catalog. + self.statusBar().showMessage("Making MOSViz Table") + colNames = ["id","ra","dec","spectrum1d","spectrum2d","cutout", + "slit_width","slit_length","pix_scale", + "cutout_x_size", "cutout_y_size", "slit_pa"] + + t = QTable(rows=catalog, names=colNames) + t["ra"].unit = u.deg + t["dec"].unit = u.deg + t["slit_width"].unit = u.arcsec + t["slit_length"].unit = u.arcsec + t["pix_scale"].unit = (u.arcsec/u.pix) + t["cutout_x_size"].unit = u.arcsec + t["cutout_y_size"].unit = u.arcsec + t["slit_pa"].unit = u.deg + + #Make cutouts using info in catalog. + self.statusBar().showMessage("Making cutouts") + success_counter, success_table = self.make_cutouts(self.img_path, t, projectName, clobber=True) + + #For files that do not have a cutout, place "None" as a filename place holder. + for idx, success in enumerate(success_table): + if not success: + t["cutout"][idx] = "None" + + #Write MOSViz Table to file. + self.statusBar().showMessage("Saving MOSViz Table") + moscatalogname = os.path.join(self.save_path ,projectName+"_MOSViz_Table.txt") + t.remove_column("cutout_x_size") + t.remove_column("cutout_y_size") + t.remove_column("slit_pa") + if self.table_checkBox.isChecked(): + t.write(moscatalogname, format="ascii.ecsv", overwrite=True) + + #Change back dir. + self.statusBar().showMessage("DONE!") + os.chdir(cwd) + + #Give notice to user on status. + string = "Cutouts were made for %s out of %s files\n\nSaved at: %s" %( + success_counter,len(success_table), + os.path.join(self.save_path ,projectName+"_cutouts/")) + info = QMessageBox.information(self, "Status:", string) + + #If some spectra files do not have a cutout, a list of their names will be saved to + # 'skipped_cutout_files.txt' in the save dir as the MOSViz Table file. + if success_counter != len(success_table): + info = QMessageBox.information(self, "Status:", "A list of spectra files" + "without cutouts is saved in" + "'skipped_cutout_files.txt' at:\n\n%s" + %os.path.join(self.save_path , + "skipped_cutout_files.txt")) + self.close() + return + +@menubar_plugin("Cutout Tool (JWST/NIRSpec MSA)") +def nIRSpec_cutout_tool(session, data_collection): + ex = CutoutTool(session.application) + return + +if __name__ == "__main__": + app = QApplication(sys.argv) + ex = CutoutTool(app) + sys.exit(app.exec_()) diff --git a/mosviz/plugins/table_generator.py b/mosviz/plugins/table_generator.py new file mode 100644 index 0000000..bf7c73c --- /dev/null +++ b/mosviz/plugins/table_generator.py @@ -0,0 +1,303 @@ +from __future__ import absolute_import, division, print_function + +import sys +import os +from glob import glob +from time import sleep +import numpy as np +import random as rn +from functools import partial + +from qtpy import compat +from qtpy.uic import loadUi +from qtpy.QtWidgets import QMainWindow,QApplication +from qtpy.QtWidgets import QWidget,QMessageBox +from qtpy.QtCore import Qt + +from glue.config import menubar_plugin + +from astropy.table import QTable, Column +import astropy.units as u +from astropy.io import fits +from astropy.wcs import WCS, NoConvergence +from astropy.coordinates import SkyCoord +from astropy.nddata.utils import (Cutout2D, NoOverlapError) +from astropy import log +from astropy.coordinates import Angle + +from .. import UI_DIR + +__all__ = ['TableGen','nIRSpec_table_gen'] + +class TableGen(QMainWindow): + + def __init__ (self, parent=None): + super(TableGen,self).__init__(parent,Qt.WindowStaysOnTopHint) + self.title = "MOSViz Table Generator for NIRSpec" + self.spec_path = "" + self.img_path = "" + self.cutouts_option = False + self.abs_path = False + self.image_ext = ['*.fits', '*.FITS', '*.fit', '*.FIT', + '*.fts', '*.FTS', '*.fits.Z', '*.fits.z', '*.fitz', + '*.FITZ', '*.ftz', '*.FTZ', '*.fz', '*.FZ'] + self.initUI() + + def initUI(self): + path = os.path.join(UI_DIR, 'table_generator.ui') + loadUi(path, self) + + self.setWindowTitle(self.title) + self.statusBar().showMessage("Waiting for user input") + + self.noCutoutsRadioButton.setChecked(True) + + self.imageBrowseButton.setDisabled(True) + self.inImage.setDisabled(True) + self.inImage.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + + self.genTableButton.clicked.connect(self.call_main) + self.noCutoutsRadioButton.toggled.connect(self.no_cutout) + self.addCutoutsRadioButton.toggled.connect(self.add_cutout) + self.specBrowseButton.clicked.connect(self.get_spec_path) + self.imageBrowseButton.clicked.connect(self.get_img_path) + + self.show() + + def no_cutout(self): + self.imageBrowseButton.setDisabled(True) + self.inImage.setDisabled(True) + self.inImage.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + return + + def add_cutout(self): + self.imageBrowseButton.setDisabled(False) + self.inImage.setDisabled(False) + self.inImage.setStyleSheet("") + return + + def get_spec_path(self): + self.spec_path = compat.getexistingdirectory() + self.inSpectra.setText(self.spec_path) + return + + def get_img_path(self): + self.img_path = compat.getexistingdirectory() + self.inImage.setText(self.img_path) + return + + def check_image_path(self): + same_path = os.path.samefile(os.path.dirname(self.img_path), + os.path.abspath(self.spec_path)) + self.abs_path = not same_path + return + + def collect_text(self): + """ + Process information in the input boxes. + Checks if user inputs are functional. + + Returns + ------- + userOk : bool + True for success, False otherwise. + + """ + self.statusBar().showMessage("Reading input") + self.spec_path = self.inSpectra.text() + self.img_path = self.inImage.text() + + userOk = True #meaning did the user input correctly? + + if self.spec_path == "": + self.inSpectra.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.inSpectra.setStyleSheet("") + + if self.addCutoutsRadioButton.isChecked(): + if self.img_path == "": + self.inImage.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.inImage.setStyleSheet("") + + if userOk: + if not os.path.isdir(self.spec_path): + info = QMessageBox.information(self, "Status:", "Broken path:\n\n"+self.spec_path) + self.inSpectra.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + + if self.addCutoutsRadioButton.isChecked(): + if not os.path.isdir(self.img_path): + info = QMessageBox.information(self, "Status:", "Broken path:\n\n"+self.img_path) + self.inImage.setStyleSheet("background-color: rgba(255, 0, 0, 128);") + userOk = False + else: + self.check_image_path() + return userOk + + def unique_id(self, ID, IDList): + keys = IDList.keys() + if ID not in keys: + IDList[ID] = 0 + return ID, IDList + + IDList[ID] += 1 + ID = ID+"-%s"%(IDList[ID]) + + return ID, IDList + + def get_cutout(self, fn, ID): + name = os.path.basename(fn) + name = name.split("_") + + img_fn = "_".join([ID,name[0],"cutout.fits"]) + img_fn = os.path.join(self.img_path, img_fn) + + if os.path.isfile(img_fn): + if self.abs_path: + return os.path.abspath(img_fn) + else: + return os.path.relpath(img_fn, self.spec_path) + + img_fn = "_".join([name[1],name[0],"cutout.fits"]) + img_fn = os.path.join(self.img_path, img_fn) + + if os.path.isfile(img_fn): + if self.abs_path: + return os.path.abspath(img_fn) + else: + return os.path.relpath(img_fn, self.spec_path) + else: + return "None" + + def call_main(self): + """ + Calls the main function and handles exceptions. + """ + try: + self.main() + except Exception as e: + info = QMessageBox.critical(self, "Error", str(e)) + self.close() + + def main(self): + """ + Main metod that will take input from the user, make a + MOSViz Table and save it to a file. It will use the information + in the headers of the spectra files to fill in rows of the table. + If the user has cutouts, it will look for an image file with the corresponding + object + project name and add it to the Table. + """ + userOK = self.collect_text() #meaning did the user input ok? + if not userOK: + self.statusBar().showMessage("Please fill in all fields") + return + + self.genTableButton.setDisabled(True) + self.statusBar().showMessage("Making a list of files") + + target_names = [] + fb = [] # File Base + searchPath = os.path.join(self.spec_path,"*s2d.fits") + for fn in glob(searchPath): + name = os.path.basename(fn) + name = name.split("_") #Split up file name + if len(name) != 5: + continue + name = name[-4] # Get the target name from file + target_names.append(name) + fb.append(fn) + + #If no files are found, close the app + if len(fb) == 0: + self.statusBar().showMessage("NIRSpec files not found") + self.genTableButton.setDisabled(False) + info = QMessageBox.information(self, "Status:", "No NIRSpec files found in this directory\n" + "File Name Format:\n\n" + "___ _.fits") + return + + #Change working path to save path + cwd = os.getcwd() + os.chdir(self.spec_path) + self.statusBar().showMessage("Making catalog") + + #Setup local catalog. + catalog = [] + IDList = {} #Counter for objects with the same ID + + #Extract info from spectra files and save to catalog. + projectName = os.path.basename(fn).split("_")[0] + for idx, fn in enumerate(fb): #For file name in file base: + row = [] + headx1d = fits.open(fn.replace("s2d.fits", "x1d.fits"))['extract1d'].header + wcs = WCS(headx1d) + w1, w2 = wcs.wcs_pix2world(0., 0., 1) + w1 = w1.tolist() + w2 = w2.tolist() + + head = fits.getheader(fn) + ID = target_names[idx] + ID, IDList = self.unique_id(ID, IDList) + + if self.addCutoutsRadioButton.isChecked(): + cutout = self.get_cutout(fn, ID) + else: + cutout = "None" + + if self.abs_path: + spectrum1d = os.path.abspath(fn.replace("s2d.fits", "x1d.fits")) + spectrum2d = os.path.abspath(fn) + else: + spectrum1d = os.path.join(".",os.path.basename(fn).replace("s2d.fits", "x1d.fits")) + spectrum2d = os.path.join(".",os.path.basename(fn)) + + row.append(ID) #id + row.append(w1) #ra + row.append(w2) #dec + row.append(spectrum1d) #spectrum1d + row.append(spectrum2d) #spectrum2d + row.append(cutout) #cutout + row.append(0.2) #slit_width + row.append(3.3) #slit_length + row.append(head["CDELT2"]) #pix_scale (spatial_pixel_scale) + + catalog.append(row) #Add row to catalog + + #Make and write MOSViz table + self.statusBar().showMessage("Making MOSViz catalog") + + colNames = ["id","ra","dec","spectrum1d","spectrum2d","cutout", + "slit_width","slit_length","pix_scale"] + t = QTable(rows=catalog, names=colNames) + t["ra"].unit = u.deg + t["dec"].unit = u.deg + t["slit_width"].unit = u.arcsec + t["slit_length"].unit = u.arcsec + t["pix_scale"].unit = (u.arcsec/u.pix) + + self.statusBar().showMessage("Saving MOSViz catalog") + #Write MOSViz Table to file. + moscatalogname = projectName+"_MOSViz_Table.txt" + t.write(moscatalogname, format="ascii.ecsv", overwrite=True) + + #Change back dir. + self.statusBar().showMessage("DONE!") + os.chdir(cwd) + + moscatalogname = os.path.abspath(os.path.join(self.spec_path,moscatalogname)) + info = QMessageBox.information(self, "Status", "Catalog saved at:\n"+moscatalogname) + + self.close() + +@menubar_plugin("MOSViz Table Generator (JWST/NIRSpec MSA)") +def nIRSpec_table_gen(session, data_collection): + ex = TableGen(session.application) + return + +if __name__ == "__main__": + app = QApplication(sys.argv) + ex = TableGen() + sys.exit(app.exec_()) diff --git a/mosviz/viewers/mos_viewer.py b/mosviz/viewers/mos_viewer.py index f39a6c3..290e66d 100644 --- a/mosviz/viewers/mos_viewer.py +++ b/mosviz/viewers/mos_viewer.py @@ -1,18 +1,23 @@ from __future__ import print_function, division, absolute_import import os +from collections import OrderedDict import numpy as np +from qtpy import compat from qtpy.QtCore import Signal -from qtpy.QtWidgets import QWidget, QLineEdit, QMessageBox +from qtpy.QtWidgets import QWidget, QLineEdit, QMessageBox, QPlainTextEdit, QPushButton from qtpy.uic import loadUi from glue.core import message as msg from glue.core import Subset from glue.core.exceptions import IncompatibleAttribute +from glue.core.data_exporters import astropy_table +from glue.core.component import Component, CategoricalComponent from glue.viewers.common.qt.data_viewer import DataViewer from glue.utils.matplotlib import defer_draw from glue.utils.decorators import avoid_circular +from glue.utils.qt import pick_item, get_text from specutils.core.generic import Spectrum1DRef @@ -51,6 +56,13 @@ def __init__(self, session, parent=None): self.load_ui() # Define some data containers + self.filepath = None + self.savepath = None + self.data_idx = None + self.comments = False + self.textChangedAt = None + self.mask = None + self.catalog = None self.current_row = None self._specviz_instance = None @@ -267,6 +279,7 @@ def add_subset(self, subset): self._layer_view.layer_combo.setCurrentIndex(index) return True + def _update_data(self, message): """ Update data message. @@ -339,11 +352,13 @@ def _unpack_selection(self, data): return data = data.data + self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta + self.comments = False col_names = data.components for att in col_names: cid = data.id[att] @@ -355,7 +370,10 @@ def _unpack_selection(self, data): if comp_labels.ndim > 1: comp_labels = comp_labels[0] - if str(att) in ['spectrum1d', 'spectrum2d', 'cutout']: + if str(att) in ["comments", "flag"]: + self.comments = True + elif str(att) in ['spectrum1d', 'spectrum2d', 'cutout']: + self.filepath = component._load_log.path path = '/'.join(component._load_log.path.split('/')[:-1]) self.catalog[str(att)] = [os.path.join(path, x) for x in comp_labels] @@ -370,6 +388,11 @@ def _unpack_selection(self, data): self.catalog[str(att)] = comp_data if len(self.catalog) > 0: + if not self.comments: + self.comments = self._load_comments(data.label) #Returns bool + else: + self._data_collection_index(data.label) + self._get_save_path() # Update gui elements self._update_navigation(select=0) @@ -455,13 +478,17 @@ def load_selection(self, row): spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) - image_data = loader_cutout(row[colname_cutout]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') - self._update_data_components(image_data, key='cutout') - - self.render_data(row, spec1d_data, spec2d_data, image_data) + + basename = os.path.basename(row[colname_cutout]) + if basename == "None": + self.render_data(row, spec1d_data, spec2d_data, None) + else: + image_data = loader_cutout(row[colname_cutout]) + self._update_data_components(image_data, key='cutout') + self.render_data(row, spec1d_data, spec2d_data, image_data) def _update_data_components(self, data, key): """ @@ -479,8 +506,8 @@ def _update_data_components(self, data, key): cur_data = self._loaded_data.get(key, None) if cur_data is None: - self.session.data_collection.append(data) self._loaded_data[key] = data + self.session.data_collection.append(data) else: cur_data.update_values_from_data(data) @@ -490,6 +517,8 @@ def render_data(self, row, spec1d_data=None, spec2d_data=None, Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ + self._check_unsaved_comments() + if spec1d_data is not None: spectrum1d_x = spec1d_data[spec1d_data.id['Wavelength']] @@ -545,6 +574,8 @@ def render_data(self, row, spec1d_data=None, spec2d_data=None, width=dx, height=dy) self.image_widget._redraw() + else: + self.image_widget.setVisible(False) # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the @@ -593,11 +624,50 @@ def render_data(self, row, spec1d_data=None, spec2d_data=None, # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: - line_edit = QLineEdit(str(row[col]), - self.central_widget.meta_form_widget) - line_edit.setReadOnly(True) + if col.lower() not in ["comments", "flag"]: + line_edit = QLineEdit(str(row[col]), + self.central_widget.meta_form_widget) + line_edit.setReadOnly(True) + + self.meta_form_layout.addRow(col, line_edit) + + # Set up comment and flag input/display boxes + if self.comments: + if self.savepath is not None: + if self.savepath == -1: + line_edit = QLineEdit(os.path.basename("Not Saving to File."), + self.central_widget.meta_form_widget) + line_edit.setReadOnly(True) + self.meta_form_layout.addRow("Save File", line_edit) + else: + line_edit = QLineEdit(os.path.basename(self.savepath), + self.central_widget.meta_form_widget) + line_edit.setReadOnly(True) + self.meta_form_layout.addRow("Save File", line_edit) + + self.input_flag = QLineEdit(self.get_flag(), + self.central_widget.meta_form_widget) + self.input_flag.textChanged.connect(self._text_changed) + self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") + self.meta_form_layout.addRow("Flag", self.input_flag) + + self.input_comments = QPlainTextEdit(self.get_comment(), + self.central_widget.meta_form_widget) + self.input_comments.textChanged.connect(self._text_changed) + self.input_comments.setStyleSheet("background-color: rgba(255, 255, 255);") + self.meta_form_layout.addRow("Comments", self.input_comments) + + self.input_save = QPushButton('Save', + self.central_widget.meta_form_widget) + self.input_save.clicked.connect(self.update_comments) + self.input_save.setDefault(True) + + self.input_refresh = QPushButton('Reload', + self.central_widget.meta_form_widget) + self.input_refresh.clicked.connect(self.refresh_comments) + + self.meta_form_layout.addRow(self.input_save, self.input_refresh) - self.meta_form_layout.addRow(col, line_edit) @defer_draw def set_locked_axes(self, x=None, y=None): @@ -616,6 +686,288 @@ def set_locked_axes(self, x=None, y=None): self.spectrum2d_widget._redraw() self.image_widget._redraw() + def layer_view(self): + return self._layer_view + + def _text_changed(self): + if self.textChangedAt is None: + i = self.toolbar.source_select.currentIndex() + self.textChangedAt = self._index_hash(i) + + def _check_unsaved_comments(self): + if self.textChangedAt is None: + return #Nothing to be changed + i = self.toolbar.source_select.currentIndex() + i = self._index_hash(i) + if self.textChangedAt == i: + self.textChangedAt = None + return #This is a refresh + info = "Comments or flags changed but were not saved. Would you like to save them?" + reply = QMessageBox.question(self,'', info, QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.update_comments(True) + self.textChangedAt = None + + def _data_collection_index(self, label): + idx = -1 + for i, l in enumerate(self.session.data_collection): + if l.label == label: + idx = i + break + if idx == -1: + return -1 + self.data_idx = idx + return idx + + def _index_hash(self, i): + """Local selection index -> Table index""" + if self.mask is not None: + size = self.mask.size + temp = np.arange(size) + return temp[self.mask][i] + else: + return i + + def _id_to_index_hash(self, ID, l): + """Object Name -> Table index""" + for i, name in enumerate(l): + if name == ID: + return i + return None + + def get_comment(self): + idx = self.data_idx + i = self.toolbar.source_select.currentIndex() + i = self._index_hash(i) + comp = self.session.data_collection[idx].get_component("comments") + return comp._categorical_data[i] + + def get_flag(self): + idx = self.data_idx + i = self.toolbar.source_select.currentIndex() + i = self._index_hash(i) + comp = self.session.data_collection[idx].get_component("flag") + return comp._categorical_data[i] + + def send_NumericalDataChangedMessage(self): + idx = self.data_idx + data = self.session.data_collection[idx] + data.hub.broadcast(msg.NumericalDataChangedMessage(data,"comments")) + + def refresh_comments(self): + self.input_flag.setText(self.get_flag()) + self.input_comments.setPlainText(self.get_comment()) + self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") + self.textChangedAt = None + + def _get_save_path(self): + """ + Try to get save path from other MOSVizViewer instances + """ + for v in self.session.application.viewers[0]: + if isinstance(v, MOSVizViewer): + if v.savepath is not None: + if v.data_idx == self.data_idx: + self.savepath = v.savepath + break + + def _setup_save_path(self): + """ + Prompt the user for a file to save comments and flags into. + """ + fail = True + success = False + info = "Where would you like to save comments and flags?" + option = pick_item([0, 1], + [os.path.basename(self.filepath), "New MOSViz Table file"], + label=info, title="Comment Setup") + if option == 0: + self.savepath = self.filepath + elif option == 1: + dirname = os.path.dirname(self.filepath) + path = compat.getsavefilename(caption="New MOSViz Table File", + basedir=dirname, filters="*.txt")[0] + if path == "": + return fail + self.savepath = path + else: + return fail + + for v in self.session.application.viewers[0]: + if isinstance(v, MOSVizViewer): + if v.data_idx == self.data_idx: + v.savepath = self.savepath + self._layer_view.refresh() + return success + + def update_comments(self, pastSelection = False): + """ + Process comment and flag changes and save to file. + + Parameters + ---------- + pastSelection : bool + True when updating past selections. Used when + user forgets to save. + """ + if self.input_flag.text() == "": + self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") + return + + i = None + try: + i = int(self.input_flag.text()) + except ValueError: + self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") + info = QMessageBox.information(self, "Status:", "Flag must be an int!") + return + self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") + + idx = self.data_idx + if pastSelection: + i = self.textChangedAt + self.textChangedAt = None + else: + i = self.toolbar.source_select.currentIndex() + i = self._index_hash(i) + data = self.session.data_collection[idx] + + comp = data.get_component("comments") + comp._categorical_data.flags.writeable = True + comp._categorical_data[i] = self.input_comments.toPlainText() + + comp = data.get_component("flag") + comp._categorical_data.flags.writeable = True + comp._categorical_data[i] = self.input_flag.text() + + self.send_NumericalDataChangedMessage() + self.write_comments() + + self.textChangedAt = None + + def _load_comments(self, label): + """ + Populate the comments and flag columns. + Attempt to load comments from file. + + Parameters + ---------- + label : str + The label of the data in + session.data_collection. + """ + + #Make sure its the right data + #(beacuse subset data is masked) + idx = self._data_collection_index(label) + if idx == -1: + return False + data = self.session.data_collection[idx] + + #Fill in default comments: + length = data.shape[0] + new_comments = np.array(["" for i in range(length)], dtype=object) + new_flags = np.array(["0" for i in range(length)], dtype=object) + + #Fill in any saved comments: + meta = data.meta + obj_names = data.get_component("id")._categorical_data + + if "MOSViz_comments" in meta.keys(): + try: + comments = meta["MOSViz_comments"] + for key in comments.keys(): + index = self._id_to_index_hash(key, obj_names) + if index is not None: + line = comments[key] + new_comments[index] = line + except Exception as e: + print("MOSViz Comment Load Failed: ", e) + + if "MOSViz_flags" in meta.keys(): + try: + flags = meta["MOSViz_flags"] + for key in flags.keys(): + index = self._id_to_index_hash(key, obj_names) + if index is not None: + line = flags[key] + new_flags[index] = line + except Exception as e: + print("MOSViz Flag Load Failed: ", e) + + #Send to DC + data.add_component(CategoricalComponent(new_flags, "flag"),"flag") + data.add_component(CategoricalComponent(new_comments, "comments"),"comments") + return True + + def write_comments(self): + """ + Setup save file. Write comments and flags to file + """ + + if self.savepath is None: + fail = self._setup_save_path() + if fail:return + if self.savepath == -1: + return #Do not save to file option + + idx = self.data_idx + data = self.session.data_collection[idx] + save_comments = data.get_component("comments")._categorical_data + save_flag = data.get_component("flag")._categorical_data + obj_names = data.get_component("id")._categorical_data + + fn = self.savepath + folder = os.path.dirname(fn) + + t = astropy_table.data_to_astropy_table(data) + + #Check if load and save dir paths match + temp = os.path.dirname(self.filepath) + if not os.path.samefile(folder,temp): + t['spectrum1d'].flags.writeable = True + t['spectrum2d'].flags.writeable = True + t['cutout'].flags.writeable = True + for i in range(len(t)): + t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) + t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) + t['cutout'][i] = os.path.abspath(t['cutout'][i]) + try: + t.remove_column("comments") + t.remove_column("flag") + + keys = t.meta.keys() + + if "MOSViz_comments" in keys: + t.meta.pop("MOSViz_comments") + + if "MOSViz_flags" in keys: + t.meta.pop("MOSViz_flags") + + comments = OrderedDict() + flags = OrderedDict() + + for i, line in enumerate(save_comments): + if line != "": + line = line.replace("\n", " ") + key = str(obj_names[i]) + comments[key] = line + + for i, line in enumerate(save_flag): + if line != "0" and line != "": + line = com.replace("\n", " ") + key = str(obj_names[i]) + flags[key] = line + + if len(comments) > 0: + t.meta["MOSViz_comments"] = comments + if len(flags) > 0: + t.meta["MOSViz_flags"] = flags + + t.write(fn, format="ascii.ecsv", overwrite=True) + except Exception as e: + print("Comment write failed:",e) + def closeEvent(self, event): """ Clean up the extraneous data components created when opening the @@ -626,5 +978,3 @@ def closeEvent(self, event): for data in self._loaded_data.values(): self.session.data_collection.remove(data) - def layer_view(self): - return self._layer_view