diff --git a/CHANGES.rst b/CHANGES.rst index e2144f3fd8..7d46055b77 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ New Features - Snackbar queue priority and history access. [#1352] +- Subset Tools plugin now shows information for composite subsets. [#1378] + - Plot options are simplified and include an advanced mode to act on multiple viewers/layers simultaneously. [#1343] diff --git a/docs/specviz/plugins.rst b/docs/specviz/plugins.rst index 10c7c96e80..d54fbace9f 100644 --- a/docs/specviz/plugins.rst +++ b/docs/specviz/plugins.rst @@ -61,8 +61,9 @@ applied by the selector tool. Note that these are synched with the subset tools in the app-level toolbar. If an existing subset is selected, the parameters of the subset will also be -shown. Note that parameters compound regions (e.g., a subset with three disjoint -regions) are not currently displayed. +shown. Note that while parameters for compound regions (e.g., a subset with +three disjoint regions) are displayed, the logical operations joining them +(``OR``, ``AND``, etc.) are not. .. _gaussian-smooth: diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 902002df9e..5644589d8f 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -1,10 +1,9 @@ from glue.core.message import EditSubsetMessage, SubsetUpdateMessage from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode, ReplaceMode, XorMode) -from glue.core.subset import (RoiSubsetState, RangeSubsetState, - OrState, AndState, XorState, InvertState) +from glue.core.subset import RoiSubsetState, RangeSubsetState, CompositeSubsetState from glue_jupyter.widgets.subset_mode_vuetify import SelectionModeMenu -from traitlets import List, Unicode, Bool, Dict, observe +from traitlets import List, Unicode, Bool, observe from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import TemplateMixin, SubsetSelect @@ -27,9 +26,9 @@ class SubsetPlugin(TemplateMixin): subset_items = List([]).tag(sync=True) subset_selected = Unicode("Create new").tag(sync=True) mode_selected = Unicode('add').tag(sync=True) - show_region_info = Bool(False).tag(sync=True) - subset_classname = Unicode('').tag(sync=True) - subset_definition = Dict({}).tag(sync=True) + show_region_info = Bool(True).tag(sync=True) + subset_types = List([]).tag(sync=True) + subset_definitions = List([]).tag(sync=True) has_subset_details = Bool(False).tag(sync=True) def __init__(self, *args, **kwargs): @@ -67,7 +66,7 @@ def _sync_selected_from_state(self, *args): def _on_subset_update(self, *args): self._sync_selected_from_state(*args) - self._get_region_definition(*args) + self._get_subset_definition(*args) subset_to_update = self.session.edit_subset_mode.edit_subset[0] self.subset_select._update_subset(subset_to_update, attribute="type") @@ -86,7 +85,7 @@ def _sync_selected_from_ui(self, change): return if change['new'] != self.subset_select.default_text: - self._get_region_definition(change['new']) + self._get_subset_definition(change['new']) self.show_region_info = change['new'] != self.subset_select.default_text m = [s for s in self.app.data_collection.subset_groups if s.label == change['new']] if m != self.session.edit_subset_mode.edit_subset: @@ -101,40 +100,65 @@ def _mode_selected_changed(self, event={}): self.session.edit_subset_mode = self.mode_selected ''' - def _get_region_definition(self, *args): - self.subset_definition = {} + def _unpack_nested_subset(self, subset_state): + ''' + Navigate through the tree of subset states for composite + subsets made up of multiple regions. + ''' + if isinstance(subset_state, CompositeSubsetState): + self._unpack_nested_subset(subset_state.state1) + self._unpack_nested_subset(subset_state.state2) + else: + if subset_state is not None: + self._get_subset_subregion_definition(subset_state) + + def _get_subset_subregion_definition(self, subset_state): + """ + Get the type and parameters for a single region in the subset. Note that + the string type and operation (if in a composite subset) need to be stored + separately from the float parameters for display reasons. + """ + subset_type = {} + subset_definition = None + + if isinstance(subset_state, RoiSubsetState): + subset_classname = subset_state.roi.__class__.__name__ + if subset_classname == "CircularROI": + x, y = subset_state.roi.get_center() + subset_definition = {"X Center": x, + "Y Center": y, + "Radius": subset_state.roi.radius} + + elif subset_classname == "RectangularROI": + subset_definition = {} + for att in ("Xmin", "Xmax", "Ymin", "Ymax"): + subset_definition[att] = getattr(subset_state.roi, att.lower()) + + elif subset_classname == "EllipticalROI": + subset_definition = {"X Center": subset_state.roi.xc, + "Y Center": subset_state.roi.yc, + "X Radius": subset_state.roi.radius_x, + "Y Radius": subset_state.roi.radius_y} + subset_type["Subset type"] = subset_classname + + elif isinstance(subset_state, RangeSubsetState): + subset_definition = {"Upper bound": subset_state.hi, + "Lower bound": subset_state.lo} + subset_type["Subset type"] = "Range" + + if subset_definition is not None and subset_definition not in self.subset_definitions: + self.subset_definitions = self.subset_definitions + [subset_definition] + self.subset_types = self.subset_types + [subset_type] + + def _get_subset_definition(self, *args): + """ + Retrieve the parameters defining the selected subset, for example the + upper and lower bounds for a simple spectral subset. + """ + self.subset_definitions = [] + self.subset_types = [] subset_group = [s for s in self.app.data_collection.subset_groups if s.label == self.subset_selected][0] subset_state = subset_group.subset_state - subset_class = subset_state.__class__ - if subset_class in (OrState, AndState, XorState, InvertState): - self.subset_classname = "Compound Subset" - self.has_subset_details = False - else: - if isinstance(subset_state, RoiSubsetState): - self.subset_classname = subset_state.roi.__class__.__name__ - self.has_subset_details = True - if self.subset_classname == "CircularROI": - x, y = subset_state.roi.get_center() - self.subset_definition = {"X Center": x, - "Y Center": y, - "Radius": subset_state.roi.radius} - elif self.subset_classname == "RectangularROI": - temp_def = {} - for att in ("Xmin", "Xmax", "Ymin", "Ymax"): - temp_def[att] = getattr(subset_state.roi, att.lower()) - self.subset_definition = temp_def - elif self.subset_classname == "EllipticalROI": - self.subset_definition = {"X Center": subset_state.roi.xc, - "Y Center": subset_state.roi.yc, - "X Radius": subset_state.roi.radius_x, - "Y Radius": subset_state.roi.radius_y} - elif isinstance(subset_state, RangeSubsetState): - self.subset_classname = "Range" - self.subset_definition = {"Upper bound": subset_state.hi, - "Lower bound": subset_state.lo} - self.has_subset_details = True - else: - self.subset_classname = subset_class.__name__ - self.has_subset_details = False + self._unpack_nested_subset(subset_state) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue index 807b3f6e0f..93de7a32c4 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue @@ -27,24 +27,28 @@
Subset Region Definition - - Subset Type: - {{ subset_classname }} -
- - {{ key }}: +
+ - + + {{ key }}: + {{ val }} + + + {{ key }}: + + + +
-
- Could not retrieve subset parameters for this subset type -
diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index ca4241827d..6262aa3764 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -56,11 +56,11 @@ def test_region_from_subset_3d(cubeviz_helper): assert_allclose(reg.height, 3.5) assert subset_plugin.subset_selected == "Subset 1" - assert subset_plugin.subset_classname == "RectangularROI" - assert subset_plugin.subset_definition["Xmin"] == 1 - assert subset_plugin.subset_definition["Xmax"] == 3.5 - assert subset_plugin.subset_definition["Ymin"] == -0.2 - assert subset_plugin.subset_definition["Ymax"] == 3.3 + assert subset_plugin.subset_types == [{"Subset type": "RectangularROI"}] + assert subset_plugin.subset_definitions[0]["Xmin"] == 1 + assert subset_plugin.subset_definitions[0]["Xmax"] == 3.5 + assert subset_plugin.subset_definitions[0]["Ymin"] == -0.2 + assert subset_plugin.subset_definitions[0]["Ymax"] == 3.3 # Circular Subset flux_viewer = cubeviz_helper.app.get_viewer("flux-viewer") @@ -68,10 +68,10 @@ def test_region_from_subset_3d(cubeviz_helper): flux_viewer.toolbar.active_tool = flux_viewer.toolbar.tools['bqplot:circle'] cubeviz_helper.app.get_viewer('flux-viewer').apply_roi(CircularROI(xc=3, yc=4, radius=2.4)) assert subset_plugin.subset_selected == "Subset 2" - assert subset_plugin.subset_classname == "CircularROI" - assert subset_plugin.subset_definition["X Center"] == 3 - assert subset_plugin.subset_definition["Y Center"] == 4 - assert subset_plugin.subset_definition["Radius"] == 2.4 + assert subset_plugin.subset_types == [{"Subset type": "CircularROI"}] + assert subset_plugin.subset_definitions[0]["X Center"] == 3 + assert subset_plugin.subset_definitions[0]["Y Center"] == 4 + assert subset_plugin.subset_definitions[0]["Radius"] == 2.4 def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): @@ -93,9 +93,9 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): assert_quantity_allclose(reg.upper, 15.0 * u.Hz) assert subset_plugin.subset_selected == "Subset 1" - assert subset_plugin.subset_classname == "Range" - assert subset_plugin.subset_definition["Lower bound"] == 5 - assert subset_plugin.subset_definition["Upper bound"] == 15.5 + assert subset_plugin.subset_types == [{"Subset type": "Range"}] + assert subset_plugin.subset_definitions[0]["Lower bound"] == 5 + assert subset_plugin.subset_definitions[0]["Upper bound"] == 15.5 def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): @@ -134,6 +134,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): + subset_plugin = SubsetPlugin(app=cubeviz_helper.app) data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) cubeviz_helper.app.data_collection.append(data) @@ -156,3 +157,10 @@ def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): assert_quantity_allclose(reg[0].upper, 15.0*u.Hz) assert_quantity_allclose(reg[1].lower, 30.0*u.Hz) assert_quantity_allclose(reg[1].upper, 35.0*u.Hz) + + assert subset_plugin.subset_selected == "Subset 1" + assert subset_plugin.subset_types == [{"Subset type": "Range"}, {"Subset type": "Range"}] + assert subset_plugin.subset_definitions[0]["Lower bound"] == 30 + assert subset_plugin.subset_definitions[0]["Upper bound"] == 35 + assert subset_plugin.subset_definitions[1]["Lower bound"] == 5 + assert subset_plugin.subset_definitions[1]["Upper bound"] == 15.5