From 5468fb111f52f9c04683eb4596b7ef90192fbf66 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 24 Jan 2024 14:38:44 -0500 Subject: [PATCH] expose zoom-radius instead of zoom-level in state/plot options --- .../plugins/plot_options/plot_options.py | 10 +-- .../plugins/plot_options/plot_options.vue | 8 +- jdaviz/core/astrowidgets_api.py | 43 +++++++++- jdaviz/core/freezable_state.py | 81 ++++++++++--------- 4 files changed, 90 insertions(+), 52 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index af88844974..dd92409c0a 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -231,8 +231,8 @@ class PlotOptions(PluginTemplateMixin): zoom_center_y_value = Float().tag(sync=True) zoom_center_y_sync = Dict().tag(sync=True) - zoom_level_value = Float().tag(sync=True) - zoom_level_sync = Dict().tag(sync=True) + zoom_radius_value = Float().tag(sync=True) + zoom_radius_sync = Dict().tag(sync=True) # scatter/marker options marker_visible_value = Bool().tag(sync=True) @@ -465,8 +465,8 @@ def state_attr_for_line_visible(state): 'zoom_center_x_value', 'zoom_center_x_sync') self.zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y', 'zoom_center_y_value', 'zoom_center_y_sync') - self.zoom_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', - 'zoom_level_value', 'zoom_level_sync') + self.zoom_radius = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_radius', + 'zoom_radius_value', 'zoom_radius_sync') # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) @@ -651,7 +651,7 @@ def user_api(self): 'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity', 'line_as_steps', 'uncertainty_visible'] if self.config != "specviz": - expose += ['zoom_center_x', 'zoom_center_y', 'zoom_level', + expose += ['zoom_center_x', 'zoom_center_y', 'zoom_radius', 'subset_color', 'subset_opacity', 'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax', 'stretch_hist_zoom_limits', 'stretch_hist_nbins', diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index ff2e61ff73..5c98c289a3 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -104,11 +104,11 @@ :step="0.1" /> - + diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index 66fe81f63a..09aa5220c5 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -9,7 +9,7 @@ from glue.config import colormaps from glue.core import Data -from jdaviz.configs.imviz.helper import get_top_layer_index +from jdaviz.configs.imviz.helper import get_top_layer_index, get_reference_image_data from jdaviz.core.events import SnackbarMessage, AstrowidgetMarkersChangedMessage from jdaviz.core.helpers import data_has_valid_wcs @@ -177,7 +177,22 @@ def zoom_level(self): if self.shape is None: # pragma: no cover raise ValueError('Viewer is still loading, try again later') - return self.state.zoom_level + if hasattr(self, '_get_real_xy'): + image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) + # TODO: Do we want top layer instead? + # i_top = get_top_layer_index(self) + # image = self.layers[i_top].layer + real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) + real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) + else: + real_min = (self.state.x_min, self.state.y_min) + real_max = (self.state.x_max, self.state.y_max) + screenx = self.shape[1] + screeny = self.shape[0] + zoom_x = screenx / abs(real_max[0] - real_min[0]) + zoom_y = screeny / abs(real_max[1] - real_min[1]) + + return max(zoom_x, zoom_y) # Similar to Ginga get_scale() # Loosely based on glue/viewers/image/state.py @zoom_level.setter @@ -195,7 +210,29 @@ def zoom_level(self, val): self.state.reset_limits() return - self.state.zoom_level = val + new_dx = self.shape[1] * 0.5 / val + if hasattr(self, '_get_real_xy'): + image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) + # TODO: Do we want top layer instead? + # i_top = get_top_layer_index(self) + # image = self.layers[i_top].layer + real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) + real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) + cur_xcen = (real_min[0] + real_max[0]) * 0.5 + new_x_min = self._get_real_xy(image, cur_xcen - new_dx - 0.5, real_min[1], reverse=True)[0] # noqa: E501 + new_x_max = self._get_real_xy(image, cur_xcen + new_dx - 0.5, real_max[1], reverse=True)[0] # noqa: E501 + else: + cur_xcen = (self.state.x_min + self.state.x_max) * 0.5 + new_x_min = cur_xcen - new_dx - 0.5 + new_x_max = cur_xcen + new_dx - 0.5 + + with delay_callback(self.state, 'x_min', 'x_max'): + self.state.x_min = new_x_min + self.state.x_max = new_x_max + + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self.state._adjust_limits_aspect() # Discussion on why we need two different ways to set zoom at # https://github.com/astropy/astrowidgets/issues/144 diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 8c50825ceb..977133efd4 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -6,6 +6,8 @@ from glue_jupyter.bqplot.image.state import BqplotImageViewerState from glue.viewers.matplotlib.state import DeferredDrawCallbackProperty as DDCProperty +from jdaviz.configs.imviz.helper import get_reference_image_data + __all__ = ['FreezableState', 'FreezableProfileViewerState', 'FreezableBqplotImageViewerState'] @@ -55,16 +57,16 @@ def _reset_x_limits(self, *event): class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): linked_by_wcs = False - zoom_level = CallbackProperty(1.0, docstring='Zoom-level') + zoom_radius = CallbackProperty(1.0, docstring="Zoom radius") zoom_center_x = CallbackProperty(0.0, docstring='x-coordinate of center of zoom box') zoom_center_y = CallbackProperty(0.0, docstring='y-coordinate of center of zoom box') def __init__(self, *args, **kwargs): self.wcs_only_layers = [] # For Imviz rotation use. self._during_zoom_sync = False - self.add_callback('zoom_level', self._set_zoom_level) - self.add_callback('zoom_center_x', self._set_zoom_center) - self.add_callback('zoom_center_y', self._set_zoom_center) + self.add_callback('zoom_radius', self._set_zoom_radius_center) + self.add_callback('zoom_center_x', self._set_zoom_radius_center) + self.add_callback('zoom_center_y', self._set_zoom_radius_center) for attr in ('x_min', 'x_max', 'y_min', 'y_max'): self.add_callback(attr, self._set_axes_lim) super().__init__(*args, **kwargs) @@ -79,40 +81,34 @@ def during_zoom_sync(self): raise self._during_zoom_sync = False - def _set_zoom_level(self, zoom_level): + def _set_zoom_radius_center(self, *args): if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: return - if zoom_level <= 0.0: - raise ValueError("zoom_level must be positive") - - cur_xcen = (self.x_min + self.x_max) * 0.5 - new_dx = self._viewer.shape[1] * 0.5 / zoom_level - new_x_min = cur_xcen - new_dx - new_x_max = cur_xcen + new_dx + if self.zoom_radius <= 0.0: + raise ValueError("zoom_radius must be positive") + + # When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units, + # x/y_min/max are in pixels of the WCS-only layer + if self.linked_by_wcs: + image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) + ref_wcs = image.coords + center_x, center_y = ref_wcs.world_to_pixel_values(self.zoom_center_x, self.zoom_center_y) # noqa + center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+self.zoom_radius, self.zoom_center_y) # noqa + radius = abs(center_xr - center_x) + else: + center_x, center_y = self.zoom_center_x, self.zoom_center_y + radius = self.zoom_radius + # now center_x/y and radius are in pixel units of the reference data, so can be used to + # update limits with self.during_zoom_sync(): - self.x_min = new_x_min - 0.5 - self.x_max = new_x_max - 0.5 + self.x_min = center_x - radius + self.x_max = center_x + radius + self.y_min = center_y - radius + self.y_max = center_y + radius - # We need to adjust the limits in here to avoid triggering all - # the update events then changing the limits again. self._adjust_limits_aspect() - def _set_zoom_center(self, *args): - if self._during_zoom_sync: - return - - cur_xcen = (self.x_min + self.x_max) * 0.5 - cur_ycen = (self.y_min + self.y_max) * 0.5 - delta_x = self.zoom_center_x - cur_xcen - delta_y = self.zoom_center_y - cur_ycen - - with self.during_zoom_sync(): - self.x_min += delta_x - self.x_max += delta_x - self.y_min += delta_y - self.y_max += delta_y - def _set_axes_aspect_ratio(self, axes_ratio): # when aspect-ratio is changed (changing viewer.shape), ensure zoom/center are synced # with zoom-limits @@ -125,17 +121,22 @@ def _set_axes_lim(self, *args): if None in (self.x_min, self.x_max, self.y_min, self.y_max): return - screenx = self._viewer.shape[1] - screeny = self._viewer.shape[0] - zoom_x = screenx / (self.x_max - self.x_min) - zoom_y = screeny / (self.y_max - self.y_min) - center_x = 0.5 * (self.x_max + self.x_min) - center_y = 0.5 * (self.y_max + self.y_min) + # When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units, + # x/y_min/max are in pixels of the WCS-only layer + if self.linked_by_wcs: + image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) + ref_wcs = image.coords + x_min, y_min = ref_wcs.pixel_to_world_values(self.x_min, self.y_min) + x_max, y_max = ref_wcs.pixel_to_world_values(self.x_max, self.y_max) + else: + x_min, y_min = self.x_min, self.y_min + x_max, y_max = self.x_max, self.y_max + # now x_min/max, y_min/max are in axes units (degrees if WCS-linked, pixels otherwise) with self.during_zoom_sync(): - self.zoom_level = max(zoom_x, zoom_y) # Similar to Ginga get_scale() - self.zoom_center_x = center_x - self.zoom_center_y = center_y + self.zoom_radius = abs(0.5 * min(x_max - x_min, y_max - y_min)) + self.zoom_center_x = 0.5 * (x_max + x_min) + self.zoom_center_y = 0.5 * (y_max + y_min) def reset_limits(self, *event): # TODO: use consistent logic for all image viewers by removing this if-statement