diff --git a/jdaviz/app.py b/jdaviz/app.py index 2a462fc546..295c329c79 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -356,6 +356,9 @@ def __init__(self, configuration=None, *args, **kwargs): self.hub.subscribe(self, SubsetUpdateMessage, handler=lambda msg: self._clear_object_cache(msg.subset.label)) + # Store for associations between Data entries: + self._data_associations = self._init_data_associations() + # Subscribe to messages that result in changes to the layers self.hub.subscribe(self, AddDataMessage, handler=self._on_layers_changed) @@ -473,9 +476,11 @@ def _on_layers_changed(self, msg): if hasattr(msg, 'data'): layer_name = msg.data.label is_wcs_only = msg.data.meta.get(_wcs_only_label, False) + is_not_child = self._get_assoc_data_parent(layer_name) is None elif hasattr(msg, 'subset'): layer_name = msg.subset.label is_wcs_only = False + is_not_child = True else: raise NotImplementedError(f"cannot recognize new layer from {msg}") @@ -490,13 +495,25 @@ def _on_layers_changed(self, msg): self.state.layer_icons = {**self.state.layer_icons, layer_name: orientation_icons.get(layer_name, wcs_only_refdata_icon)} - else: + elif is_not_child: self.state.layer_icons = { **self.state.layer_icons, layer_name: alpha_index(len([ln for ln, ic in self.state.layer_icons.items() - if not ic.startswith('mdi-')])) + if not ic.startswith('mdi-') and + self._get_assoc_data_parent(ln) is None])) } + # all remaining layers at this point have a parent: + for layer_name in self.state.layer_icons: + children_layers = self._get_assoc_data_children(layer_name) + if children_layers is not None: + parent_icon = self.state.layer_icons[layer_name] + for i, child_layer in enumerate(children_layers, start=1): + self.state.layer_icons = { + **self.state.layer_icons, + child_layer: f'{parent_icon}{i}' + } + def _change_reference_data(self, new_refdata_label, viewer_id=None): """ Change reference data to Data with ``data_label``. @@ -1251,7 +1268,7 @@ def merge_overlapping_spectral_regions(self, subset_name, att): return new_state - def add_data(self, data, data_label=None, notify_done=True): + def add_data(self, data, data_label=None, notify_done=True, parent=None): """ Add data to the Glue ``DataCollection``. @@ -1280,6 +1297,11 @@ def add_data(self, data, data_label=None, notify_done=True): self.data_collection[data_label] = data + # manage associated Data entries: + self._add_assoc_data_as_parent(data_label) + if parent is not None: + self._set_assoc_data_as_child(data_label, new_parent_label=parent) + # Send out a toast message if notify_done: snackbar_message = SnackbarMessage( @@ -1998,6 +2020,17 @@ def set_data_visibility(self, viewer_reference, data_label, visible=True, replac if layer.layer.data.label != data_label: layer.visible = False + # if Data has children, update their visibilities to match Data: + assoc_children = self._get_assoc_data_children(data_label) + for layer in viewer.layers: + for data_label in assoc_children: + if layer.layer.data.label == data_label: + if visible and not layer.visible: + layer.visible = True + layer.update() + else: + layer.visible = visible + # update data menu - selected_data_items should be READ ONLY, not modified by the user/UI selected_items = viewer_item['selected_data_items'] data_id = self._data_id_from_label(data_label) @@ -2577,3 +2610,27 @@ def get_tray_item_from_name(self, name): raise KeyError(f'{name} not found in app.state.tray_items') return tray_item + + def _init_data_associations(self): + # assume all Data are parents: + data_associations = { + data.label: {'parent': None, 'children': []} + for data in self.data_collection + } + return data_associations + + def _add_assoc_data_as_parent(self, data_label): + self._data_associations[data_label] = {'parent': None, 'children': []} + + def _set_assoc_data_as_child(self, data_label, new_parent_label): + # Data has a new parent: + self._data_associations[data_label]['parent'] = new_parent_label + # parent has a new child: + self._data_associations[new_parent_label]['children'].append(data_label) + + def _get_assoc_data_children(self, data_label): + # intentionally not recursive for now, just one generation: + return self._data_associations.get(data_label, {}).get('children', []) + + def _get_assoc_data_parent(self, data_label): + return self._data_associations.get(data_label, {}).get('parent') diff --git a/jdaviz/configs/imviz/plugins/parsers.py b/jdaviz/configs/imviz/plugins/parsers.py index 7228ae5079..97558ed568 100644 --- a/jdaviz/configs/imviz/plugins/parsers.py +++ b/jdaviz/configs/imviz/plugins/parsers.py @@ -30,7 +30,7 @@ @data_parser_registry("imviz-data-parser") -def parse_data(app, file_obj, ext=None, data_label=None): +def parse_data(app, file_obj, ext=None, data_label=None, parent=None): """Parse a data file into Imviz. Parameters @@ -74,17 +74,17 @@ def parse_data(app, file_obj, ext=None, data_label=None): else: # Assume RGB pf = rgb2gray(im) pf = pf[::-1, :] # Flip it - _parse_image(app, pf, data_label, ext=ext) + _parse_image(app, pf, data_label, ext=ext, parent=parent) elif file_obj_lower.endswith('.asdf'): try: if HAS_ROMAN_DATAMODELS: with rdd.open(file_obj) as pf: - _parse_image(app, pf, data_label, ext=ext) + _parse_image(app, pf, data_label, ext=ext, parent=parent) except TypeError: # if roman_datamodels cannot parse the file, load it with asdf: with asdf.open(file_obj) as af: - _parse_image(app, af, data_label, ext=ext) + _parse_image(app, af, data_label, ext=ext, parent=parent) elif file_obj_lower.endswith('.reg'): # This will load DS9 regions as Subset but only if there is already data. @@ -92,9 +92,9 @@ def parse_data(app, file_obj, ext=None, data_label=None): else: # Assume FITS with fits.open(file_obj) as pf: - _parse_image(app, pf, data_label, ext=ext) + _parse_image(app, pf, data_label, ext=ext, parent=parent) else: - _parse_image(app, file_obj, data_label, ext=ext) + _parse_image(app, file_obj, data_label, ext=ext, parent=parent) def get_image_data_iterator(app, file_obj, data_label, ext=None): @@ -168,7 +168,7 @@ def get_image_data_iterator(app, file_obj, data_label, ext=None): return data_iter -def _parse_image(app, file_obj, data_label, ext=None): +def _parse_image(app, file_obj, data_label, ext=None, parent=None): if app is None: raise ValueError("app is None, cannot proceed") if data_label is None: @@ -186,7 +186,7 @@ def _parse_image(app, file_obj, data_label, ext=None): data.coords.bounding_box = None if not data.meta.get(_wcs_only_label, False): data_label = app.return_data_label(data_label, alt_name="image_data") - app.add_data(data, data_label) + app.add_data(data, data_label, parent=parent) # Do not link image data here. We do it at the end in Imviz.load_data() diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 1a4626b42c..f5de99412a 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -1365,6 +1365,12 @@ def __init__(self, plugin, items, selected, viewer, self._update_layer_items() self.update_wcs_only_filter(only_wcs_layers) + # ignore layers that are children in associations: + def is_parent(data): + return self.app._get_assoc_data_parent(data.label) is None + + self.add_filter(is_parent) + def _get_viewer(self, viewer): # newer will likely be the viewer name in most cases, but viewer id in the case # of additional viewers in imviz. @@ -2923,6 +2929,12 @@ def __init__(self, plugin, items, selected, # initialize items from original viewers self._on_data_changed() + # ignore layers that are children in associations: + def is_parent(data): + return self.app._get_assoc_data_parent(data.label) is None + + self.add_filter(is_parent) + def _cubeviz_include_spatial_subsets(self): """ Call this method to prepend spatial subsets to the list of datasets (and listen for newly