diff --git a/jdaviz/app.py b/jdaviz/app.py index f1b8763835..f9907a8afc 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -420,13 +420,8 @@ def _on_layers_changed(self, msg): } def _on_refdata_changed(self, msg): - is_wcs_only = ( - msg.old.meta.get('WCS-ONLY', False) or - msg.new.meta.get('WCS-ONLY', False) - ) - - if not is_wcs_only: - return + old_is_wcs_only = msg.old.meta.get('WCS-ONLY', False) + new_is_wcs_only = msg.new.meta.get('WCS-ONLY', False) wcs_only_refdata_icon = 'mdi-compass-outline' wcs_only_not_refdata_icon = 'mdi-compass-off-outline' @@ -438,11 +433,11 @@ def switch_icon(old_icon, new_icon): new_layer_icons = {} for i, (layer_name, layer_icon) in enumerate(self.state.layer_icons.items()): - if layer_name == msg.old.label: + if layer_name == msg.old.label and old_is_wcs_only: new_layer_icons[layer_name] = switch_icon( layer_icon, wcs_only_not_refdata_icon ) - elif layer_name == msg.new.label: + elif layer_name == msg.new.label and new_is_wcs_only: new_layer_icons[layer_name] = switch_icon( layer_icon, wcs_only_refdata_icon ) @@ -461,7 +456,7 @@ def _change_reference_data(self, new_refdata_label): viewer_reference = self._get_first_viewer_reference_name() viewer = self.get_viewer(viewer_reference) - old_refdata = viewer.state.reference_data + old_refdata = self._viewer_store[viewer_reference].state.reference_data if new_refdata_label == old_refdata.label: # if there's no refdata change, don't do anything: @@ -472,8 +467,6 @@ def _change_reference_data(self, new_refdata_label): if data.label == new_refdata_label ] - self._viewer_store[viewer_reference].state.reference_data = new_refdata - change_refdata_message = ChangeRefDataMessage( new_refdata, viewer, @@ -482,9 +475,11 @@ def _change_reference_data(self, new_refdata_label): old=old_refdata, new=new_refdata ) + + self._viewer_store[viewer_reference].state.reference_data = new_refdata self.hub.broadcast(change_refdata_message) - viewer.state.reset_limits() + self._viewer_store[viewer_reference].state.reset_limits() def _link_new_data(self, reference_data=None, data_to_be_linked=None): """ diff --git a/jdaviz/configs/imviz/wcs_utils.py b/jdaviz/configs/imviz/wcs_utils.py index 1bdbb1d59e..eca0c99e9f 100644 --- a/jdaviz/configs/imviz/wcs_utils.py +++ b/jdaviz/configs/imviz/wcs_utils.py @@ -288,7 +288,7 @@ def data_outside_gwcs_bounding_box(data, x, y): return outside_bounding_box -def get_fits_wcs_from_file(filename): +def _get_fits_wcs_from_file(filename): header = fits.getheader(filename) with warnings.catch_warnings(): # Ignore a warning on using DATE-OBS in place of MJD-OBS @@ -303,6 +303,7 @@ def rotated_gwcs( center_world_coord, rotation_angle, pixel_scales, + cdelt_signs, refdata_shape=(2, 2) ): # based on ``gwcs_simple_imaging_units`` in gwcs: @@ -319,11 +320,12 @@ def rotated_gwcs( rescale_pixel_scale = np.array(refdata_shape) / 1000 shift_by_crpix = ( - models.Shift(-refdata_shape[1] / 2 * u.pixel) & - models.Shift(-refdata_shape[0] / 2 * u.pixel) + models.Shift(-refdata_shape[0] / 2 * u.pixel) & + models.Shift(-refdata_shape[1] / 2 * u.pixel) ) - # multiplying by +/-1 can flip east/west - flip_east_west = models.Multiply(-1) & models.Multiply(1) + + # multiplying by +/-1 can flip north/south or east/west + flip_axes = models.Multiply(cdelt_signs[0]) & models.Multiply(cdelt_signs[1]) rotation = models.AffineTransformation2D( rotation_matrix * u.deg, translation=[0, 0] * u.deg ) @@ -344,7 +346,7 @@ def rotated_gwcs( ) det2sky = ( - shift_by_crpix | flip_east_west | rotation | + flip_axes | shift_by_crpix | rotation | tan | celestial_rotation ) det2sky.name = "linear_transform" @@ -367,11 +369,46 @@ def rotated_gwcs( return GWCS(pipeline) -def get_rotated_nddata_from_fits(filename, rotation_angle, refdata_shape=(2, 2)): +def _prepare_rotated_nddata(wcs, rotation_angle, refdata_shape): + # get the world coordinates of the central pixel + real_image_shape = np.array(wcs.array_shape) + central_pixel_coord = real_image_shape / 2 * u.pix + central_world_coord = wcs.pixel_to_world(*central_pixel_coord) + rotation_angle = coord.Angle(rotation_angle).wrap_at(360 * u.deg) + + # compute the x/y plate scales from the WCS: + pixel_scales = [ + value * unit / u.pix + for value, unit in zip( + proj_plane_pixel_scales(wcs), wcs.wcs.cunit + ) + ] + + # flip e.g. RA or Dec axes? + cdelt_signs = np.sign(wcs.wcs.cdelt) + + # create a GWCS centered on ``filename``, + # and rotated by ``rotation_angle``: + new_rotated_gwcs = rotated_gwcs( + central_world_coord, rotation_angle, pixel_scales, cdelt_signs + ) + + # create an all-nan NDDataArray with the rotated GWCS: + ndd = NDDataArray( + data=np.nan * np.ones(refdata_shape), + wcs=new_rotated_gwcs, + ) + return ndd + + +def _get_rotated_nddata_from_fits(filename, rotation_angle, refdata_shape=(2, 2)): """ Create a synthetic NDDataArray which stores GWCS that approximate the FITS WCS in ``filename`` rotated by ``rotation_angle``. + This method is useful for ensuring that future datasets are loaded + in the correct orientation. + Parameters ---------- filename : path-like, str @@ -388,31 +425,41 @@ def get_rotated_nddata_from_fits(filename, rotation_angle, refdata_shape=(2, 2)) Data are all NaNs, wcs are rotated. """ # get the FITS WCS from the file: - wcs = get_fits_wcs_from_file(filename) + wcs = _get_fits_wcs_from_file(filename) - # get the world coordinates of the central pixel - real_image_shape = np.array(wcs.array_shape) - central_pixel_coord = real_image_shape / 2 * u.pix - central_world_coord = wcs.pixel_to_world(*central_pixel_coord) + return _prepare_rotated_nddata(wcs, rotation_angle, refdata_shape) - # compute the x/y plate scales from the WCS: - pixel_scales = [ - value * unit / u.pix - for value, unit in zip( - proj_plane_pixel_scales(wcs), wcs.wcs.cunit - ) - ] - # create a GWCS centered on ``filename``, - # and rotated by ``rotation_angle``: - new_rotated_gwcs = rotated_gwcs( - central_world_coord, rotation_angle, pixel_scales - ) +def _get_rotated_nddata_from_label(app, data_label, rotation_angle, refdata_shape=(2, 2)): + """ + Create a synthetic NDDataArray which stores GWCS that approximate + the WCS in the coords attr of the Data object with label ``data_label`` + loaded into ``app``. - # create an all-nan NDDataArray with the rotated GWCS: - ndd = NDDataArray( - data=np.nan * np.ones(refdata_shape), - wcs=new_rotated_gwcs, - ) + This method is useful for rotating pre-loaded datasets when + combined with ``app._change_reference_data(data_label)``. - return ndd + Parameters + ---------- + app : `~jdaviz.Application` + App instance containing ``data_label`` + data_label : str + Data label for the Data to rotate + rotation_angle : `~astropy.units.Quantity` + Angle to rotate the image counter-clockwise from its + original orientation + refdata_shape : tuple + Shape of the reference data array + + Returns + ------- + ndd : `~astropy.nddata.NDDataArray` + Data are all NaNs, wcs are rotated. + """ + # get the WCS from the Data object's coords attribute: + [wcs] = [ + data.coords for data in app.data_collection + if data.label == data_label + ] + + return _prepare_rotated_nddata(wcs, rotation_angle, refdata_shape)