diff --git a/docs/imviz/displayimages.rst b/docs/imviz/displayimages.rst index 6a7abb8570..5beef47b9a 100644 --- a/docs/imviz/displayimages.rst +++ b/docs/imviz/displayimages.rst @@ -42,7 +42,7 @@ If your *reference data* has GWCS with a bounding box, any coordinates transform outside that bounding box is less reliable. This still applies even when you are looking at some other data that is not the reference data if they are linked by WCS because all transformations in glue go through the reference data. Such a situation -is indicated by "(est.)" and the affected coordinates becoming gray. +is indicated by the affected coordinates becoming gray. If your data of interest also has a GWCS with a bounding box, only the mouseover data where it overlaps with the reference data's diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index 9e0ef2e2f1..4aa299a1f1 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -43,12 +43,14 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): assert mm.results_label_overwrite is True # Make sure coordinate display works - flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(flux_viewer, {'event': 'mousemove', + 'domain': {'x': 0, 'y': 0}}) assert flux_viewer.state.slices == (0, 0, 1) - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+8.00000e+00 Jy' # Slice 0 has 8 pixels, this is Slice 1 # noqa - assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755346' - assert flux_viewer.label_mouseover.world_dec_deg == '27.0000999998' + # Slice 0 has 8 pixels, this is Slice 1 + assert label_mouseover.as_text() == ("Pixel x=00.0 y=00.0 Value +8.00000e+00 Jy", + "World 13h39m59.9461s +27d00m00.3600s (ICRS)", + "204.9997755346 27.0000999998 (deg)") # noqa # Make sure adding it to viewer does not crash. cubeviz_helper.app.add_data_to_viewer( @@ -62,11 +64,12 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): assert dc[1].coords is None # Make sure coordinate display now show moment map info (no WCS) - flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+8.00000e+00 Jy' # Slice 0 has 8 pixels, this is Slice 1 # noqa - assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755346' - assert flux_viewer.label_mouseover.world_dec_deg == '27.0000999998' + label_mouseover._viewer_mouse_event(flux_viewer, {'event': 'mousemove', + 'domain': {'x': 0, 'y': 0}}) + # Slice 0 has 8 pixels, this is Slice 1 # noqa + assert label_mouseover.as_text() == ("Pixel x=00.0 y=00.0 Value +8.00000e+00 Jy", + "World 13h39m59.9461s +27d00m00.3600s (ICRS)", + "204.9997755346 27.0000999998 (deg)") # noqa assert mm.filename == 'moment0_test_FLUX.fits' # Auto-populated on calculate. mm.filename = str(tmpdir.join(mm.filename)) # But we want it in tmpdir for testing. @@ -96,10 +99,11 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): assert dc.external_links[3].cids2[0] == dc[-1].pixel_component_ids[0] # Coordinate display should be unaffected. - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+8.00000e+00 Jy' # Slice 0 has 8 pixels, this is Slice 1 # noqa - assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755346' - assert flux_viewer.label_mouseover.world_dec_deg == '27.0000999998' + label_mouseover._viewer_mouse_event(flux_viewer, {'event': 'mousemove', + 'domain': {'x': 0, 'y': 0}}) + assert label_mouseover.as_text() == ("Pixel x=00.0 y=00.0 Value +8.00000e+00 Jy", + "World 13h39m59.9461s +27d00m00.3600s (ICRS)", + "204.9997755346 27.0000999998 (deg)") # noqa @pytest.mark.filterwarnings('ignore:No observer defined on WCS') diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index 4d4eba8afb..2d520c4913 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -42,14 +42,21 @@ def test_fits_image_hdu_with_microns(image_cube_hdu_obj_microns, cubeviz_helper) assert cubeviz_helper.app.data_collection[i].meta[PRIHDR_KEY]['BITPIX'] == 8 flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') - flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+1.00000e+00 1e-17 erg / (Angstrom cm2 s)' + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(flux_viewer, + {'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert label_mouseover.as_text() == ('Pixel x=00.0 y=00.0 Value +1.00000e+00 1e-17 erg / (Angstrom cm2 s)', # noqa + 'World 13h41m45.5759s +27d00m12.3044s (ICRS)', + '205.4398995981 27.0034178810 (deg)') # noqa unc_viewer = cubeviz_helper.app.get_viewer('uncert-viewer') - unc_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) - assert unc_viewer.label_mouseover.pixel == 'x=-1.0 y=00.0' - assert unc_viewer.label_mouseover.value == '' # Out of bounds + label_mouseover._viewer_mouse_event(unc_viewer, + {'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) + assert label_mouseover.as_text()[0] == 'Pixel x=-1.0 y=00.0' # Out of bounds + # FIXME: remaining lines are unvalidated, + # see https://github.com/spacetelescope/jdaviz/issues/1991 + # 'World 13h41m45.5759s +27d00m12.3044s (ICRS)', + # '205.4398995981 27.0034178810 (deg)') # noqa def test_spectrum1d_with_fake_fixed_units(spectrum1d, cubeviz_helper): @@ -100,18 +107,19 @@ def test_fits_image_hdu_parse_from_file(tmpdir, image_cube_hdu_obj, cubeviz_help assert cubeviz_helper.app.data_collection[i].meta[PRIHDR_KEY]['BITPIX'] == 8 flux_viewer = cubeviz_helper.app.get_viewer(cubeviz_helper._default_flux_viewer_reference_name) - flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+1.00000e+00 1e-17 erg / (Angstrom cm2 s)' - assert flux_viewer.label_mouseover.world_ra_deg == '205.4433848390' - assert flux_viewer.label_mouseover.world_dec_deg == '26.9996149270' + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(flux_viewer, + {'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert label_mouseover.as_text() == ('Pixel x=00.0 y=00.0 Value +1.00000e+00 1e-17 erg / (Angstrom cm2 s)', # noqa + 'World 13h41m46.4124s +26d59m58.6137s (ICRS)', + '205.4433848390 26.9996149270 (deg)') # noqa unc_viewer = cubeviz_helper.app.get_viewer(cubeviz_helper._default_uncert_viewer_reference_name) - unc_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) - assert unc_viewer.label_mouseover.pixel == 'x=-1.0 y=00.0' - assert unc_viewer.label_mouseover.value == '' # Out of bounds - assert unc_viewer.label_mouseover.world_ra_deg == '205.4441642302' - assert unc_viewer.label_mouseover.world_dec_deg == '26.9996148973' + label_mouseover._viewer_mouse_event(unc_viewer, + {'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) + assert label_mouseover.as_text() == ('Pixel x=-1.0 y=00.0', # Out of bounds + 'World 13h41m46.5994s +26d59m58.6136s (ICRS)', + '205.4441642302 26.9996148973 (deg)') # noqa @pytest.mark.filterwarnings('ignore') @@ -128,17 +136,19 @@ def test_spectrum3d_parse(image_cube_hdu_obj, cubeviz_helper): # Same as flux viewer data in test_fits_image_hdu_parse_from_file flux_viewer = cubeviz_helper.app.get_viewer(cubeviz_helper._default_flux_viewer_reference_name) - flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) - assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' - assert flux_viewer.label_mouseover.value == '+1.00000e+00 1e-17 erg / (Angstrom cm2 s)' - assert flux_viewer.label_mouseover.world_ra_deg == '205.4433848390' - assert flux_viewer.label_mouseover.world_dec_deg == '26.9996149270' + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(flux_viewer, + {'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert label_mouseover.as_text() == ('Pixel x=00.0 y=00.0 Value +1.00000e+00 1e-17 erg / (Angstrom cm2 s)', # noqa + 'World 13h41m46.4124s +26d59m58.6137s (ICRS)', + '205.4433848390 26.9996149270 (deg)') # noqa # These viewers have no data. unc_viewer = cubeviz_helper.app.get_viewer(cubeviz_helper._default_uncert_viewer_reference_name) - unc_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) - assert unc_viewer.label_mouseover is None + label_mouseover._viewer_mouse_event(unc_viewer, + {'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) + assert label_mouseover.as_text() == ('', '', '') def test_spectrum3d_no_wcs_parse(cubeviz_helper): @@ -163,8 +173,8 @@ def test_spectrum1d_parse(spectrum1d, cubeviz_helper): assert cubeviz_helper.app.data_collection[0].meta['uncertainty_type'] == 'std' # Coordinate display is only for spatial image, which is missing here. - flux_viewer = cubeviz_helper.app.get_viewer(cubeviz_helper._default_flux_viewer_reference_name) - assert flux_viewer.label_mouseover is None + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + assert label_mouseover.as_text() == ('', '', '') def test_numpy_cube(cubeviz_helper): diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py index d9803bdb28..5ce4867451 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py @@ -50,12 +50,12 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): assert len(spectrum_viewer.data()) == 1 # Check coordinate info panel - flux_viewer.on_mouse_or_key_event( - {'event': 'mousemove', 'domain': {'x': 1, 'y': 1}}) - assert flux_viewer.label_mouseover.pixel == 'x=01.0 y=01.0' - assert flux_viewer.label_mouseover.value == '+1.30000e+01 Jy' - assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755344' - assert flux_viewer.label_mouseover.world_dec_deg == '27.0001999998' + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(flux_viewer, + {'event': 'mousemove', 'domain': {'x': 1, 'y': 1}}) + assert label_mouseover.as_text() == ('Pixel x=01.0 y=01.0 Value +1.30000e+01 Jy', + 'World 13h39m59.9461s +27d00m00.7200s (ICRS)', + '204.9997755344 27.0001999998 (deg)') # Click on spaxel location flux_viewer.toolbar.active_tool.on_mouse_event( @@ -83,12 +83,11 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): assert isinstance(reg2, RectanglePixelRegion) # Make sure coordinate info panel did not change - flux_viewer.on_mouse_or_key_event( - {'event': 'mousemove', 'domain': {'x': 1, 'y': 1}}) - assert flux_viewer.label_mouseover.pixel == 'x=01.0 y=01.0' - assert flux_viewer.label_mouseover.value == '+1.30000e+01 Jy' - assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755344' - assert flux_viewer.label_mouseover.world_dec_deg == '27.0001999998' + label_mouseover._viewer_mouse_event(flux_viewer, + {'event': 'mousemove', 'domain': {'x': 1, 'y': 1}}) + assert label_mouseover.as_text() == ('Pixel x=01.0 y=01.0 Value +1.30000e+01 Jy', + 'World 13h39m59.9461s +27d00m00.7200s (ICRS)', + '204.9997755344 27.0001999998 (deg)') # Make sure linked pan mode works on all image viewers t_linkedpan = flux_viewer.toolbar.tools['jdaviz:simplepanzoommatch'] diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 730edc9ffa..45548b505c 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -1,13 +1,11 @@ -import numpy as np from glue.core import BaseData from glue.core.subset import RoiSubsetState, RangeSubsetState from glue_jupyter.bqplot.image import BqplotImageView from jdaviz.core.registries import viewer_registry from jdaviz.core.marks import SliceIndicatorMarks, ShadowSpatialSpectral -from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.cubeviz.helper import layer_is_cube_image_data -from jdaviz.configs.imviz.helper import data_has_valid_wcs +from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView __all__ = ['CubevizImageView', 'CubevizProfileView'] @@ -45,102 +43,17 @@ def __init__(self, *args, **kwargs): self._subscribe_to_layers_update() self.state.add_callback('reference_data', self._initial_x_axis) - self.label_mouseover = None - self.add_event_callback(self.on_mouse_or_key_event, events=['mousemove', 'mouseenter', - 'mouseleave']) - - def on_mouse_or_key_event(self, data): - + @property + def active_image_layer(self): + """Active image layer in the viewer, if available.""" # Find visible layers visible_layers = [layer for layer in self.state.layers if (layer.visible and layer_is_cube_image_data(layer.layer))] if len(visible_layers) == 0: - return - - if self.label_mouseover is None: - if 'g-coords-info' in self.session.application._tools: - self.label_mouseover = self.session.application._tools['g-coords-info'] - else: - return - - if data['event'] == 'mousemove': - # Display the current cursor coordinates (both pixel and world) as - # well as data values. For now we use the first dataset in the - # viewer for the data values. - - # Extract first dataset from visible layers and use this for coordinates - the choice - # of dataset shouldn't matter if the datasets are linked correctly - active_layer = visible_layers[-1] - image = active_layer.layer - self.label_mouseover.icon = self.jdaviz_app.state.layer_icons.get(active_layer.layer.label) # noqa - - # Extract data coordinates - these are pixels in the reference image - x = data['domain']['x'] - y = data['domain']['y'] - - if x is None or y is None: # Out of bounds - self.label_mouseover.pixel = "" - self.label_mouseover.reset_coords_display() - self.label_mouseover.value = "" - return - - maxsize = int(np.ceil(np.log10(np.max(image.shape[:2])))) + 3 - fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' - self.label_mouseover.pixel = (fmt.format(x, y)) - - # TODO: This assumes data_collection[0] is the main reference - # data for this application. This section will need to be updated - # when that is no longer true. - # Hack to insert WCS for generated 2D and 3D images using FLUX cube WCS. - if 'Plugin' in image.meta: - coo_data = self.jdaviz_app.data_collection[0] - else: - coo_data = image - - # Hack around various WCS propagation issues in Cubeviz. - if '_orig_wcs' in coo_data.meta: - coo = coo_data.meta['_orig_wcs'].pixel_to_world(x, y, self.state.slices[-1])[0].icrs - self.label_mouseover.set_coords(coo) - elif data_has_valid_wcs(coo_data): - try: - coo = coo_data.coords.pixel_to_world(x, y, self.state.slices[-1])[-1].icrs - except Exception: - self.label_mouseover.reset_coords_display() - else: - self.label_mouseover.set_coords(coo) - else: - self.label_mouseover.reset_coords_display() - - # Extract data values at this position. - # Check if shape is [x, y, z] or [y, x] and show value accordingly. - if image.ndim == 3: - ix_shape = 0 - iy_shape = 1 - elif image.ndim == 2: - ix_shape = 1 - iy_shape = 0 - else: # pragma: no cover - raise ValueError(f'Cubeviz does not support ndim={image.ndim}') - - if (-0.5 < x < image.shape[ix_shape] - 0.5 and -0.5 < y < image.shape[iy_shape] - 0.5 - and hasattr(active_layer, 'attribute')): - attribute = active_layer.attribute - arr = image.get_component(attribute).data - unit = image.get_component(attribute).units - if image.ndim == 3: - value = arr[int(round(x)), int(round(y)), self.state.slices[-1]] - else: # 2 - value = arr[int(round(y)), int(round(x))] - self.label_mouseover.value = f'{value:+10.5e} {unit}' - else: - self.label_mouseover.value = '' - - elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': + return None - self.label_mouseover.pixel = "" - self.label_mouseover.reset_coords_display() - self.label_mouseover.value = "" + return visible_layers[-1] def _initial_x_axis(self, *args): # Make sure that the x_att is correct on data load diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py index c6b67fcac6..0e34f9b648 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py @@ -71,35 +71,29 @@ def test_linking_after_spectral_smooth(cubeviz_helper, spectrum1d_cube): # Mouseover should automatically jump from one spectrum # to another, depending on which one is closer. - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) - assert spec_viewer.label_mouseover.pixel == '4.62360e-07, 6.00000e+01' - assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' - assert spec_viewer.label_mouseover.world_ra == '4.62360e-07 m (1 pix)' - assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' - assert spec_viewer.label_mouseover.world_ra_deg == '9.20000e+01 Jy' - assert spec_viewer.label_mouseover.icon == 'a' - - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 20}}) - assert spec_viewer.label_mouseover.pixel == '4.62360e-07, 2.00000e+01' - assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' - assert spec_viewer.label_mouseover.world_ra == '4.62360e-07 m (1 pix)' - assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' - assert spec_viewer.label_mouseover.world_ra_deg == '1.47943e+01 Jy' - assert spec_viewer.label_mouseover.icon == 'b' + label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) + assert label_mouseover.as_text() == ('Cursor 4.62360e-07, 6.00000e+01', + 'Wave 4.62360e-07 m (1 pix)', + 'Flux 9.20000e+01 Jy') + assert label_mouseover.icon == 'a' + + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 20}}) + assert label_mouseover.as_text() == ('Cursor 4.62360e-07, 2.00000e+01', + 'Wave 4.62360e-07 m (1 pix)', + 'Flux 1.47943e+01 Jy') + assert label_mouseover.icon == 'b' # Check mouseover behavior when we hide everything. for lyr in spec_viewer.layers: lyr.visible = False - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) - assert spec_viewer.label_mouseover.pixel == '' - assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' - assert spec_viewer.label_mouseover.world_ra == '' - assert spec_viewer.label_mouseover.world_dec == '' - assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' - assert spec_viewer.label_mouseover.world_ra_deg == '' - assert spec_viewer.label_mouseover.world_dec_deg == '' - assert spec_viewer.label_mouseover.icon == '' + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) + assert label_mouseover.as_text() == ('', '', '') + assert label_mouseover.icon == '' def test_spatial_convolution(cubeviz_helper, spectrum1d_cube): @@ -143,28 +137,23 @@ def test_spectrum1d_smooth(specviz_helper, spectrum1d): # Mouseover should automatically jump from one spectrum # to another, depending on which one is closer. - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6400, 'y': 120}}) - assert spec_viewer.label_mouseover.pixel == '6.40000e+03, 1.20000e+02' - assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' - assert spec_viewer.label_mouseover.world_ra == '6.44444e+03 Angstrom (2 pix)' - assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' - assert spec_viewer.label_mouseover.world_ra_deg == '1.35366e+01 Jy' - assert spec_viewer.label_mouseover.icon == 'a' - - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6400, 'y': 5}}) - assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' - assert spec_viewer.label_mouseover.world_ra == '6.44444e+03 Angstrom (2 pix)' - assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' - assert spec_viewer.label_mouseover.world_ra_deg == '5.34688e+00 Jy' - assert spec_viewer.label_mouseover.icon == 'b' + label_mouseover = specviz_helper.app.session.application._tools['g-coords-info'] + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 6400, 'y': 120}}) + assert label_mouseover.as_text() == ('Cursor 6.40000e+03, 1.20000e+02', + 'Wave 6.44444e+03 Angstrom (2 pix)', + 'Flux 1.35366e+01 Jy') + assert label_mouseover.icon == 'a' + + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 6400, 'y': 5}}) + assert label_mouseover.as_text() == ('Cursor 6.40000e+03, 5.00000e+00', + 'Wave 6.44444e+03 Angstrom (2 pix)', + 'Flux 5.34688e+00 Jy') + assert label_mouseover.icon == 'b' # Out-of-bounds shows nothing. - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 5500, 'y': 120}}) - assert spec_viewer.label_mouseover.pixel == '' - assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' - assert spec_viewer.label_mouseover.world_ra == '' - assert spec_viewer.label_mouseover.world_dec == '' - assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' - assert spec_viewer.label_mouseover.world_ra_deg == '' - assert spec_viewer.label_mouseover.world_dec_deg == '' - assert spec_viewer.label_mouseover.icon == '' + label_mouseover._viewer_mouse_event(spec_viewer, + {'event': 'mousemove', 'domain': {'x': 5500, 'y': 120}}) + assert label_mouseover.as_text() == ('', '', '') + assert label_mouseover.icon == '' diff --git a/jdaviz/configs/default/plugins/viewers.py b/jdaviz/configs/default/plugins/viewers.py index b54fb866c0..2467e19b73 100644 --- a/jdaviz/configs/default/plugins/viewers.py +++ b/jdaviz/configs/default/plugins/viewers.py @@ -6,6 +6,7 @@ from glue_jupyter.bqplot.scatter.layer_artist import BqplotScatterLayerState from glue_jupyter.table import TableViewer +from jdaviz.configs.imviz.helper import layer_is_image_data from jdaviz.components.toolbar_nested import NestedJupyterToolbar from jdaviz.core.registries import viewer_registry from jdaviz.utils import ColorCycler @@ -185,6 +186,18 @@ def _on_subset_create(self, msg): if msg.subset.label not in self._expected_subset_layers and msg.subset.label: self._expected_subset_layers.append(msg.subset.label) + @property + def active_image_layer(self): + """Active image layer in the viewer, if available.""" + # Find visible layers + visible_layers = [layer for layer in self.state.layers + if (layer.visible and layer_is_image_data(layer.layer))] + + if len(visible_layers) == 0: + return None + + return visible_layers[-1] + def initialize_toolbar(self, default_tool_priority=[]): # NOTE: this overrides glue_jupyter.IPyWidgetView self.toolbar = NestedJupyterToolbar(self, self.tools_nested, default_tool_priority) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 4a1694db81..51d79af308 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -1,6 +1,18 @@ +import math +import numpy as np from traitlets import Bool, Unicode +from astropy import units as u +from glue.core import BaseData +from glue.core.subset import RoiSubsetState +from glue.core.subset_group import GroupedSubset + +from jdaviz.configs.cubeviz.plugins.viewers import CubevizImageView +from jdaviz.configs.imviz.helper import data_has_valid_wcs +from jdaviz.configs.imviz.plugins.viewers import ImvizImageView +from jdaviz.configs.mosviz.plugins.viewers import MosvizImageView, MosvizProfile2DView from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView +from jdaviz.core.events import ViewerAddedMessage from jdaviz.core.registries import tool_registry from jdaviz.core.template_mixin import TemplateMixin from jdaviz.core.marks import PluginScatter @@ -12,23 +24,47 @@ class CoordsInfo(TemplateMixin): template_file = __file__, "coords_info.vue" icon = Unicode("").tag(sync=True) - pixel_prefix = Unicode("Pixel").tag(sync=True) - pixel = Unicode("").tag(sync=True) - value = Unicode("").tag(sync=True) - world_label_prefix = Unicode("\u00A0").tag(sync=True) - world_label_prefix_2 = Unicode("\u00A0").tag(sync=True) - world_label_icrs = Unicode("\u00A0").tag(sync=True) - world_label_deg = Unicode("\u00A0").tag(sync=True) - world_ra = Unicode("").tag(sync=True) - world_dec = Unicode("").tag(sync=True) - world_ra_deg = Unicode("").tag(sync=True) - world_dec_deg = Unicode("").tag(sync=True) - unreliable_world = Bool(False).tag(sync=True) - unreliable_pixel = Bool(False).tag(sync=True) + + row1a_title = Unicode("").tag(sync=True) + row1a_text = Unicode("").tag(sync=True) + row1b_title = Unicode("").tag(sync=True) + row1b_text = Unicode("").tag(sync=True) + row1_unreliable = Bool(False).tag(sync=True) + + row2_title = Unicode("").tag(sync=True) + row2_text = Unicode("").tag(sync=True) + row2_unreliable = Bool(False).tag(sync=True) + + row3_title = Unicode("").tag(sync=True) + row3_text = Unicode("").tag(sync=True) + row3_unreliable = Bool(False).tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._marks = {} + self._x, self._y = None, None # latest known cursor positions + + # subscribe/unsubscribe to mouse events across all existing viewers + for viewer in self.app._viewer_store.values(): + self._create_viewer_callbacks(viewer) + + # subscribe to mouse events on any new viewers + self.hub.subscribe(self, ViewerAddedMessage, handler=self._on_viewer_added) + + def _create_viewer_callbacks(self, viewer): + if isinstance(viewer, + (SpecvizProfileView, + ImvizImageView, + CubevizImageView, + MosvizImageView, + MosvizProfile2DView)): + callback = self._viewer_callback(viewer, self._viewer_mouse_event) + viewer.add_event_callback(callback, events=['mousemove', 'mouseleave', 'mouseenter']) + + viewer.state.add_callback('layers', lambda msg: self._layers_changed(viewer)) + + def _on_viewer_added(self, msg): + self._create_viewer_callbacks(self.app.get_viewer_by_id(msg.viewer_id)) @property def marks(self): @@ -49,41 +85,284 @@ def marks(self): viewer.figure.marks = viewer.figure.marks + [self._marks[id]] return self._marks + def as_text(self): + return (f"{self.row1a_title} {self.row1a_text} {self.row1b_title} {self.row1b_text}".strip(), # noqa + f"{self.row2_title} {self.row2_text}".strip(), + f"{self.row3_title} {self.row3_text}".strip()) + def reset_coords_display(self): - self.pixel_prefix = "Pixel" - self.world_label_prefix = '\u00A0' - self.world_label_prefix_2 = '\u00A0' - self.world_label_icrs = '\u00A0' - self.world_label_deg = '\u00A0' - self.world_ra = '' - self.world_dec = '' - self.world_ra_deg = '' - self.world_dec_deg = '' - self.unreliable_world = False - self.unreliable_pixel = False - - def set_coords(self, sky, unreliable_world=False, unreliable_pixel=False): - celestial_coordinates = sky.to_string('hmsdms', precision=4, pad=True).split() - celestial_coordinates_deg = sky.to_string('decimal', precision=10, pad=True).split() - world_ra = celestial_coordinates[0] - world_dec = celestial_coordinates[1] - world_ra_deg = celestial_coordinates_deg[0] - world_dec_deg = celestial_coordinates_deg[1] - - if "nan" in (world_ra, world_dec, world_ra_deg, world_dec_deg): - self.reset_coords_display() - else: - self.pixel_prefix = 'Pixel' - self.world_label_prefix = 'World' - self.world_label_icrs = '(ICRS)' - self.world_label_deg = '(deg)' - self.world_ra = world_ra - self.world_dec = world_dec - self.world_ra_deg = world_ra_deg - self.world_dec_deg = world_dec_deg - self.unreliable_world = unreliable_world - self.unreliable_pixel = unreliable_pixel - if unreliable_world: - self.world_label_prefix_2 = '(est.)' + self.row1a_title = '\u00A0' # to force empty line if no other content + self.row1a_text = "" + self.row1b_title = "" + self.row1b_text = "" + self.row1_unreliable = False + + self.row2_title = '\u00A0' + self.row2_text = "" + self.row2_unreliable = False + + self.row3_title = '\u00A0' + self.row3_text = "" + self.row3_unreliable = False + + self.icon = "" + + def _viewer_mouse_clear_event(self, viewer, data=None): + self.reset_coords_display() + marks = self.marks.get(viewer._reference_id) + if marks is not None: + marks.visible = False + + def _viewer_mouse_event(self, viewer, data): + if data['event'] in ('mouseleave', 'mouseenter'): + self._viewer_mouse_clear_event(viewer, data) + return + + if len(self.app.data_collection) < 1: + self._viewer_mouse_clear_event(viewer) + return + + # otherwise a mousemove event, we need to get cursor coordinates and update the display + + # Extract data coordinates - these are pixels in the reference image + x = data['domain']['x'] + y = data['domain']['y'] + + if x is None or y is None: # Out of bounds + self._viewer_mouse_clear_event(viewer) + return + + # update last known cursor position (so another event like a change in layers can update + # the coordinates with the last known position) + self._x, self._y = x, y + self.update_display(viewer, x=x, y=y) + + def _layers_changed(self, viewer): + if self._x is None or self._y is None: + return + + # update display for a (possible) change to the active layer based on the last known + # cursor position + self.update_display(viewer, self._x, self._y) + + def update_display(self, viewer, x, y): + if isinstance(viewer, SpecvizProfileView): + self._spectrum_viewer_update(viewer, x, y) + elif isinstance(viewer, + (ImvizImageView, CubevizImageView, MosvizImageView, MosvizProfile2DView)): + self._image_viewer_update(viewer, x, y) + + def _image_viewer_update(self, viewer, x, y): + # Display the current cursor coordinates (both pixel and world) as + # well as data values. For now we use the first dataset in the + # viewer for the data values. + + # Extract first dataset from visible layers and use this for coordinates - the choice + # of dataset shouldn't matter if the datasets are linked correctly + active_layer = viewer.active_image_layer + if active_layer is None: + self._viewer_mouse_clear_event(viewer) + return + + image = active_layer.layer + self.icon = self.app.state.layer_icons.get(image.label, '') # noqa + + unreliable_pixel, unreliable_world = False, False + + # separate logic for each viewer type, ultimately needs to result in extracting sky coords + if isinstance(viewer, ImvizImageView): + x, y, coords_status, (unreliable_world, unreliable_pixel) = viewer._get_real_xy(image, x, y) # noqa + if coords_status: + try: + sky = image.coords.pixel_to_world(x, y).icrs + except Exception: # WCS might not be celestial + coords_status = False + self.reset_coords_display() + + elif isinstance(viewer, CubevizImageView): + # TODO: This assumes data_collection[0] is the main reference + # data for this application. This section will need to be updated + # when that is no longer true. + # Hack to insert WCS for generated 2D and 3D images using FLUX cube WCS. + if 'Plugin' in image.meta: + coo_data = self.app.data_collection[0] else: - self.world_label_prefix_2 = '\u00A0' + coo_data = image + + # Hack around various WCS propagation issues in Cubeviz. + if '_orig_wcs' in coo_data.meta: + sky = coo_data.meta['_orig_wcs'].pixel_to_world(x, y, viewer.state.slices[-1])[0].icrs # noqa + coords_status = True + elif data_has_valid_wcs(coo_data): + try: + sky = coo_data.coords.pixel_to_world(x, y, viewer.state.slices[-1])[-1].icrs + except Exception: + coords_status = False + else: + coords_status = True + else: # pragma: no cover + self.reset_coords_display() + + elif isinstance(viewer, MosvizImageView): + + if data_has_valid_wcs(image, ndim=2): + try: + sky = image.coords.pixel_to_world(x, y).icrs + except Exception: # WCS might not be celestial # pragma: no cover + coords_status = False + else: + coords_status = True + else: # pragma: no cover + self.reset_coords_display() + + elif isinstance(viewer, MosvizProfile2DView): + coords_status = False + + if coords_status: + celestial_coordinates = sky.to_string('hmsdms', precision=4, pad=True).split() + celestial_coordinates_deg = sky.to_string('decimal', precision=10, pad=True).split() + world_ra = celestial_coordinates[0] + world_dec = celestial_coordinates[1] + world_ra_deg = celestial_coordinates_deg[0] + world_dec_deg = celestial_coordinates_deg[1] + + if "nan" in (world_ra, world_dec, world_ra_deg, world_dec_deg): + self.reset_coords_display() + + self.row2_title = 'World' + self.row2_text = f'{world_ra} {world_dec} (ICRS)' + self.row2_unreliable = unreliable_world + self.row3_title = '' + self.row3_text = f'{world_ra_deg} {world_dec_deg} (deg)' + self.row3_unreliable = unreliable_world + else: + self.row2_title = '\u00A0' + self.row2_text = "" + self.row2_unreliable = False + + self.row3_title = '\u00A0' + self.row3_text = "" + self.row3_unreliable = False + + maxsize = int(np.ceil(np.log10(np.max(image.shape)))) + 3 + fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' + self.row1a_title = 'Pixel' + self.row1a_text = (fmt.format(x, y)) + self.row1_unreliable = unreliable_pixel + + # Extract data values at this position. + # TODO: for now we just use the first visible layer but we should think + # of how to display values when multiple datasets are present. + + # Extract data values at this position. + # Check if shape is [x, y, z] or [y, x] and show value accordingly. + if image.ndim == 3: + # needed for cubeviz + ix_shape = 0 + iy_shape = 1 + elif image.ndim == 2: + ix_shape = 1 + iy_shape = 0 + else: # pragma: no cover + raise ValueError(f'does not support ndim={image.ndim}') + + if (-0.5 < x < image.shape[ix_shape] - 0.5 and -0.5 < y < image.shape[iy_shape] - 0.5 + and hasattr(active_layer, 'attribute')): + attribute = active_layer.attribute + if isinstance(viewer, (ImvizImageView, MosvizImageView, MosvizProfile2DView)): + value = image.get_data(attribute)[int(round(y)), int(round(x))] + unit = image.get_component(attribute).units + elif isinstance(viewer, CubevizImageView): + arr = image.get_component(attribute).data + unit = image.get_component(attribute).units + if image.ndim == 3: + value = arr[int(round(x)), int(round(y)), viewer.state.slices[-1]] + else: # 2 + value = arr[int(round(y)), int(round(x))] + self.row1b_title = 'Value' + self.row1b_text = f'{value:+10.5e} {unit}' + else: + self.row1b_title = '' + self.row1b_text = '' + + def _spectrum_viewer_update(self, viewer, x, y): + self.row1a_title = 'Cursor' + self.row1a_text = f'{x:10.5e}, {y:10.5e}' + + # show the locked marker/coords only if either no tool or the default tool is active + locking_active = viewer.toolbar.active_tool_id in viewer.toolbar.default_tool_priority + [None] # noqa + if not locking_active: + self.row2_title = '\u00A0' + self.row2_text = '' + self.row3_title = '\u00A0' + self.row3_text = '' + self.icon = '' + self.marks[viewer._reference_id].visible = False + return + + # Snap to the closest data point, not the actual mouse location. + sp = None + closest_i = None + closest_wave = None + closest_flux = None + closest_icon = '' + closest_distance = None + for lyr in viewer.state.layers: + if not lyr.visible: + continue + if isinstance(lyr.layer, GroupedSubset): + if not isinstance(lyr.layer.subset_state, RoiSubsetState): + # then this is a SPECTRAL subset + continue + elif ((not isinstance(lyr.layer, BaseData)) or (lyr.layer.ndim not in (1, 3)) + or (not lyr.visible)): + continue + + try: + # Cache should have been populated when spectrum was first plotted. + # But if not (maybe user changed statistic), we cache it here too. + statistic = getattr(viewer.state, 'function', None) + cache_key = (lyr.layer.label, statistic) + if cache_key in self.app._get_object_cache: + sp = self.app._get_object_cache[cache_key] + else: + sp = self.app.get_data_from_viewer('spectrum-viewer', lyr.layer.label) + self.app._get_object_cache[cache_key] = sp + + # Out of range in spectral axis. + if x < sp.spectral_axis.value.min() or x > sp.spectral_axis.value.max(): + continue + + cur_i = np.argmin(abs(sp.spectral_axis.value - x)) + cur_wave = sp.spectral_axis[cur_i] + cur_flux = sp.flux[cur_i] + + dx = cur_wave.value - x + dy = cur_flux.value - y + cur_distance = math.sqrt(dx * dx + dy * dy) + if (closest_distance is None) or (cur_distance < closest_distance): + closest_distance = cur_distance + closest_i = cur_i + closest_wave = cur_wave + closest_flux = cur_flux + closest_icon = self.app.state.layer_icons.get(lyr.layer.label) + except Exception: # nosec + # Something is loaded but not the right thing + continue + + if closest_wave is None: + self._viewer_mouse_clear_event(viewer) + return + + self.row2_title = 'Wave' + self.row2_text = f'{closest_wave.value:10.5e} {closest_wave.unit.to_string()}' + if closest_wave.unit != u.pix: + self.row2_text += f' ({int(closest_i)} pix)' + + self.row3_title = 'Flux' + self.row3_text = f'{closest_flux.value:10.5e} {closest_flux.unit.to_string()}' + + self.icon = closest_icon + + self.marks[viewer._reference_id].update_xy([closest_wave.value], [closest_flux.value]) # noqa + self.marks[viewer._reference_id].visible = True diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue index fbdc8718ab..73bbb8cd4f 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue @@ -1,27 +1,23 @@