"""
Helper classes are meant to provide a convenient user API for specific
configurations. They allow a separation of "viztool-specific" API and the glue
application objects.
See also https://github.com/spacetelescope/jdaviz/issues/104 for more details
on the motivation behind this concept.
"""
import re
import warnings
from contextlib import contextmanager
from inspect import isclass
import numpy as np
import astropy.units as u
from astropy.wcs.wcsapi import BaseHighLevelWCS
from astropy.nddata import CCDData
from regions.core.core import Region
from glue.core import HubListener
from glue.core.edit_subset_mode import NewMode
from glue.core.message import SubsetCreateMessage, SubsetDeleteMessage
from glue.core.subset import Subset, MaskSubsetState
from glue.config import data_translator
from ipywidgets.widgets import widget_serialization
from specutils import Spectrum1D, SpectralRegion
from jdaviz.app import Application
from jdaviz.core.events import SnackbarMessage, ExitBatchLoadMessage
from jdaviz.core.template_mixin import show_widget
__all__ = ['ConfigHelper', 'ImageConfigHelper']
[docs]class ConfigHelper(HubListener):
"""The Base Helper Class.
Provides shared abstracted helper methods to the user.
Subclasses should set ``_default_configuration`` if they are meant to be
used with a specific configuration.
Parameters
----------
app : `~jdaviz.app.Application` or `None`
The application object, or if `None`, creates a new one based on the
default configuration for this helper.
verbosity : {'debug', 'info', 'warning', 'error'}
Verbosity of the popup messages in the application.
history_verbosity : {'debug', 'info', 'warning', 'error'}
Verbosity of the history logger in the application.
"""
_default_configuration = 'default'
def __init__(self, app=None, verbosity='warning', history_verbosity='info'):
if app is None:
self.app = Application(configuration=self._default_configuration)
else:
self.app = app
self.app.verbosity = verbosity
self.app.history_verbosity = history_verbosity
# give a reference from the app back to this config helper. These can be accessed from a
# viewer via viewer.jdaviz_app and viewer.jdaviz_helper
# if the helper has already been set, this is probably a nested viz tool. Don't overwrite
if self.app._jdaviz_helper is None:
self.app._jdaviz_helper = self
self.app.hub.subscribe(self, SubsetCreateMessage,
handler=lambda msg: self._propagate_callback_to_viewers('_on_subset_create', msg)) # noqa
self.app.hub.subscribe(self, SubsetDeleteMessage,
handler=lambda msg: self._propagate_callback_to_viewers('_on_subset_delete', msg)) # noqa
self._in_batch_load = 0
self._delayed_show_in_viewer_labels = {} # label: viewer_reference pairs
def _propagate_callback_to_viewers(self, method, msg):
# viewers don't have access to the app/hub to subscribe to messages, so we'll
# catch all messages here and pass them on to each of the viewers that
# have the applicable method implemented.
for viewer in self.app._viewer_store.values():
if hasattr(viewer, method):
getattr(viewer, method)(msg)
[docs] @contextmanager
def batch_load(self):
"""
Context manager to delay linking and loading data into viewers
"""
# we'll use a counter instead of a boolean to allow the user to nest multiple
# context managers. Once they're all exited, then the linking/showing will
# take place.
self._in_batch_load += 1
with self.app.data_collection.delay_link_manager_update():
# user entrypoint (anything within the with-statement will get called here)
yield
self._in_batch_load -= 1
if not self._in_batch_load:
self.app.hub.broadcast(ExitBatchLoadMessage(sender=self.app))
# add any data to viewers that were requested but deferred
for data_label, viewer_ref in self._delayed_show_in_viewer_labels.items():
self.app.set_data_visibility(viewer_ref, data_label,
visible=True, replace=False)
self._delayed_show_in_viewer_labels = {}
[docs] def load_data(self, data, data_label=None, parser_reference=None, **kwargs):
if data_label:
kwargs['data_label'] = data_label
self.app.load_data(data, parser_reference=parser_reference, **kwargs)
@property
def plugins(self):
"""
Access API objects for plugins in the plugin tray.
Returns
-------
plugins : dict
dict of plugin objects
"""
return {item['label']: widget_serialization['from_json'](item['widget'], None).user_api
for item in self.app.state.tray_items}
@property
def fitted_models(self):
"""
Returns the fitted models.
Returns
-------
parameters : dict
dict of `astropy.modeling.Model` objects, or None.
"""
return self.app.fitted_models
[docs] def get_models(self, models=None, model_label=None, x=None, y=None):
"""
Loop through all models and output models of the label model_label.
If x or y is set, return model_labels of those (x, y) coordinates.
If x and y are None, print all models regardless of coordinates.
Parameters
----------
models : dict
A dict of models, with the key being the label name and the value
being an `astropy.modeling.CompoundModel` object. Defaults to
`fitted_models` if no parameter is provided.
model_label : str
The name of the model that will be found and returned. If it
equals default, every model present will be returned.
x : int
The x coordinate of the model spaxels that will be returned.
y : int
The y coordinate of the model spaxels that will be returned.
Returns
-------
selected_models : dict
Dictionary of the selected models.
"""
selected_models = {}
# If models is not provided, use the app's fitted models
if not models:
models = self.fitted_models
# Loop through all keys in the dict models
for label in models:
# Prevent "Model 2" from being returned when model_label is "Model"
if model_label is not None:
if label.split(" (")[0] != model_label:
continue
# If no label was provided, use label name without coordinates.
if model_label is None and " (" in label:
find_label = label.split(" (")[0]
# If coordinates are not present, just use the label.
elif model_label is None:
find_label = label
else:
find_label = model_label
# If x and y are set, return keys that match the model plus that
# coordinate pair. If only x or y is set, return keys that fit
# that value for the appropriate coordinate.
if x is not None and y is not None:
find_label = r"{} \({}, {}\)".format(find_label, x, y)
elif x:
find_label = r"{} \({}, .+\)".format(find_label, x)
elif y:
find_label = r"{} \(.+, {}\)".format(find_label, y)
if re.search(find_label, label):
selected_models[label] = models[label]
return selected_models
[docs] def get_model_parameters(self, models=None, model_label=None, x=None, y=None):
"""
Convert each parameter of model inside models into a coordinate that
maps the model name and parameter name to a `astropy.units.Quantity`
object.
Parameters
----------
models : dict
A dictionary where the key is a model name and the value is an
`astropy.modeling.CompoundModel` object.
model_label : str
Get model parameters for a particular model by inputting its label.
x : int
The x coordinate of the model spaxels that will be returned from
get_models.
y : int
The y coordinate of the model spaxels that will be returned from
get_models.
Returns
-------
:dict: a dictionary of the form
{model name: {parameter name: [[`astropy.units.Quantity`]]}}
for 3d models or
{model name: {parameter name: `astropy.units.Quantity`}} where the
Quantity object represents the parameter value and unit of one of
spaxel models or the 1d models, respectively.
"""
if models and model_label:
models = self.get_models(models=models, model_label=model_label, x=x, y=y)
elif models is None and model_label:
models = self.get_models(model_label=model_label, x=x, y=y)
elif models is None:
models = self.fitted_models
data_shapes = {}
for label in models:
data_label = label.split(" (")[0]
if data_label not in data_shapes:
data_shapes[data_label] = self.app.data_collection[data_label].data.shape
param_dict = {}
parameters_cube = {}
param_x_y = {}
param_units = {}
for label in models:
# 3d models take the form of "Model (1,2)" so this if statement
# looks for that style and separates out the pertinent information.
if " (" in label:
label_split = label.split(" (")
model_name = label_split[0]
x = int(label_split[1].split(", ")[0])
y = int(label_split[1].split(", ")[1][:-1])
# x and y values are added to this dict where they will be used
# to convert the models of each spaxel into a single
# coordinate in the parameters_cube dictionary.
if model_name not in param_x_y:
param_x_y[model_name] = {'x': [], 'y': []}
if x not in param_x_y[model_name]['x']:
param_x_y[model_name]['x'].append(x)
if y not in param_x_y[model_name]['y']:
param_x_y[model_name]['y'].append(y)
# 1d models will be handled by this else statement.
else:
model_name = label
if model_name not in param_dict:
param_dict[model_name] = list(models[label].param_names)
# This adds another dictionary as the value of
# parameters_cube[model_name] where the key is the parameter name
# and the value is either a 2d array of zeros or a single value, depending
# on whether the model in question is 3d or 1d, respectively.
for model_name in param_dict:
if model_name in param_x_y:
parameters_cube[model_name] = {x: np.zeros(shape=data_shapes[model_name][:2])
for x in param_dict[model_name]}
else:
parameters_cube[model_name] = {x: 0
for x in param_dict[model_name]}
# This loop handles the actual placement of param.values and
# param.units into the parameter_cubes dictionary.
for label in models:
if " (" in label:
label_split = label.split(" (")
model_name = label_split[0]
# If the get_models method is used to build a dictionary of
# models and a value is set for the x or y parameters, that
# will mean that only one x or y value is present in the
# models.
if len(param_x_y[model_name]['x']) == 1:
x = 0
else:
x = int(label_split[1].split(", ")[0])
if len(param_x_y[model_name]['y']) == 1:
y = 0
else:
y = int(label_split[1].split(", ")[1][:-1])
param_units[model_name] = {}
for name in param_dict[model_name]:
param = getattr(models[label], name)
parameters_cube[model_name][name][x][y] = param.value
param_units[model_name][name] = param.unit
else:
model_name = label
param_units[model_name] = {}
# 1d models do not have anything set of param.unit, so the
# return_units and input_units properties need to be used
# instead, depending on the type of parameter `name` is.
for name in param_dict[model_name]:
param = getattr(models[label], name)
parameters_cube[model_name][name] = param.value
param_units[model_name][name] = param.unit
# Convert values of parameters_cube[key][param_name] into u.Quantity
# objects that contain the appropriate unit set in
# param_units[key][param_name]
for key in parameters_cube:
for param_name in parameters_cube[key]:
parameters_cube[key][param_name] = u.Quantity(
parameters_cube[key][param_name],
param_units[key].get(param_name, None))
return parameters_cube
[docs] def show(self, loc="inline", title=None, height=None): # pragma: no cover
"""Display the Jdaviz application.
Parameters
----------
loc : str
The display location determines where to present the viz app.
Supported locations:
"inline": Display the Jdaviz application inline in a notebook.
Note this is functionally equivalent to displaying the cell
``viz.app`` in the notebook.
"sidecar": Display the Jdaviz application in a separate JupyterLab window from the
notebook, the location of which is decided by the 'anchor.' right is the default
Other anchors:
* ``sidecar:right`` (The default, opens a tab to the right of display)
* ``sidecar:tab-before`` (Full-width tab before the current notebook)
* ``sidecar:tab-after`` (Full-width tab after the current notebook)
* ``sidecar:split-right`` (Split-tab in the same window right of the notebook)
* ``sidecar:split-left`` (Split-tab in the same window left of the notebook)
* ``sidecar:split-top`` (Split-tab in the same window above the notebook)
* ``sidecar:split-bottom`` (Split-tab in the same window below the notebook)
See `jupyterlab-sidecar <https://github.com/jupyter-widgets/jupyterlab-sidecar>`_
for the most up-to-date options.
"popout": Display the Jdaviz application in a detached display. By default, a new
window will open. Browser popup permissions required.
Other anchors:
* ``popout:window`` (The default, opens Jdaviz in a new, detached popout)
* ``popout:tab`` (Opens Jdaviz in a new, detached tab in your browser)
title : str, optional
The title of the sidecar tab. Defaults to the name of the
application; e.g., "specviz".
NOTE: Only applicable to a "sidecar" display.
height: int, optional
The height of the top-level application widget, in pixels. Applies to all
instances of the same application in the notebook.
Notes
-----
If "sidecar" is requested in the "classic" Jupyter notebook, the app will appear inline,
as only JupyterLab has a mechanism to have multiple tabs.
"""
title = self.app.config if title is None else title
if height is not None:
if isinstance(height, int):
height = f"{height}px"
self.app.layout.height = height
self.app.state.settings['context']['notebook']['max_height'] = height
show_widget(self.app, loc=loc, title=title)
[docs] def show_in_sidecar(self, anchor=None, title=None): # pragma: no cover
"""
Preserved for backwards compatibility
Shows Jdaviz in a sidecar with the default anchor: right
"""
warnings.warn('show_in_sidecar has been replaced with show(loc="sidecar")',
DeprecationWarning)
location = 'sidecar' if anchor is None else f"sidecar:{anchor}"
return self.show(loc=location, title=title)
[docs] def show_in_new_tab(self, title=None): # pragma: no cover
"""
Preserved for backwards compatibility
Shows Jdaviz in a sidecar in a new tab to the right
"""
warnings.warn('show_in_new_tab has been replaced with show(loc="sidecar:tab-after")',
DeprecationWarning)
return self.show(loc="sidecar:tab-after", title=title)
def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None,
mask_subset=None, function=None, cls=None):
# Start validity checks
list_of_valid_function_values = ('minimum', 'maximum', 'mean',
'median', 'sum')
if function and function not in list_of_valid_function_values:
raise ValueError(f"function {function} not in list of valid"
f" function values {list_of_valid_function_values}")
list_of_valid_subset_names = [x.label for x in self.app.data_collection.subset_groups]
for subset in (spatial_subset, spectral_subset, mask_subset):
if subset and subset not in list_of_valid_subset_names:
raise ValueError(f"Subset {subset} not in list of valid"
f" subset names {list_of_valid_subset_names}")
if data_label and data_label not in self.app.data_collection.labels:
raise ValueError(f'{data_label} not in {self.app.data_collection.labels}.')
elif not data_label and len(self.app.data_collection) > 1:
raise ValueError('data_label must be set if more than'
' one data exists in data_collection.')
elif not data_label and len(self.app.data_collection) == 1:
data_label = self.app.data_collection[0].label
if cls is not None and not isclass(cls):
raise TypeError(
"cls in get_data must be a class or None.")
if spectral_subset:
if mask_subset is not None:
raise ValueError("cannot use both mask_subset and spectral_subset")
# spectral_subset is applied as a mask, the only difference is that it has
# its own set of validity checks (whereas mask_subset can be used by downstream
# apps which would then need to do their own type checks, if necessary)
mask_subset = spectral_subset
# End validity checks and start data retrieval
data = self.app.data_collection[data_label]
if not cls:
if 'Trace' in data.meta:
cls = None
elif data.ndim == 2 and self.app.config == "specviz2d":
cls = Spectrum1D
elif data.ndim == 2:
cls = CCDData
elif data.ndim in [1, 3]:
cls = Spectrum1D
object_kwargs = {}
if cls == Spectrum1D:
object_kwargs['statistic'] = function
if not spatial_subset and not mask_subset:
if 'Trace' in data.meta:
if cls is not None: # pragma: no cover
raise ValueError("cls not supported for Trace object")
data = data.get_object()
else:
data = data.get_object(cls=cls, **object_kwargs)
return data
if not cls and spatial_subset:
raise AttributeError(f"A valid cls must be provided to"
f" apply subset {spatial_subset} to data. "
f"Instead, {cls} was given.")
elif not cls and mask_subset:
raise AttributeError(f"A valid cls must be provided to"
f" apply subset {mask_subset} to data. "
f"Instead, {cls} was given.")
# Now we work on applying subsets to the data
all_subsets = self.app.get_subsets(object_only=True)
# Handle spatial subset
if spatial_subset and not isinstance(all_subsets[spatial_subset][0],
Region):
raise ValueError(f"{spatial_subset} is not a spatial subset.")
elif spatial_subset:
real_spatial = [sub for subsets in self.app.data_collection.subset_groups
for sub in subsets.subsets
if sub.data.label == data_label and subsets.label == spatial_subset][0]
handler, _ = data_translator.get_handler_for(cls)
try:
data = handler.to_object(real_spatial, **object_kwargs)
except Exception as e:
warnings.warn(f"Not able to get {data_label} returned with"
f" subset {spatial_subset} applied of type {cls}."
f" Exception: {e}")
elif function:
# This covers the case where cubeviz.get_data is called using a spectral_subset
# with function set.
data = data.get_object(cls=cls, **object_kwargs)
# Handle spectral subset, including case where spatial subset is also set
if spectral_subset and not isinstance(all_subsets[spectral_subset],
SpectralRegion):
raise ValueError(f"{spectral_subset} is not a spectral subset.")
if mask_subset:
real_spectral = [sub for subsets in self.app.data_collection.subset_groups
for sub in subsets.subsets
if sub.data.label == data_label and subsets.label == mask_subset][0] # noqa
handler, _ = data_translator.get_handler_for(cls)
try:
spec_subset = handler.to_object(real_spectral, **object_kwargs)
except Exception as e:
warnings.warn(f"Not able to get {data_label} returned with"
f" subset {mask_subset} applied of type {cls}."
f" Exception: {e}")
if spatial_subset or function:
# Return collapsed Spectrum1D object with spectral subset mask applied
data.mask = spec_subset.mask
else:
data = spec_subset
return data
[docs] def get_data(self, data_label=None, cls=None):
"""
Returns data with name equal to data_label of type cls.
Parameters
----------
data_label : str, optional
Provide a label to retrieve a specific data set from data_collection.
cls : `~specutils.Spectrum1D`, `~astropy.nddata.CCDData`, optional
The type that data will be returned as.
Returns
-------
data : cls
Data is returned as type cls.
"""
return self._get_data(data_label=data_label, spatial_subset=None,
spectral_subset=None, function=None, cls=None)
[docs]class ImageConfigHelper(ConfigHelper):
"""`ConfigHelper` that uses an image viewer as its primary viewer.
For example, Imviz and Cubeviz.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# NOTE: The first viewer must always be there and is an image viewer.
self._default_viewer = self.app.get_viewer_by_id(f'{self.app.config}-0')
@property
def default_viewer(self):
"""Default viewer instance. This is typically the first viewer
(e.g., "imviz-0" or "cubeviz-0")."""
return self._default_viewer
[docs] def load_regions_from_file(self, region_file, region_format='ds9', max_num_regions=20,
**kwargs):
"""Load regions defined in the given file.
See :ref:`regions:regions_io` for supported file formats.
See :meth:`load_regions` for how regions are loaded.
Parameters
----------
region_file : str
Path to region file.
region_format : {'crtf', 'ds9', 'fits'}
See :meth:`regions.Regions.get_formats`.
max_num_regions : int or `None`
Maximum number of regions to read from the file, starting
from top of the file. Default is first 20 regions that
can be successfully loaded. If `None`, it will load everything.
kwargs : dict
See :meth:`load_regions`.
Returns
-------
bad_regions : list of (obj, str) or `None`
See :meth:`load_regions`.
"""
from regions import Regions
raw_regs = Regions.read(region_file, format=region_format)
return self.load_regions(raw_regs, max_num_regions=max_num_regions, **kwargs)
[docs] def load_regions(self, regions, max_num_regions=None, refdata_label=None,
return_bad_regions=False, **kwargs):
"""Load given region(s) into the viewer.
WCS-to-pixel translation and mask creation, if needed, is relative
to the image defined by ``refdata_label``. Meanwhile, the rest of
the Subset operations are based on reference image as defined by Glue.
.. note:: Loading too many regions will affect performance.
A valid region can be loaded into one of the following categories:
* An interactive Subset, as if it was drawn by hand. This is
always done for supported shapes. Its label will be
``'Subset N'``, where ``N`` is an integer.
* A masked Subset that will display on the image but cannot be
modified once loaded. This is done if the shape cannot be
made interactive. Its label will be ``'MaskedSubset N'``,
where ``N`` is an integer.
Parameters
----------
regions : list of obj
A list of region objects. A region object can be one of the following:
* Astropy ``regions`` object
* ``photutils`` apertures (limited support until ``photutils``
fully supports ``regions``)
* Numpy boolean array (shape must match data, dtype should be ``np.bool_``)
max_num_regions : int or `None`
Maximum number of regions to load, starting from top of the list.
Default is to load everything.
refdata_label : str or `None`
Label of data to use for sky-to-pixel conversion for a region, or
mask creation. Data must already be loaded into Jdaviz.
If `None`, defaults to the reference data in the default viewer.
Choice of this data is particularly important when sky or masked
region is involved.
return_bad_regions : bool
If `True`, return the regions that failed to load (see ``bad_regions``);
This is useful for debugging. If `False`, do not return anything (`None`).
kwargs : dict
Extra keywords to be passed into the region's ``to_mask`` method.
**This is ignored if the region can be made interactive or
if a Numpy array is given.**
Returns
-------
bad_regions : list of (obj, str) or `None`
If requested (see ``return_bad_regions`` option), return a
list of ``(region, reason)`` tuples for region objects that failed to load.
If all the regions loaded successfully, this list will be empty.
If not requested, return `None`.
"""
from photutils.aperture import (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture)
from regions import (Regions, CirclePixelRegion, CircleSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectanglePixelRegion, RectangleSkyRegion)
from jdaviz.core.region_translators import regions2roi, aperture2regions
# If user passes in one region obj instead of list, try to be smart.
if not isinstance(regions, (list, tuple, Regions)):
regions = [regions]
n_loaded = 0
bad_regions = []
# To keep track of masked subsets.
msg_prefix = 'MaskedSubset'
msg_count = _next_subset_num(msg_prefix, self.app.data_collection.subset_groups)
# Subset is global but reference data is viewer-dependent.
if refdata_label is None:
data = self.default_viewer.state.reference_data
else:
data = self.app.data_collection[refdata_label]
# TODO: Make this work for data cube.
# https://github.com/glue-viz/glue-astronomy/issues/75
has_wcs = data_has_valid_wcs(data, ndim=2)
for region in regions:
if isinstance(region, (SkyCircularAperture, SkyEllipticalAperture,
SkyRectangularAperture, CircleSkyRegion,
EllipseSkyRegion, RectangleSkyRegion)) and not has_wcs:
bad_regions.append((region, 'Sky region provided but data has no valid WCS'))
continue
# photutils: Convert to regions shape first
if isinstance(region, (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture)):
region = aperture2regions(region)
# regions: Convert to ROI.
# NOTE: Out-of-bounds ROI will succeed; this is native glue behavior.
if isinstance(region, (CirclePixelRegion, CircleSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectanglePixelRegion, RectangleSkyRegion)):
state = regions2roi(region, wcs=data.coords)
# TODO: Do we want user to specify viewer? Does it matter?
self.app.session.edit_subset_mode._mode = NewMode
self.default_viewer.apply_roi(state)
self.app.session.edit_subset_mode.edit_subset = None # No overwrite next iteration # noqa
# Last resort: Masked Subset that is static (if data is not a cube)
elif data.ndim == 2:
im = None
if hasattr(region, 'to_pixel'): # Sky region: Convert to pixel region
if not has_wcs:
bad_regions.append((region, 'Sky region provided but data has no valid WCS')) # noqa
continue
region = region.to_pixel(data.coords)
if hasattr(region, 'to_mask'):
try:
mask = region.to_mask(**kwargs)
im = mask.to_image(data.shape) # Can be None
except Exception as e: # pragma: no cover
bad_regions.append((region, f'Failed to load: {repr(e)}'))
continue
elif (isinstance(region, np.ndarray) and region.shape == data.shape
and region.dtype == np.bool_):
im = region
if im is None:
bad_regions.append((region, 'Mask creation failed'))
continue
# NOTE: Region creation info is thus lost.
try:
subset_label = f'{msg_prefix} {msg_count}'
state = MaskSubsetState(im, data.pixel_component_ids)
self.app.data_collection.new_subset_group(subset_label, state)
msg_count += 1
except Exception as e: # pragma: no cover
bad_regions.append((region, f'Failed to load: {repr(e)}'))
continue
else:
bad_regions.append((region, 'Mask creation failed'))
continue
n_loaded += 1
if max_num_regions is not None and n_loaded >= max_num_regions:
break
n_reg_in = len(regions)
n_reg_bad = len(bad_regions)
if n_loaded == 0:
snack_color = "error"
elif n_reg_bad > 0:
snack_color = "warning"
else:
snack_color = "success"
self.app.hub.broadcast(SnackbarMessage(
f"Loaded {n_loaded}/{n_reg_in} regions, max_num_regions={max_num_regions}, "
f"bad={n_reg_bad}", color=snack_color, timeout=8000, sender=self.app))
if return_bad_regions:
return bad_regions
[docs] def get_interactive_regions(self):
"""Return spatial regions that can be interacted with in the viewer.
This does not return masked regions added via :meth:`load_regions`.
Unsupported region shapes will be skipped. When that happens,
a yellow snackbar message will appear on display.
Returns
-------
regions : dict
Dictionary mapping interactive region names to respective Astropy
``regions`` objects.
"""
regions = {}
failed_regs = set()
# Subset is global, so we just use default viewer.
for lyr in self.default_viewer.layers:
if (not hasattr(lyr, 'layer') or not isinstance(lyr.layer, Subset)
or lyr.layer.ndim not in (2, 3)):
continue
subset_data = lyr.layer
subset_label = subset_data.label
# TODO: Remove this when Jdaviz support round-tripping, see
# https://github.com/spacetelescope/jdaviz/pull/721
if not subset_label.startswith('Subset'):
continue
try:
region = subset_data.data.get_selection_definition(
subset_id=subset_label, format='astropy-regions')
except (NotImplementedError, ValueError):
failed_regs.add(subset_label)
else:
regions[subset_label] = region
if len(failed_regs) > 0:
self.app.hub.broadcast(SnackbarMessage(
f"Regions skipped: {', '.join(sorted(failed_regs))}",
color="warning", timeout=8000, sender=self.app))
return regions
# See https://github.com/glue-viz/glue-jupyter/issues/253
def _apply_interactive_region(self, toolname, from_pix, to_pix):
"""Mimic interactive region drawing.
This is for internal testing only.
"""
self.app.session.edit_subset_mode._mode = NewMode
tool = self.default_viewer.toolbar.tools[toolname]
tool.activate()
tool.interact.brushing = True
tool.interact.selected = [from_pix, to_pix]
tool.interact.brushing = False
self.app.session.edit_subset_mode.edit_subset = None # No overwrite next iteration
# TODO: Make this public API?
def _delete_region(self, subset_label):
"""Delete region given the Subset label."""
all_subset_labels = [s.label for s in self.app.data_collection.subset_groups]
if subset_label not in all_subset_labels:
return
i = all_subset_labels.index(subset_label)
subset_grp = self.app.data_collection.subset_groups[i]
self.app.data_collection.remove_subset_group(subset_grp)
# TODO: Make this public API?
def _delete_all_regions(self):
"""Delete all regions."""
for subset_grp in self.app.data_collection.subset_groups: # should be a copy
self.app.data_collection.remove_subset_group(subset_grp)
def data_has_valid_wcs(data, ndim=None):
"""Check if given glue Data has WCS that is compatible with APE 14."""
status = hasattr(data, 'coords') and isinstance(data.coords, BaseHighLevelWCS)
if ndim is not None:
status = status and data.coords.world_n_dim == ndim
return status
def _next_subset_num(label_prefix, subset_groups):
"""Assumes ``prefix i`` format.
Does not go back and fill in lower but available numbers. This is consistent with Glue.
"""
max_i = 0
for sg in subset_groups:
if sg.label.startswith(label_prefix):
sub_label = sg.label.split(' ')
if len(sub_label) > 1:
i_str = sub_label[-1]
try:
i = int(i_str)
except Exception: # nosec
continue
else:
if i > max_i:
max_i = i
return max_i + 1