text: import os
  import re
  import warnings
  from copy import deepcopy
  
  import numpy as np
  from astropy.wcs.wcsapi import BaseHighLevelWCS
  from glue.core import BaseData
  from glue.core.link_helpers import LinkSame
  from glue.core.subset import Subset, MaskSubsetState
  from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink, NoAffineApproximation
  
  from jdaviz.core.events import SnackbarMessage, NewViewerMessage
  from jdaviz.core.helpers import ConfigHelper
  
  __all__ = ['Imviz', 'link_image_data']
  
  
  class Imviz(ConfigHelper):
      """Imviz Helper class."""
      _default_configuration = 'imviz'
  
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self._default_viewer = self.app.get_viewer('imviz-0')
  
      @property
      def default_viewer(self):
          """Default viewer instance. This is typically the first viewer ("imviz-0")."""
          return self._default_viewer
  
      def create_image_viewer(self, viewer_name=None):
          """Create a new image viewer.
  
          To display data in this new viewer programmatically,
          first get the new viewer ID from the small tab on the top
          left of viewer display. Then, use
          :meth:`~jdaviz.app.Application.add_data_to_viewer` from ``imviz.app``
          by passing in the new viewer ID and the desired data label,
          once per dataset you wish to display.
  
          Alternately, you can also display data interactively via the GUI.
  
          Parameters
          ----------
          viewer_name : str or `None`
              Viewer name/ID to use. If `None`, it is auto-generated.
  
          Returns
          -------
          viewer : `~jdaviz.configs.imviz.plugins.viewers.ImvizImageView`
              Image viewer instance.
  
          """
          from jdaviz.configs.imviz.plugins.viewers import ImvizImageView
  
          # Cannot assign data to real Data because it loads but it will
          # not update checkbox in Data menu.
          return self.app._on_new_viewer(
              NewViewerMessage(ImvizImageView, data=None, sender=self.app),
              vid=viewer_name, name=viewer_name)
  
      def destroy_viewer(self, viewer_id):
          """Destroy a viewer associated with the given ID.
  
          Raises
          ------
          ValueError
              Default viewer cannot be destroyed.
  
          """
          if viewer_id not in self.app._viewer_store:  # Silent no-op
              return
          if viewer_id == 'imviz-0':
              raise ValueError(f"Default viewer '{viewer_id}' cannot be destroyed")
          self.app.vue_destroy_viewer_item(viewer_id)
  
      def load_data(self, data, parser_reference=None, **kwargs):
          """Load data into Imviz.
  
          Parameters
          ----------
          data : obj or str
              File name or object to be loaded. Supported formats include:
  
              * ``'filename.fits'`` (or any extension that ``astropy.io.fits``
                supports; first image extension found is loaded unless ``ext``
                keyword is also given)
              * ``'filename.fits[SCI]'`` (loads only first SCI extension)
              * ``'filename.fits[SCI,2]'`` (loads the second SCI extension)
              * ``'filename.jpg'`` (requires ``scikit-image``; grayscale only)
              * ``'filename.png'`` (requires ``scikit-image``; grayscale only)
              * JWST ASDF-in-FITS file (requires ``asdf`` and ``gwcs``; ``data`` or given
                ``ext`` + GWCS)
              * `~astropy.io.fits.HDUList` object (first image extension found
                is loaded unless ``ext`` keyword is also given)
              * `~astropy.io.fits.ImageHDU` object
              * `~astropy.nddata.NDData` object (2D only but may have unit,
                mask, or uncertainty attached)
              * Numpy array (2D only)
  
          parser_reference
              This is used internally by the app.
  
          kwargs : dict
              Extra keywords to be passed into app-level parser.
              The only one you might call directly here is ``ext`` (any FITS
              extension format supported by `astropy.io.fits`) and
              ``show_in_viewer`` (bool).
  
          Notes
          -----
          When loading image formats that support RGB color like JPG or PNG, the
          files are converted to greyscale. This is done following the algorithm
          of :func:`skimage.color.rgb2grey`, which involves weighting the channels as
          ``0.2125 R + 0.7154 G + 0.0721 B``. If you prefer a different weighting,
          you can use :func:`skimage.io.imread` to produce your own greyscale
          image as Numpy array and load the latter instead.
          """
          if isinstance(data, str):
              filelist = data.split(',')
  
              if len(filelist) > 1 and 'data_label' in kwargs:
                  raise ValueError('Do not manually overwrite data_label for '
                                   'a list of images')
  
              for data in filelist:
                  kw = deepcopy(kwargs)
                  filepath, ext, data_label = split_filename_with_fits_ext(data)
  
                  # This, if valid, will overwrite input.
                  if ext is not None:
                      kw['ext'] = ext
  
                  # This will only overwrite if not provided.
                  if 'data_label' not in kw:
                      kw['data_label'] = data_label
  
                  self.app.load_data(
                      filepath, parser_reference=parser_reference, **kw)
  
          else:
              self.app.load_data(
                  data, parser_reference=parser_reference, **kwargs)
  
      def link_data(self, **kwargs):
          """(Re)link loaded data in Imviz with the desired link type.
          All existing links will be replaced.
  
          See :func:`~jdaviz.configs.imviz.helper.link_image_data`
          for available keyword options and more details.
          """
          link_image_data(self.app, **kwargs)
  
      def load_static_regions(self, regions, **kwargs):
          """Load given region(s) into the viewer.
          Region(s) is relative to the reference image.
          Once loaded, the region(s) cannot be modified.
  
          Parameters
          ----------
          regions : dict
              Dictionary mapping desired region name to one of the following:
  
              * Astropy ``regions`` object
              * ``photutils`` apertures (limited support until ``photutils``
                fully supports ``regions``)
              * Numpy boolean array (shape must match data)
  
              Region name that starts with "Subset" is forbidden and reserved
              for internal use only.
  
          kwargs : dict
              Extra keywords to be passed into the region's ``to_mask`` method.
              This is ignored if Numpy array is given.
  
          """
          # Subset is global, so we just use default viewer.
          data = self.default_viewer.state.reference_data
  
          for subset_label, region in regions.items():
              if subset_label.startswith('Subset'):
                  warnings.warn(f'{subset_label} is not allowed, skipping. '
                                'Do not use region name that starts with Subset.')
                  continue
  
              if hasattr(region, 'to_pixel'):
                  if data_has_valid_wcs(data):
                      pixreg = region.to_pixel(data.coords)
                      mask = pixreg.to_mask(**kwargs)
                      im = mask.to_image(data.shape)
                  else:
                      warnings.warn(f'{region} given but data has no valid WCS, skipping')
                      continue
              elif hasattr(region, 'to_mask'):
                  mask = region.to_mask(**kwargs)
                  im = mask.to_image(data.shape)
              elif (isinstance(region, np.ndarray) and region.shape == data.shape
                      and region.dtype == np.bool_):
                  im = region
              else:
                  warnings.warn(f'Unsupported region type: {type(region)}, skipping')
                  continue
  
              # NOTE: Region creation info is thus lost.
              state = MaskSubsetState(im, data.pixel_component_ids)
              self.app.data_collection.new_subset_group(subset_label, state)
  
      def get_interactive_regions(self):
          """Return regions interactively drawn in the viewer.
          This does not return regions added via :meth:`load_static_regions`.
  
          Unsupported region shapes will be skipped. When that happens,
          a red 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 != 2):
                  continue
  
              subset_data = lyr.layer
              subset_label = subset_data.label
  
              # TODO: Remove this when Imviz 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:
                  failed_regs.add(subset_label)
              else:
                  regions[subset_label] = region
  
          if len(failed_regs) > 0:
              self.app.hub.broadcast(SnackbarMessage(
                  f"Failed to get regions: {', '.join(failed_regs)}",
                  color="error", 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.
          """
          tool = self.default_viewer.toolbar.tools[toolname]
          tool.activate()
          tool.interact.brushing = True
          tool.interact.selected = [from_pix, to_pix]
          tool.interact.brushing = False
  
      # 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 split_filename_with_fits_ext(filename):
      """Split a ``filename[ext]`` input into filename and FITS extension.
  
      Parameters
      ----------
      filename : str
          Can be a plain filename or ``filename[ext]``. The latter is a form
          of input that is commonly used by DS9. Example values:
  
          * ``'myimage.fits'``
          * ``'myimage.fits[SCI]'`` (assumes ``EXTVER=1``)
          * ``'myimage.fits[SCI,1]'``
  
      Returns
      -------
      filepath : str
          Path to the file, without extension.
  
      ext : str, tuple, or `None`
          FITS extension, if given. Examples: ``'SCI'`` or ``('SCI', 1)``
  
      data_label : str
          Human-readable data label for Glue. Extension info will be added
          later in the parser.
  
      """
      s = os.path.splitext(filename)
      ext_match = re.match(r'(.+)\[(.+)\]', s[1])
      if ext_match is None:
          sfx = s[1]
          ext = None
      else:
          sfx = ext_match.group(1)
          ext = ext_match.group(2)
          if ',' in ext:
              ext = ext.split(',')
              ext[1] = int(ext[1])
              ext = tuple(ext)
          elif not re.match(r'\D+', ext):
              ext = int(ext)
  
      filepath = f'{s[0]}{sfx}'
      data_label = os.path.basename(s[0])
  
      return filepath, ext, data_label
  
  
  def data_has_valid_wcs(data):
      return hasattr(data, 'coords') and isinstance(data.coords, BaseHighLevelWCS)
  
  
  def layer_is_image_data(layer):
      return isinstance(layer, BaseData) and layer.ndim == 2
  
  
  def get_top_layer_index(viewer):
      """Get index of the top visible image layer in Imviz.
      This is because when blinked, first layer might not be top visible layer.
  
      """
      return [i for i, lyr in enumerate(viewer.layers)
              if lyr.visible and layer_is_image_data(lyr.layer)][-1]
  
  
  def get_reference_image_data(app):
      """Return the first 2D image data in collection and its index to use as reference."""
      refdata = None
      iref = 0
      for i, data in enumerate(app.data_collection):
          if layer_is_image_data(data):
              iref = i
              refdata = data
              break
      if refdata is None:
          raise ValueError(f'No valid reference data found in collection: {app.data_collection}')
      return refdata, iref
  
  
  def link_image_data(app, link_type='pixels', wcs_fallback_scheme='pixels', wcs_use_affine=True,
                      error_on_fail=False):
      """(Re)link loaded data in Imviz with the desired link type.
      All existing links will be replaced.
  
      .. warning::
  
          Any markers added in Imviz would be removed automatically.
          You can add back the markers using
          :meth:`~jdaviz.core.astrowidgets_api.AstrowidgetsImageViewerMixin.add_markers`
          for the relevant viewer(s). During the markers removal, pan/zoom will also reset.
  
      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:  # No need to link, we are done.
          return
  
      if link_type not in ('pixels', 'wcs'):
          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'):
          raise ValueError("wcs_fallback_scheme must be None or 'pixels', "
                           f"got {wcs_fallback_scheme}")
  
      # TODO: If different viewers have the same _marktags key, the key actually
      #       points to the same Data table. Subsequent attempt to remove the
      #       same key in this loop will emit warning.
      # Clear any existing markers. Otherwise, re-linking will crash.
      # Imviz can have multiple viewers open at the same time and each
      # tracks their own markers.
      for viewer in app._viewer_store.values():
          viewer.reset_markers()
  
      refdata, iref = get_reference_image_data(app)
      links_list = []
      ids0 = refdata.pixel_component_ids
      ndim_range = range(refdata.ndim)
  
      for i, data in enumerate(app.data_collection):
          # Do not link with self
          if i == iref:
              continue
  
          # We are not touching any existing Subsets. They keep their own links.
          if not layer_is_image_data(data):
              continue
  
          ids1 = data.pixel_component_ids
          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:
              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:
                  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():
              app.data_collection.set_links(links_list)
          app.hub.broadcast(SnackbarMessage(
              'Images successfully relinked', color='success', timeout=8000, sender=app))
  