from astropy import units as u
from astropy.wcs.wcsapi import BaseHighLevelWCS
from glue.core.link_helpers import LinkSame
from glue.core.message import (
DataCollectionAddMessage, SubsetCreateMessage, SubsetDeleteMessage
)
from glue.core.subset import Subset
from glue.core.subset_group import GroupedSubset
from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink, NoAffineApproximation
from traitlets import List, Unicode, Bool, Dict, observe
from jdaviz.configs.imviz.helper import get_reference_image_data, layer_is_2d
from jdaviz.configs.imviz.wcs_utils import (
get_compass_info, _get_rotated_nddata_from_label
)
from jdaviz.core.custom_traitlets import FloatHandleEmpty
from jdaviz.core.events import (
ExitBatchLoadMessage, ChangeRefDataMessage,
AstrowidgetMarkersChangedMessage, MarkersPluginUpdate,
SnackbarMessage, ViewerAddedMessage, AddDataMessage, LinkUpdatedMessage
)
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (
PluginTemplateMixin, SelectPluginComponent, LayerSelect, ViewerSelectMixin, AutoTextField
)
from jdaviz.core.user_api import PluginUserApi
from jdaviz.utils import _wcs_only_label
__all__ = ['Orientation']
base_wcs_layer_label = 'Default orientation'
link_type_msg_to_trait = {'pixels': 'Pixels', 'wcs': 'WCS'}
[docs]
@tray_registry('imviz-orientation', label="Orientation", viewer_requirements="image")
class Orientation(PluginTemplateMixin, ViewerSelectMixin):
"""
See the :ref:`Orientation Plugin Documentation <imviz-orientation>` for more details.
.. note::
Changing linking after adding markers via
`~jdaviz.core.astrowidgets_api.AstrowidgetsImageViewerMixin.add_markers` is unsupported and
will raise an error requiring resetting the markers manually via
`~jdaviz.core.astrowidgets_api.AstrowidgetsImageViewerMixin.add_markers`
or clicking a button in the plugin first.
Only the following attributes and methods are available through the
:ref:`public plugin API <plugin-apis>`:
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
* ``link_type`` (`~jdaviz.core.template_mixin.SelectPluginComponent`)
* ``wcs_use_affine``
* ``delete_subsets``
* ``viewer``
* ``orientation``
* ``rotation_angle``
* ``east_left``
* ``add_orientation``
"""
template_file = __file__, "orientation.vue"
link_type_items = List().tag(sync=True)
link_type_selected = Unicode().tag(sync=True)
wcs_use_fallback = Bool(True).tag(sync=True)
wcs_use_affine = Bool(True).tag(sync=True)
wcs_linking_available = Bool(False).tag(sync=True)
need_clear_astrowidget_markers = Bool(False).tag(sync=True)
plugin_markers_exist = Bool(False).tag(sync=True)
linking_in_progress = Bool(False).tag(sync=True)
need_clear_subsets = Bool(False).tag(sync=True)
# rotation angle, counterclockwise [degrees]
rotation_angle = FloatHandleEmpty(0).tag(sync=True)
east_left = Bool(True).tag(sync=True) # set convention for east left of north
icons = Dict().tag(sync=True)
viewer_items = List().tag(sync=True)
viewer_selected = Unicode().tag(sync=True)
orientation_layer_items = List().tag(sync=True)
orientation_layer_selected = Unicode().tag(sync=True)
new_layer_label = Unicode().tag(sync=True)
new_layer_label_default = Unicode().tag(sync=True)
new_layer_label_auto = Bool(True).tag(sync=True)
multiselect = Bool(False).tag(sync=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.icons = {k: v for k, v in self.app.state.icons.items()}
self.link_type = SelectPluginComponent(self,
items='link_type_items',
selected='link_type_selected',
manual_options=['Pixels', 'WCS'])
self.orientation = LayerSelect(
self, 'orientation_layer_items', 'orientation_layer_selected', 'viewer_selected',
'multiselect', only_wcs_layers=True
)
self.orientation_layer_label = AutoTextField(
self, 'new_layer_label', 'new_layer_label_default', 'new_layer_label_auto', None
)
self.hub.subscribe(self, DataCollectionAddMessage,
handler=self._on_new_app_data)
self.hub.subscribe(self, ExitBatchLoadMessage,
handler=self._on_new_app_data)
self.hub.subscribe(self, AstrowidgetMarkersChangedMessage,
handler=self._on_astrowidget_markers_changed)
self.hub.subscribe(self, MarkersPluginUpdate,
handler=self._on_markers_plugin_update)
self.hub.subscribe(self, ChangeRefDataMessage,
handler=self._on_refdata_change)
self.hub.subscribe(self, SubsetCreateMessage,
handler=self._on_subset_change)
self.hub.subscribe(self, SubsetDeleteMessage,
handler=self._on_subset_change)
self.hub.subscribe(self, ViewerAddedMessage,
handler=self._on_viewer_added)
self.hub.subscribe(self, AddDataMessage,
handler=self._on_data_add_to_viewer)
self._update_layer_label_default()
@property
def user_api(self):
return PluginUserApi(
self,
expose=(
'link_type', 'wcs_use_affine', 'delete_subsets',
'viewer', 'orientation',
'rotation_angle', 'east_left', 'add_orientation'
)
)
def _link_image_data(self):
self.linking_in_progress = True
try:
link_type = self.link_type.selected.lower()
link_image_data(
self.app,
link_type=link_type,
wcs_fallback_scheme='pixels' if self.wcs_use_fallback else None,
wcs_use_affine=self.wcs_use_affine,
error_on_fail=False)
except Exception: # pragma: no cover
raise
else:
# Only broadcast after success.
self.app.hub.broadcast(LinkUpdatedMessage(
link_type, self.wcs_use_fallback, self.wcs_use_affine, sender=self.app))
finally:
self.linking_in_progress = False
def _check_if_data_with_wcs_exists(self):
for data in self.app.data_collection:
if hasattr(data.coords, 'pixel_to_world'):
self.wcs_linking_available = True
return
self.wcs_linking_available = False
def _on_new_app_data(self, msg):
if self.app._jdaviz_helper._in_batch_load > 0:
return
if isinstance(msg, DataCollectionAddMessage):
components = [str(comp) for comp in msg.data.main_components]
if "ra" in components or "Lon" in components:
# linking currently removes any markers, so we want to skip
# linking immediately after new markers are added.
# Eventually we'll probably want to support linking WITH markers,
# at which point this if-statement should be removed.
return
self._link_image_data()
self._check_if_data_with_wcs_exists()
def _on_astrowidget_markers_changed(self, msg):
self.need_clear_astrowidget_markers = msg.has_markers
def _on_markers_plugin_update(self, msg):
self.plugin_markers_exist = msg.table_length > 0
@observe('link_type_selected', 'wcs_use_fallback', 'wcs_use_affine')
def _update_link(self, msg={}):
"""Run link_image_data with the selected parameters."""
if not hasattr(self, 'link_type'):
# could happen before plugin is fully initialized
return
if msg.get('name', None) == 'wcs_use_affine' and self.link_type.selected == 'Pixels':
# approximation doesn't apply, avoid updating when not necessary!
return
if self.linking_in_progress:
return
self.linking_in_progress = True # Prevent recursion
if self.need_clear_subsets:
self.linking_in_progress = False
raise ValueError("Link type can only be changed after existing subsets "
f"are deleted, but {len(self.app.data_collection.subset_groups)} "
f"subset(s) still exist. To delete them, you can use "
f"`delete_subsets()` from the plugin API.")
if self.need_clear_astrowidget_markers:
setattr(self, msg.get('name'), msg.get('old'))
self.linking_in_progress = False
raise ValueError(f"cannot change linking with markers present (value reverted to "
f"'{msg.get('old')}'), call viewer.reset_markers()")
if self.link_type.selected == 'Pixels':
self.wcs_use_affine = True
self.linking_in_progress = False
self._link_image_data()
# load data into the viewer that are now compatible with the
# new link type, remove data from the viewer that are now
# incompatible:
wcs_linked = self.link_type.selected == 'WCS'
viewer_selected = self.app.get_viewer(self.viewer.selected)
data_in_viewer = self.app.get_viewer(viewer_selected.reference).data()
for data in self.app.data_collection:
is_wcs_only = data.meta.get(_wcs_only_label, False)
has_wcs = hasattr(data.coords, 'pixel_to_world')
if not is_wcs_only:
if data in data_in_viewer and wcs_linked and not has_wcs:
# data is in viewer but must be removed:
self.app.remove_data_from_viewer(viewer_selected.reference, data.label)
self.hub.broadcast(SnackbarMessage(
f"Data '{data.label}' does not have a valid WCS - removing from viewer.",
sender=self, color="warning"))
if wcs_linked:
self._send_wcs_layers_to_all_viewers()
self._update_layer_label_default()
# Clear previous zoom limits because they no longer mean anything.
for v in self.app._viewer_store.values():
v._prev_limits = None
def _on_subset_change(self, msg):
self.need_clear_subsets = len(self.app.data_collection.subset_groups) > 0
[docs]
def delete_subsets(self):
# subsets will be deleted on changing link type:
for subset_group in self.app.data_collection.subset_groups:
self.app.data_collection.remove_subset_group(subset_group)
[docs]
def vue_delete_subsets(self, *args):
self.delete_subsets()
def _get_wcs_angles(self, first_loaded_image=None):
if first_loaded_image is None:
first_loaded_image = self.viewer.selected_obj.first_loaded_data
if first_loaded_image is None:
# These won't end up getting used in this case, but we need an actual number
return 0, 0, 0
degn, dege, flip = get_compass_info(
first_loaded_image.coords, first_loaded_image.shape
)[-3:]
return degn, dege, flip
[docs]
def rotation_angle_deg(self, rotation_angle=None):
if rotation_angle is None:
rotation_angle = self.rotation_angle
if rotation_angle is not None:
if (
(isinstance(rotation_angle, str) and len(rotation_angle)) or
isinstance(rotation_angle, (int, float))
):
return float(rotation_angle) * u.deg
return 0 * u.deg
[docs]
def add_orientation(self, rotation_angle=None, east_left=None, label=None,
set_on_create=True, wrt_data=None):
"""
Add new orientation options.
Parameters
----------
rotation_angle : float, optional
Desired sky orientation angle in degrees.
If `None`, the value will follow ``self.rotation_angle``.
If nothing is set anywhere, it defaults to zero degrees.
east_left : bool, optional
Set to `True` if you want N-up E-left or `False` for N-up E-right.
If `None`, the value will follow ``self.east_left``.
label : str, optional
Data label for this new orientation layer.
If `None`, the value will follow ``self.new_layer_label``.
set_on_create : bool, optional
If `True`, this new orientation layer will become active
on creation. Otherwise, it will be created but stay inactive
in the background.
wrt_data : str, optional
Orientation calculations is done with respect to this data WCS.
If `None`, it grabs the first loaded data in the selected viewer
(may or may not be visible); If no data is loaded in the viewer,
nothing will be done.
"""
self._add_orientation(rotation_angle=rotation_angle, east_left=east_left, label=label,
set_on_create=set_on_create, wrt_data=wrt_data)
def _add_orientation(self, rotation_angle=None, east_left=None, label=None,
set_on_create=True, wrt_data=None, from_ui=False):
if self.link_type_selected != 'WCS':
raise ValueError("must be aligned by WCS to add orientation options")
if wrt_data is None:
# if not specified, use first-loaded image layer as the
# default rotation:
wrt_data = self.viewer.selected_obj.first_loaded_data
if wrt_data is None: # Nothing in viewer
msg = "Viewer must have data loaded to add an orientation."
if from_ui:
self.hub.broadcast(SnackbarMessage(msg, color="error",
timeout=6000, sender=self))
else:
raise ValueError(msg)
return
rotation_angle = self.rotation_angle_deg(rotation_angle)
if east_left is None:
east_left = self.east_left
if label is None:
label = self.new_layer_label
# Default rotation is the same orientation as the original reference data:
degn = self._get_wcs_angles(first_loaded_image=wrt_data)[0]
if east_left:
rotation_angle = -degn * u.deg + rotation_angle
else:
rotation_angle = (180 - degn) * u.deg - rotation_angle
ndd = _get_rotated_nddata_from_label(
app=self.app,
data_label=wrt_data.label,
rotation_angle=rotation_angle,
target_wcs_east_left=east_left,
target_wcs_north_up=True,
)
self.app._jdaviz_helper.load_data(
ndd, data_label=label
)
# add orientation layer to all viewers:
for viewer_ref in self.app._viewer_store:
self._add_data_to_viewer(label, viewer_ref)
if set_on_create:
# Not sure why this is sometimes missing, but we can add it here
if label not in self.orientation.choices:
self.orientation.choices += [label]
self.orientation.selected = label
def _add_data_to_viewer(self, data_label, viewer_id):
viewer = self.app.get_viewer_by_id(viewer_id)
wcs_only_layers = viewer.state.wcs_only_layers
if data_label not in wcs_only_layers:
self.app.add_data_to_viewer(viewer_id, data_label)
def _on_viewer_added(self, msg):
self._send_wcs_layers_to_all_viewers(viewers_to_update=[msg._viewer_id])
@observe('viewer_items')
def _send_wcs_layers_to_all_viewers(self, *args, **kwargs):
if not hasattr(self, 'viewer'):
return
wcs_only_layers = self.app._jdaviz_helper.default_viewer._obj.state.wcs_only_layers
viewers_to_update = kwargs.get(
'viewers_to_update', self.app._viewer_store.keys()
)
for viewer_ref in viewers_to_update:
self.viewer.selected = viewer_ref
self.orientation.update_wcs_only_filter(wcs_only=self.link_type_selected == 'WCS')
for wcs_layer in wcs_only_layers:
if wcs_layer not in self.viewer.selected_obj.layers:
self.app.add_data_to_viewer(viewer_ref, wcs_layer)
if (
self.orientation.selected not in
self.viewer.selected_obj.state.wcs_only_layers and
self.link_type_selected == 'WCS'
):
self.orientation.selected = base_wcs_layer_label
def _on_data_add_to_viewer(self, msg):
all_wcs_only_layers = all(
layer.layer.meta.get(_wcs_only_label)
for layer in self.viewer.selected_obj.layers
if hasattr(layer.layer, "meta")
)
if all_wcs_only_layers and msg.data.meta.get(_wcs_only_label, False):
# on adding first data layer, reset the limits:
self.viewer.selected_obj.state.reset_limits()
[docs]
def vue_add_orientation(self, *args, **kwargs):
self._add_orientation(set_on_create=True, from_ui=True)
@observe('orientation_layer_selected')
def _change_reference_data(self, *args, **kwargs):
if self._refdata_change_available:
self.app._change_reference_data(
self.orientation.selected, viewer_id=self.viewer.selected
)
viewer_item = self.app._viewer_item_by_id(self.viewer.selected)
if viewer_item['reference_data_label'] != self.orientation.selected:
viewer_item['reference_data_label'] = self.orientation.selected
def _on_refdata_change(self, msg):
if hasattr(self, 'viewer'):
ref_data = self.ref_data
viewer = self.viewer.selected_obj
# don't select until reference data are available:
if ref_data is not None:
link_type = viewer.get_link_type(ref_data.label)
if link_type != 'self':
self.link_type_selected = link_type_msg_to_trait[link_type]
elif not len(viewer.data()):
self.link_type_selected = link_type_msg_to_trait['pixels']
if msg.data.label not in self.orientation.choices:
return
if msg.viewer_id == self.viewer.selected:
self.orientation.selected = msg.data.label
# we never want to highlight subsets of pixels within WCS-only layers,
# so if this layer is an ImageSubsetLayerState on a WCS-only layer,
# ensure that it is never visible:
for layer in viewer.state.layers:
if (
hasattr(layer.layer, 'label') and
layer.layer.label.startswith("Subset") and
layer.layer.data.meta.get("_WCS_ONLY", False)
):
layer.visible = False
@property
def ref_data(self):
if hasattr(self, 'viewer'):
return self.app.get_viewer_by_id(self.viewer.selected).state.reference_data
return None
@property
def _refdata_change_available(self):
viewer = self.app.get_viewer(self.viewer.selected)
selected_layer = [lyr.layer for lyr in viewer.layers
if lyr.layer.label == self.orientation.selected]
if len(selected_layer):
is_subset = isinstance(selected_layer[0], (Subset, GroupedSubset))
else:
is_subset = False
return (
len(self.orientation.selected) and
len(self.viewer.selected) and
not is_subset
)
@observe('viewer_selected')
def _on_viewer_change(self, msg={}):
# don't update choices until viewer is available:
ref_data = self.ref_data
if hasattr(self, 'viewer') and ref_data is not None:
if ref_data.label in self.orientation.choices:
self.orientation.selected = ref_data.label
[docs]
def create_north_up_east_left(self, label="North-up, East-left", set_on_create=False,
from_ui=False):
"""
Set the rotation angle and flip to achieve North up and East left
according to the reference image WCS.
"""
if label not in self.orientation.choices:
degn = self._get_wcs_angles()[-3]
self._add_orientation(rotation_angle=degn, east_left=True,
label=label, set_on_create=set_on_create,
from_ui=from_ui)
elif set_on_create:
self.orientation.selected = label
[docs]
def create_north_up_east_right(self, label="North-up, East-right", set_on_create=False,
from_ui=False):
"""
Set the rotation angle and flip to achieve North up and East right
according to the reference image WCS.
"""
if label not in self.orientation.choices:
degn = self._get_wcs_angles()[-3]
self._add_orientation(rotation_angle=180 - degn, east_left=False,
label=label, set_on_create=set_on_create,
from_ui=from_ui)
elif set_on_create:
self.orientation.selected = label
[docs]
def vue_select_north_up_east_left(self, *args, **kwargs):
self.create_north_up_east_left(set_on_create=True, from_ui=True)
[docs]
def vue_select_north_up_east_right(self, *args, **kwargs):
self.create_north_up_east_right(set_on_create=True, from_ui=True)
[docs]
def vue_select_default_orientation(self, *args, **kwargs):
self.orientation.selected = base_wcs_layer_label
@observe('east_left', 'rotation_angle')
def _update_layer_label_default(self, event={}):
self.new_layer_label_default = (
f'CCW {self.rotation_angle_deg():.2f} ' +
('(E-left)' if self.east_left else '(E-right)')
)
def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_affine=True,
error_on_fail=False):
"""(Re)link loaded data in Imviz with the desired link type.
.. note::
Any markers added in Imviz will need to be removed manually before changing linking type.
You can add back the markers using
:meth:`~jdaviz.core.astrowidgets_api.AstrowidgetsImageViewerMixin.add_markers`
for the relevant viewer(s).
Parameters
----------
app : `~jdaviz.app.Application`
Application associated with Imviz, e.g., ``imviz.app``.
link_type : {'pixels', 'wcs'}
Choose to link by pixels or WCS.
wcs_fallback_scheme : {None, 'pixels'}
If WCS linking failed, choose to fall back to linking by pixels or not at all.
This is only used when ``link_type='wcs'``.
Choosing `None` may result in some Imviz functionality not working properly.
wcs_use_affine : bool
Use an affine transform to represent the offset between images if possible
(requires that the approximation is accurate to within 1 pixel with the
full WCS transformations). If approximation fails, it will automatically
fall back to full WCS transformation. This is only used when ``link_type='wcs'``.
Affine approximation is much more performant at the cost of accuracy.
error_on_fail : bool
If `True`, any failure in linking will raise an exception.
If `False`, warnings will be emitted as snackbar messages.
When only warnings are emitted and no links are assigned,
some Imviz functionality may not work properly.
Raises
------
ValueError
Invalid inputs or reference data.
"""
if len(app.data_collection) <= 1 and link_type != 'wcs': # No need to link, we are done.
return
if link_type not in ('pixels', 'wcs'): # pragma: no cover
raise ValueError(f"link_type must be 'pixels' or 'wcs', got {link_type}")
if link_type == 'wcs' and wcs_fallback_scheme not in (None, 'pixels'): # pragma: no cover
raise ValueError("wcs_fallback_scheme must be None or 'pixels', "
f"got {wcs_fallback_scheme}")
if link_type == 'wcs':
at_least_one_data_have_wcs = len([
hasattr(d, 'coords') and isinstance(d.coords, BaseHighLevelWCS)
for d in app.data_collection
]) > 0
if not at_least_one_data_have_wcs: # pragma: no cover
if wcs_fallback_scheme is None:
if error_on_fail:
raise ValueError("link_type can only be 'wcs' when wcs_fallback_scheme "
"is 'None' if at least one image has a valid WCS.")
else:
return
else:
# fall back on pixel linking
link_type = 'pixels'
old_link_type = getattr(app, '_link_type', None)
# In WCS linking, changing orientation layer is done within Orientation plugin,
# so here we assume viewer.state.reference_data is already the desired
# orientation by the time this function is called.
#
# In pixels linking, Affine approximation does not matter.
#
# data1 = reference, data2 = actual data
data_already_linked = []
if (link_type == old_link_type and
(link_type == "pixels" or wcs_use_affine == app._wcs_use_affine)):
# We are only here to link new data with existing configuration,
# so no need to relink existing data.
for link in app.data_collection.external_links:
data_already_linked.append(link.data2)
else: # pragma: no cover
# Everything has to be relinked.
for viewer in app._viewer_store.values():
if len(viewer._marktags):
raise ValueError(f"cannot change link_type (from '{app._link_type}' to "
f"'{link_type}') when markers are present. "
f" Clear markers with viewer.reset_markers() first")
# set internal tracking of link_type before changing reference data for anything that is
# subscribed to a change in reference data
app._link_type = link_type
app._wcs_use_affine = wcs_use_affine
# wcs -> pixels: First loaded real data will be reference.
if link_type == 'pixels' and old_link_type == 'wcs':
# default reference layer is the first-loaded image in default viewer:
refdata = app._jdaviz_helper.default_viewer._obj.first_loaded_data
if refdata is None: # No data in viewer, just use first in collection # pragma: no cover
iref = 0
refdata = app.data_collection[iref]
else:
iref = app.data_collection.index(refdata)
# set default layer to reference data in all viewers:
for viewer_id in app.get_viewer_ids():
app._change_reference_data(refdata.label, viewer_id=viewer_id)
# pixels -> wcs: Always the default orientation
elif link_type == 'wcs' and old_link_type == 'pixels':
# Have to create the default orientation first.
if base_wcs_layer_label not in app.data_collection.labels:
default_reference_layer = (app._jdaviz_helper.default_viewer._obj.first_loaded_data
or app.data_collection[0])
degn = get_compass_info(default_reference_layer.coords, default_reference_layer.shape)[-3] # noqa: E501
# Default rotation is the same orientation as the original reference data:
rotation_angle = -degn * u.deg
ndd = _get_rotated_nddata_from_label(app, default_reference_layer.label, rotation_angle)
app._jdaviz_helper.load_data(ndd, base_wcs_layer_label)
# set default layer to reference data in all viewers:
for viewer_id in app.get_viewer_ids():
app._change_reference_data(base_wcs_layer_label, viewer_id=viewer_id)
refdata = app.data_collection[base_wcs_layer_label]
iref = app.data_collection.index(refdata)
# Re-use current reference data.
else:
refdata, iref = get_reference_image_data(app)
# App just loaded, nothing yet, so take first image.
if refdata is None:
refdata = app._jdaviz_helper.default_viewer._obj.first_loaded_data
if refdata is None: # No data in viewer, just use first in collection
iref = 0
refdata = app.data_collection[iref]
else: # pragma: no cover
iref = app.data_collection.index(refdata)
# With reference data changed, if needed, now we relink as needed.
links_list = []
ids0 = refdata.pixel_component_ids
ndim_range = range(2) # We only support 2D
for i, data in enumerate(app.data_collection):
# 1. Do not link with self.
# 2. We are not touching any existing Subsets or Table. They keep their own links.
# 3. Links already exist for this entry and we're not changing the type.
# 4. We are not touching fake WCS layers in pixel linking.
# 5. We are not touching data without WCS in WCS linking.
if ((i == iref) or (not layer_is_2d(data)) or (data in data_already_linked) or
(link_type == "pixels" and data.meta.get(_wcs_only_label)) or
(link_type == "wcs" and not hasattr(data.coords, 'pixel_to_world'))):
continue
ids1 = data.pixel_component_ids
new_links = []
try:
if link_type == 'pixels':
new_links = [LinkSame(ids0[i], ids1[i]) for i in ndim_range]
else: # wcs
wcslink = WCSLink(data1=refdata, data2=data, cids1=ids0, cids2=ids1)
if wcs_use_affine:
try:
new_links = [wcslink.as_affine_link()]
except NoAffineApproximation: # pragma: no cover
new_links = [wcslink]
else:
new_links = [wcslink]
except Exception as e: # pragma: no cover
if link_type == 'wcs' and wcs_fallback_scheme == 'pixels':
try:
new_links = [LinkSame(ids0[i], ids1[i]) for i in ndim_range]
except Exception as e: # pragma: no cover
if error_on_fail:
raise
else:
app.hub.broadcast(SnackbarMessage(
f"Error linking '{data.label}' to '{refdata.label}': "
f"{repr(e)}", color="warning", timeout=8000, sender=app))
continue
else: # pragma: no cover
if error_on_fail:
raise
else:
app.hub.broadcast(SnackbarMessage(
f"Error linking '{data.label}' to '{refdata.label}': "
f"{repr(e)}", color="warning", timeout=8000, sender=app))
continue
links_list += new_links
if len(links_list) > 0:
with app.data_collection.delay_link_manager_update():
if len(data_already_linked):
app.data_collection.add_link(links_list) # Add to existing
else:
app.data_collection.set_links(links_list) # Redo all links
app.hub.broadcast(SnackbarMessage(
'Images successfully relinked', color='success', timeout=8000, sender=app))
for viewer in app._viewer_store.values():
wcs_linked = link_type == 'wcs'
# viewer-state needs to know link type for reset_limits behavior
viewer.state.linked_by_wcs = wcs_linked
# also need to store a copy in the viewer item for the data dropdown to access
viewer_item = app._get_viewer_item(viewer.reference)
viewer_item['reference_data_label'] = refdata.label
viewer_item['linked_by_wcs'] = wcs_linked
# if changing from one link type to another, reset the limits:
if link_type != old_link_type:
viewer.state.reset_limits()