From 1d629997c2c94bbee872822c38883790d914909c Mon Sep 17 00:00:00 2001 From: Clare Shanahan Date: Wed, 15 Nov 2023 11:45:46 -0500 Subject: [PATCH] applying changes --- jdaviz/app.py | 20 +- .../plugins/subset_plugin/subset_plugin.py | 156 +++++++++++----- .../subset_plugin/tests/test_subset_plugin.py | 152 ++++++++++++++- .../default/plugins/subset_plugin/utils.py | 175 ++++++++++++++++++ 4 files changed, 446 insertions(+), 57 deletions(-) create mode 100644 jdaviz/configs/default/plugins/subset_plugin/utils.py diff --git a/jdaviz/app.py b/jdaviz/app.py index 54d51db95e..cfc6e1bd42 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -1014,6 +1014,17 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type return regions + def _get_wcs_from_subset(self, subset_state): + """ Usually WCS is subset.parent.coords, except special cuebviz case.""" + + if self.config == 'cubeviz': + parent_data = subset_state.attributes[0].parent + wcs = parent_data.meta.get("_orig_spatial_wcs", None) + else: + wcs = subset_state.xatt.parent.coords + + return wcs + def get_subsets(self, subset_name=None, spectral_only=False, spatial_only=False, object_only=False, simplify_spectral=True, use_display_units=False, @@ -1200,13 +1211,12 @@ def _get_roi_subset_definition(self, subset_state, to_sky=False): # pixel region roi_as_region = roi_subset_state_to_region(subset_state) + # pixel region + roi_as_region = roi_subset_state_to_region(subset_state) + wcs = None if to_sky: - if self.config == 'cubeviz': - parent_data = subset_state.attributes[0].parent - wcs = parent_data.meta.get("_orig_spatial_wcs", None) - else: - wcs = subset_state.xatt.parent.coords # imviz, try getting WCS from subset data + wcs = self._get_wcs_from_subset(subset_state) # if no spatial wcs on subset, we have to skip computing sky region for this subset # but want to do so without raising an error (since many subsets could be requested) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index af20ddf8e5..7af1d3ed6c 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -14,12 +14,15 @@ from glue_jupyter.common.toolbar_vuetify import read_icon from traitlets import Any, List, Unicode, Bool, observe -from jdaviz.core.events import SnackbarMessage, GlobalDisplayUnitChanged +from jdaviz.core.events import SnackbarMessage, GlobalDisplayUnitChanged, LinkUpdatedMessage from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin, SubsetSelect from jdaviz.core.tools import ICON_DIR from jdaviz.utils import MultiMaskSubsetState +from jdaviz.configs.default.plugins.subset_plugin import utils + + __all__ = ['SubsetPlugin'] SUBSET_MODES = { @@ -76,6 +79,8 @@ def __init__(self, *args, **kwargs): handler=self._on_subset_update) self.session.hub.subscribe(self, GlobalDisplayUnitChanged, handler=self._on_display_unit_changed) + self.session.hub.subscribe(self, LinkUpdatedMessage, + handler=self._on_link_update) self.subset_select = SubsetSelect(self, 'subset_items', @@ -85,6 +90,23 @@ def __init__(self, *args, **kwargs): self.subset_states = [] self.spectral_display_unit = None + if self.multiselect: + self.display_sky_coordinates = False + else: + self.display_sky_coordinates = (self.app._link_type == 'wcs' if hasattr(self.app, '_link_type') else False) + + def _on_link_update(self, *args): + """When linking is changed pixels<>wcs, change display units of the + subset plugin from pixel (for pixel linking) to sky (for wcs linking). + If there is an active selection in the subset plugin, push this change + to the UI upon link change by calling _get_subset_definition, which + will re-determine how to display subset information.""" + + self.display_sky_coordinates = (self.app._link_type == 'wcs') + + if self.subset_selected != self.subset_select.default_text: + self._get_subset_definition(*args) + def _sync_selected_from_state(self, *args): if not hasattr(self, 'subset_select') or self.multiselect: # during initial init, this can trigger before the component is initialized @@ -145,7 +167,9 @@ def _unpack_get_subsets_for_ui(self): subset_information = self.app.get_subsets(self.subset_selected, simplify_spectral=False, - use_display_units=True) + use_display_units=True, + include_sky_region=True) + _around_decimals = 6 # Avoid 30 degrees from coming back as 29.999999999999996 if not subset_information: return @@ -159,60 +183,69 @@ def _unpack_get_subsets_for_ui(self): self.is_centerable = False for spec in subset_information: + subset_definition = [] subset_type = '' subset_state = spec["subset_state"] glue_state = spec["glue_state"] + if isinstance(subset_state, RoiSubsetState): subset_definition.append({ "name": "Parent", "att": "parent", "value": subset_state.xatt.parent.label, "orig": subset_state.xatt.parent.label}) - if isinstance(subset_state.roi, CircularROI): - x, y = subset_state.roi.center() - r = subset_state.roi.radius - subset_definition += [ - {"name": "X Center", "att": "xc", "value": x, "orig": x}, - {"name": "Y Center", "att": "yc", "value": y, "orig": y}, - {"name": "Radius", "att": "radius", "value": r, "orig": r}] - - elif isinstance(subset_state.roi, RectangularROI): - for att in ("Xmin", "Xmax", "Ymin", "Ymax"): - real_att = att.lower() - val = getattr(subset_state.roi, real_att) - subset_definition.append( - {"name": att, "att": real_att, "value": val, "orig": val}) - theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) - subset_definition.append( - {"name": "Angle", "att": "theta", "value": theta, "orig": theta}) - - elif isinstance(subset_state.roi, EllipticalROI): - xc, yc = subset_state.roi.center() - rx = subset_state.roi.radius_x - ry = subset_state.roi.radius_y - theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) - subset_definition += [ - {"name": "X Center", "att": "xc", "value": xc, "orig": xc}, - {"name": "Y Center", "att": "yc", "value": yc, "orig": yc}, - {"name": "X Radius", "att": "radius_x", "value": rx, "orig": rx}, - {"name": "Y Radius", "att": "radius_y", "value": ry, "orig": ry}, - {"name": "Angle", "att": "theta", "value": theta, "orig": theta}] - - elif isinstance(subset_state.roi, CircularAnnulusROI): - x, y = subset_state.roi.center() - inner_r = subset_state.roi.inner_radius - outer_r = subset_state.roi.outer_radius - subset_definition += [{"name": "X Center", "att": "xc", "value": x, "orig": x}, - {"name": "Y Center", "att": "yc", "value": y, "orig": y}, - {"name": "Inner radius", "att": "inner_radius", - "value": inner_r, "orig": inner_r}, - {"name": "Outer radius", "att": "outer_radius", - "value": outer_r, "orig": outer_r}] + sky_region = spec['sky_region'] + if self.display_sky_coordinates and (sky_region is not None): + subset_definition += utils._sky_region_to_subset_def(sky_region) + + else: + if isinstance(subset_state.roi, CircularROI): + x, y = subset_state.roi.center() + r = subset_state.roi.radius + subset_definition += [ + {"name": "X Center (pixels)", "att": "xc", "value": x, "orig": x}, + {"name": "Y Center (pixels)", "att": "yc", "value": y, "orig": y}, + {"name": "Radius (pixels)", "att": "radius", "value": r, "orig": r}] + + if isinstance(subset_state.roi, RectangularROI): + for att in ("Xmin", "Xmax", "Ymin", "Ymax"): + real_att = att.lower() + val = getattr(subset_state.roi, real_att) + subset_definition.append( + {"name": att + " (pixels)", "att": real_att, "value": val, "orig": val}) + + theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) + subset_definition.append({"name": "Angle", "att": "theta", "value": theta, "orig": theta}) + + if isinstance(subset_state.roi, EllipticalROI): + xc, yc = subset_state.roi.center() + rx = subset_state.roi.radius_x + ry = subset_state.roi.radius_y + theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) + + subset_definition += [ + {"name": "X Center (pixels)", "att": "xc", "value": xc, "orig": xc}, + {"name": "Y Center (pixels)", "att": "yc", "value": yc, "orig": yc}, + {"name": "X Radius (pixels)", "att": "radius_x", "value": rx, "orig": rx}, + {"name": "Y Radius (pixels)", "att": "radius_y", "value": ry, "orig": ry}, + {"name": "Angle", "att": "theta", "value": theta, "orig": theta}] + + if isinstance(subset_state.roi, CircularAnnulusROI): + xc, yc = subset_state.roi.center() + inner_r = subset_state.roi.inner_radius + outer_r = subset_state.roi.outer_radius + display_labels = '(pixels)' + subset_definition += [{"name": "X Center (pixels)", "att": "xc", "value": xc, "orig": xc}, + {"name": "Y Center (pixels)", "att": "yc", "value": yc, "orig": yc}, + {"name": "Inner Radius (pixels)", "att": "inner_radius", + "value": inner_r, "orig": inner_r}, + {"name": "Outer Radius (pixels)", "att": "outer_radius", + "value": outer_r, "orig": outer_r}] subset_type = subset_state.roi.__class__.__name__ - elif isinstance(subset_state, RangeSubsetState): + if isinstance(subset_state, RangeSubsetState): region = spec['region'] if isinstance(region, Time): lo = region.min() @@ -230,7 +263,7 @@ def _unpack_get_subsets_for_ui(self): "orig": hi.value, "unit": str(hi.unit)}] subset_type = "Range" - elif isinstance(subset_state, MultiMaskSubsetState): + if isinstance(subset_state, MultiMaskSubsetState): total_masked = subset_state.total_masked_first_data() subset_definition = [{"name": "Masked values", "att": "masked", "value": total_masked, @@ -303,12 +336,11 @@ def _on_display_unit_changed(self, msg): self._get_subset_definition(self.subset_selected) def vue_update_subset(self, *args): + if self.multiselect: self.hub.broadcast(SnackbarMessage("Cannot update subset " "when multiselect is active", color='warning', sender=self)) - return - status, reason = self._check_input() if not status: self.hub.broadcast(SnackbarMessage(reason, color='error', sender=self)) @@ -318,12 +350,34 @@ def vue_update_subset(self, *args): if len(self.subset_states) <= index: return sub_states = self.subset_states[index] + + # we need to push updates to subset in pixels. to do this when wcs + # linked, convert the new input subset parameters from sky to pix + wcs = None + + if self.display_sky_coordinates: + wcs = self.app._get_wcs_from_subset(sub_states) + + if wcs is not None: + # convert newly entered sky coords to pixel + print(f'self.subset_types[index] is {self.subset_types[index]}!!!') + skyregion = utils._subset_def_to_region(self.subset_types[index], sub) + new = utils._get_pixregion_params_in_dict(skyregion.to_pixel(wcs)) + # convert previous entered sky coords to pixel + new_orig_skyregion = utils._subset_def_to_region(self.subset_types[index], sub, val='orig') + new_orig = utils._get_pixregion_params_in_dict(new_orig_skyregion.to_pixel(wcs)) + for d_att in sub: if d_att["att"] == 'parent': # Read-only continue - - if d_att["att"] == 'theta': # Humans use degrees but glue uses radians - d_val = np.radians(d_att["value"]) + if self.display_sky_coordinates and (wcs is not None): + d_att["value"] = new[d_att["att"]] + d_att["orig"] = new_orig[d_att["att"]] + + if (d_att["att"] == 'theta') and (self.display_sky_coordinates is False): + # Humans use degrees but glue uses radians + # We've already enforced this in wcs linking in _get_pixregion_params_in_dict + d_val = (d_att["value"]*u.deg).to(u.rad).value else: d_val = float(d_att["value"]) @@ -342,6 +396,7 @@ def vue_update_subset(self, *args): setattr(sub_states, d_att["att"], d_val) else: setattr(sub_states.roi, d_att["att"], d_val) + self._push_update_to_ui() def _push_update_to_ui(self, subset_name=None): @@ -422,7 +477,7 @@ def vue_recenter_subset(self, *args): def _do_recentering(subset, subset_state): try: - reg = _get_region_from_spatial_subset(self, subset_state) # noqa + reg = _get_region_from_spatial_subset(self, subset_state) # noqa aperture = regions2aperture(reg) data = self.dataset.selected_dc_item comp = data.get_component(data.main_components[0]) @@ -439,7 +494,6 @@ def _do_recentering(subset, subset_state): subset_state.xatt.parent.coords, phot_aperstats.xcentroid, phot_aperstats.ycentroid) - else: x = phot_aperstats.xcentroid y = phot_aperstats.ycentroid diff --git a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py index 3d0b4e094a..040c8e10e9 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py @@ -1,7 +1,18 @@ import warnings import pytest -from glue.core.roi import XRangeROI +from astropy.coordinates import SkyCoord +from astropy.nddata import NDData +import astropy.units as u +from glue.core.roi import EllipticalROI, CircularROI, CircularAnnulusROI, RectangularROI, XRangeROI +from glue_astronomy.translators.regions import roi_subset_state_to_region +from jdaviz import Imviz +from jdaviz.configs.imviz.helper import link_image_data +from jdaviz.core.region_translators import regions2roi +import numpy as np +from numpy.testing import assert_allclose + +from jdaviz.configs.default.plugins.subset_plugin import utils @pytest.mark.filterwarnings('ignore') @@ -25,3 +36,142 @@ def test_subset_definition_with_composite_subset(cubeviz_helper, spectrum1d_cube warnings.simplefilter('ignore') cubeviz_helper.load_data(spectrum1d_cube) cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') + + +circle_subset_info = {'xc': {'pixel_name': 'X Center (pixels)', 'wcs_name': + 'RA Center (degrees)', 'initial_value': 5, + 'final_value': 6}, + 'yc': {'pixel_name': 'Y Center (pixels)', 'wcs_name': + 'Dec Center (degrees)', 'initial_value': 5, + 'final_value': 6}, + 'radius': {'pixel_name': 'Radius (pixels)', 'wcs_name': + 'Radius (degrees)', 'initial_value': 2, + 'final_value': 3}} +elliptical_subset_info = {'xc': {'pixel_name': 'X Center (pixels)', 'wcs_name': + 'RA Center (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'yc': {'pixel_name': 'Y Center (pixels)', 'wcs_name': + 'Dec Center (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'radius_x': {'pixel_name': 'X Radius (pixels)', 'wcs_name': + 'RA Radius (degrees)', 'initial_value': 2., + 'final_value': 3.}, + 'radius_y': {'pixel_name': 'Y Radius (pixels)', 'wcs_name': + 'Dec Radius (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'theta': {'pixel_name': 'Angle', 'wcs_name': + 'Angle', 'initial_value': 0., + 'final_value': 45.}} +circ_annulus_subset_info = {'xc': {'pixel_name': 'X Center (pixels)', 'wcs_name': + 'RA Center (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'yc': {'pixel_name': 'Y Center (pixels)', 'wcs_name': + 'Dec Center (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'inner_radius': {'pixel_name': 'Inner Radius (pixels)', 'wcs_name': + 'Inner Radius (degrees)', 'initial_value': 2., + 'final_value': 3.}, + 'outer_radius': {'pixel_name': 'Outer Radius (pixels)', 'wcs_name': + 'Outer Radius (degrees)', 'initial_value': 5., + 'final_value': 6.}} +rectangular_subset_info = {'xmin': {'pixel_name': 'Xmin (pixels)', 'wcs_name': + 'RA min (degrees)', 'initial_value': 5., + 'final_value': 6.}, + 'xmax': {'pixel_name': 'Xmax (pixels)', 'wcs_name': + 'RA max (degrees)', 'initial_value': 6., + 'final_value': 7.}, + 'ymin': {'pixel_name': 'Ymin (pixels)', 'wcs_name': + 'Dec min (degrees)', 'initial_value': 2., + 'final_value': 3.}, + 'ymax': {'pixel_name': 'Ymax (pixels)', 'wcs_name': + 'Dec max (degrees)', 'initial_value': 5., + 'final_value': 6.}} + + +@pytest.mark.parametrize("roi_class, subset_info", [(CircularROI, circle_subset_info), + (EllipticalROI, elliptical_subset_info), + (CircularAnnulusROI, circ_annulus_subset_info), + (RectangularROI, rectangular_subset_info)]) +def test_circle_recenter_linking(roi_class, subset_info, imviz_helper, image_2d_wcs): + + arr = np.ones((10, 10)) + ndd = NDData(arr, wcs=image_2d_wcs) + imviz_helper.load_data(ndd, data_label='dataset1') + imviz_helper.load_data(ndd, data_label='dataset2') + # force link to be pixel initially. + link_image_data(imviz_helper.app, link_type='pixels') + + # apply subset + roi_params = {key: subset_info[key]['initial_value'] for key in subset_info} + imviz_helper.app.get_viewer('imviz-0').apply_roi(roi_class(**roi_params)) + + # get plugin and check that attribute tracking link type is set properly + plugin = imviz_helper.plugins['Subset Tools']._obj + assert plugin.display_sky_coordinates is False + + # get initial subset definitions from ROI applied + subset_defs = plugin.subset_definitions + + # check that the subset definitions, which control what is displayed in the UI, are correct + for i, attr in enumerate(subset_info): + assert subset_defs[0][i+1]['name'] == subset_info[attr]['pixel_name'] + assert subset_defs[0][i+1]['value'] == subset_info[attr]['initial_value'] + + # get original subset location as a sky region for use later + original_subs = imviz_helper.app.get_subsets(include_sky_region=True) + original_sky_region = original_subs['Subset 1'][0]['sky_region'] + + # move subset (subset state is what is modified in UI) + for attr in subset_info: + plugin._set_value_in_subset_definition(0, subset_info[attr]['pixel_name'], + attr, subset_info[attr]['final_value']) + + # update subset to apply these changes + plugin.vue_update_subset() + subset_defs = plugin.subset_definitions + + # and check that it is changed after vue_update_subset runs + for i, attr in enumerate(subset_info): + assert subset_defs[0][i+1]['name'] == subset_info[attr]['pixel_name'] + assert subset_defs[0][i+1]['value'] == subset_info[attr]['final_value'] + + # get updated subset location as a sky region, we need this later + updated_sky_region = imviz_helper.app.get_subsets(include_sky_region=True) + updated_sky_region = updated_sky_region['Subset 1'][0]['sky_region'] + + # remove subsets and change link type to wcs + dc = imviz_helper.app.data_collection + dc.remove_subset_group(dc.subset_groups[0]) + link_image_data(imviz_helper.app, link_type='wcs') + assert plugin.display_sky_coordinates is True # linking change should trigger this to change to True + + # apply original subset. transform sky coord of original subset to new pixels + # using wcs of orientation layer (won't be the same as original pixels when pix linked) + img_wcs = imviz_helper.app.data_collection['Default orientation'].data.coords + new_pix_region = original_sky_region.to_pixel(img_wcs) + new_roi = regions2roi(new_pix_region) + imviz_helper.app.get_viewer('imviz-0').apply_roi(new_roi) + + # get subset definitions again, which should now be in sky coordinates + subset_defs = plugin.subset_definitions + + # check that the subset definitions, which control what is displayed in the UI, + # are correct and match our original sky region generated when we were first pixel linked + true_values_orig = utils._sky_region_to_subset_def(original_sky_region) + + for i, attr in enumerate(subset_info): + assert subset_defs[0][i+1]['name'] == subset_info[attr]['wcs_name'] + assert_allclose(subset_defs[0][i+1]['value'], true_values_orig[i]['value']) + + true_values_final = utils._sky_region_to_subset_def(updated_sky_region) + + for i, attr in enumerate(subset_info): + plugin._set_value_in_subset_definition(0, subset_info[attr]['wcs_name'], + attr, true_values_final[i]['value']) + # update subset + plugin.vue_update_subset() + + subset_defs = plugin.subset_definitions + for i, attr in enumerate(subset_info): + assert subset_defs[0][i+1]['name'] == subset_info[attr]['wcs_name'] + assert_allclose(subset_defs[0][i+1]['value'], true_values_final[i]['value']) diff --git a/jdaviz/configs/default/plugins/subset_plugin/utils.py b/jdaviz/configs/default/plugins/subset_plugin/utils.py new file mode 100644 index 0000000000..5af42887f3 --- /dev/null +++ b/jdaviz/configs/default/plugins/subset_plugin/utils.py @@ -0,0 +1,175 @@ +# utility functions for subset plugin +# these don't belong in glue.core.region_translators, as they deal with conversions that +# only happen within this plugin (e.g between region/'subset defintion') + +from astropy.coordinates import SkyCoord +import astropy.units as u +from jdaviz.core.region_translators import regions2roi +import numpy as np +from regions import CircleAnnulusSkyRegion, CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion + + +def _subset_def_to_region(subset_type, sub, val='value', name='name', region_type='sky'): + """Subset definitions are carried through the plugin in a list of dictionaries, + one entry each for each ROI attribute of that subset along with its value, + its previous value, and the name to display for that attribute in the UI. + (see _unpack_get_subsets_for_ui for how these are constructed). + + This function takes one of those dictionary lists describing a subset, + and converts it to a Regions object. + + Parameters + ---------- + subset_type : str + Name of ROI class (CircularROI/TrueCircularROI, EllipticalROI, + CircularAnnulusROI, or RectangularROI) + sub : list of dict + List of dictionaries defining the subset, with one dict for each + subset attribute (see _unpack_get_subsets_for_ui for how these + are defined) + val : str + Key to get subset attribute (e.g radius, xc, yc) value. Will + normally be 'value' or 'orig' within plugin to get the new and + previous values of the attribute, respectivley. + name : str + Key to get subset attribute name. Will normally be 'name'. + region_type : str + 'sky' to return a SkyRegion, 'pixel' to return a PixelRegion + + + Returns + ------- + reg : `SkyRegion` or `PixelRegion` + A SkyRegion or PixelRegion object (depending on what + `region_type` is) of the subset that the dictionary `sub` + represents.""" + + if "CircularROI" in subset_type: # TrueCircular or Circular + if region_type == 'sky': + reg = CircleSkyRegion(center=SkyCoord(*[x[val] for x in sub if 'Center' in x[name]]*u.deg), + radius=[x[val] for x in sub if 'Radius' in x[name]][0]*u.deg) + if region_type == 'pixel': + reg = CirclePixelRegion(center=PixCoord(*[x[val] for x in sub if 'Center' in x[name]]), + radius=[x[val] for x in sub if 'Radius' in x[name]][0]) + elif subset_type == "EllipticalROI": + rads = [x[val] for x in sub if 'Radius' in x[name]] + if region_type == 'sky': + reg = EllipseSkyRegion(SkyCoord(*[x[val] for x in sub if 'Center' in x[name]]*u.deg), + width=2.*rads[0]*u.deg, + height=2.*rads[1]*u.deg, + angle=[x[val] for x in sub if 'Angle' in x[name]][0]*u.deg) + if region_type == 'pixel': + EllipsePixelRegion(center=PixCoord(*[x[val] for x in sub if 'Center' in x[name]]), + width=2.*rads[0], + height=2.*rads[1], + angle=[x[val] for x in sub if 'Angle' in x[name]][0]) + + elif subset_type == "CircularAnnulusROI": + rads = [x[val] for x in sub if 'Radius' in x['name']] + reg = CircleAnnulusSkyRegion(SkyCoord(*[x[val] for x in sub if 'Center' in x[name]]*u.deg), + inner_radius=rads[0]*u.deg, + outer_radius=rads[1]*u.deg) + + elif subset_type == "RectangularROI": + xmin, ymin = [x[val] for x in sub if 'min' in x[name]] + xmax, ymax = [x[val] for x in sub if 'max' in x[name]] + angle = [x[val] for x in sub if 'Angle' in x[name]][0] + width = xmax - xmin + height = ymax - ymin + center = ((xmin+xmax) / 2, (ymin+ymax) / 2) + reg = RectangleSkyRegion(center=SkyCoord(*center*u.deg), + width=width*u.deg, + height=height*u.deg, + angle=angle*u.deg) + return reg + + +def _sky_region_to_subset_def(sky_region, _around_decimals=6): + """Generates a 'subset_definition' list of dictionaries from a + `regions.SkyRegion`object. + + Parameters + ---------- + sky_region : `regions.SkyRegion` + Name of ROI class (CircularROI/TrueCircularROI, EllipticalROI, + CircularAnnulusROI, or RectangularROI) + _around_decimals : str + Rounding for 'theta', if present. + + Returns + ------- + deff : list of dict + List of dictionaries, each sub-dictionary describing a + subset attribute.""" + + if isinstance(sky_region, CircleSkyRegion): + x = sky_region.center.ra.deg + y = sky_region.center.dec.deg + r = sky_region.radius.deg + deff = [{"name": 'RA Center (degrees)', "att": "xc", "value": x, "orig": x}, + {"name": 'Dec Center (degrees)', "att": "yc", "value": y, "orig": y}, + {"name": 'Radius (degrees)', "att": "radius", "value": r, "orig": r}] + + if isinstance(sky_region, EllipseSkyRegion): + xc = sky_region.center.ra.deg + yc = sky_region.center.dec.deg + rx = sky_region.width.to(u.deg).value / 2. + ry = sky_region.height.to(u.deg).value / 2. + ang = (sky_region.angle).to(u.deg).value + theta = np.around(ang, decimals=_around_decimals) + deff = [{"name": "RA Center (degrees)", "att": "xc", "value": xc, "orig": xc}, + {"name": "Dec Center (degrees)", "att": "yc", "value": yc, "orig": yc}, + {"name": "RA Radius (degrees)", "att": "radius_x", "value": rx, "orig": rx}, + {"name": "Dec Radius (degrees)", "att": "radius_y", "value": ry, "orig": ry}, + {"name": "Angle", "att": "theta", "value": theta, "orig": theta}] + + if isinstance(sky_region, CircleAnnulusSkyRegion): + xc = sky_region.center.ra.deg + yc = sky_region.center.dec.deg + inner_r = sky_region.inner_radius.to(u.deg).value + outer_r = sky_region.outer_radius.to(u.deg).value + deff = [{"name": "RA Center (degrees)", "att": "xc", "value": xc, "orig": xc}, + {"name": "Dec Center (degrees)", "att": "yc", "value": yc, "orig": yc}, + {"name": "Inner Radius (degrees)", "att": "inner_radius", "value": inner_r, "orig": inner_r}, + {"name": "Outer Radius (degrees)", "att": "outer_radius", "value": outer_r, "orig": outer_r}] + + if isinstance(sky_region, RectangleSkyRegion): + deff = [] + mins_maxs = {'xmin': sky_region.center.ra.deg - ((sky_region.width).to(u.deg).value / 2.), + 'xmax': sky_region.center.ra.deg + ((sky_region.width).to(u.deg).value / 2.), + 'ymin': sky_region.center.dec.deg - ((sky_region.height).to(u.deg).value / 2.), + 'ymax': sky_region.center.dec.deg + ((sky_region.height).to(u.deg).value / 2.)} + for att in ("Xmin", "Xmax", "Ymin", "Ymax"): + val = mins_maxs[att.lower()] + name = att.replace('X', 'RA ').replace('Y', 'Dec ') + deff.append({"name": f"{name} (degrees)", "att": att.lower(), + "value": val, "orig": val}) + theta = (sky_region.angle).to(u.deg).value + deff.append({"name": "Angle", "att": "theta", "value": theta, "orig": theta}) + + return deff + + +def _get_pixregion_params_in_dict(region): + """Given a Region, returns a dictionary with the attribute names + of the corresponding ROI type (i.e CirclePixelRegion and CircularROI). + + This makes use of `jdaviz.core.region_translators.regions2roi`, but instead + of returning an ROI object it returns a dictionary of parameters that + would be used to create that same ROI object. This involves changing some + keys and deleting some, which is why that `regions2roi` can't be + called directly. + """ + + roi = regions2roi(region) + region_dict = roi.__dict__.copy() + + if 'center' in region_dict.keys(): + region_dict['xc'] = region_dict['center'].x + region_dict['yc'] = region_dict['center'].y + del region_dict['center'] + + region_dict.pop('meta', None) + region_dict.pop('visual', None) + + return region_dict