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..64bda12fb1 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -42,14 +42,19 @@ 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() == ('Pixel x=-1.0 y=00.0', + 'World 13h41m45.5759s +27d00m12.3044s (ICRS)', + '205.4398995981 27.0034178810 (deg)') # noqa def test_spectrum1d_with_fake_fixed_units(spectrum1d, cubeviz_helper): @@ -100,18 +105,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', + 'World 13h41m46.5994s +26d59m58.6136s (ICRS)', + '205.4441642302 26.9996148973 (deg)') # noqa @pytest.mark.filterwarnings('ignore') @@ -128,17 +134,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 +171,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..c9218c861a 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,16 @@ 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): # 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 f5b4cd7ce7..cebd9a6e4a 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 @@ -68,35 +68,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): @@ -137,28 +131,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..9cbce27b0d 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,17 @@ 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): + # 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..8b52b01bff 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,275 @@ 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']: + return self._viewer_mouse_clear_event(viewer, data) + + if len(self.app.data_collection) < 1: + return self._viewer_mouse_clear_event(viewer) + + # 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 + return self._viewer_mouse_clear_event(viewer) + + # 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 + + 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 @@