From a9c4418614ae5adc4d6296faa24360f57419e64f Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 5 Jan 2024 15:31:10 -0500 Subject: [PATCH] move zoom-level and center logic to image viewer state --- jdaviz/configs/imviz/plugins/viewers.py | 2 + jdaviz/core/astrowidgets_api.py | 22 ++------ jdaviz/core/freezable_state.py | 68 ++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py index 53121734c2..7d80a9488c 100644 --- a/jdaviz/configs/imviz/plugins/viewers.py +++ b/jdaviz/configs/imviz/plugins/viewers.py @@ -35,6 +35,8 @@ class ImvizImageView(JdavizViewerMixin, BqplotImageView, AstrowidgetsImageViewer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # provide reference from state back to viewer to use for zoom syncing + self.state._viewer = self self.init_astrowidgets_api() self._subscribe_to_layers_update() diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index da6639a3af..8e681a2fe6 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -173,12 +173,7 @@ def zoom_level(self): if self.shape is None: # pragma: no cover raise ValueError('Viewer is still loading, try again later') - screenx = self.shape[1] - screeny = self.shape[0] - zoom_x = screenx / (self.state.x_max - self.state.x_min) - zoom_y = screeny / (self.state.y_max - self.state.y_min) - - return max(zoom_x, zoom_y) # Similar to Ginga get_scale() + return self.state.zoom_level # Loosely based on glue/viewers/image/state.py @zoom_level.setter @@ -196,19 +191,8 @@ def zoom_level(self, val): if val == 'fit': self.state.reset_limits() return - else: - cur_xcen = (self.state.x_min + self.state.x_max) * 0.5 - new_dx = self.shape[1] * 0.5 / val - new_x_min = cur_xcen - new_dx - new_x_max = cur_xcen + new_dx - - with delay_callback(self.state, 'x_min', 'x_max'): - self.state.x_min = new_x_min - 0.5 - self.state.x_max = new_x_max - 0.5 - - # 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() + + self.state.zoom_level = val # 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 6405bf035b..4e82b586df 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -1,4 +1,5 @@ -from echo import delay_callback +from contextlib import contextmanager +from echo import delay_callback, CallbackProperty import numpy as np from glue.viewers.profile.state import ProfileViewerState @@ -54,8 +55,73 @@ def _reset_x_limits(self, *event): class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): linked_by_wcs = False + zoom_level = CallbackProperty(1.0, docstring='Zoom-level') + zoom_center = CallbackProperty((0.0, 0.0), docstring='Coordinates of center of zoom box') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._during_zoom_sync = False + self.add_callback('zoom_level', self._set_zoom_level) + self.add_callback('zoom_center', self._set_zoom_center) + for attr in ('x_min', 'x_max', 'y_min', 'y_max'): + self.add_callback(attr, self._set_axes_lim) + + @contextmanager + def during_zoom_sync(self): + self._during_zoom_sync = True + try: + yield + except Exception: + self._during_zoom_sync = False + raise + self._during_zoom_sync = False + + def _set_zoom_level(self, zoom_level): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + return + + 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 + + with self.during_zoom_sync(): + self.x_min = new_x_min - 0.5 + self.x_max = new_x_max - 0.5 + + # 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, zoom_center): + 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 = zoom_center[0] - cur_xcen + delta_y = zoom_center[1] - 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_lim(self, *args): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + 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) + + with self.during_zoom_sync(): + self.zoom_level = max(zoom_x, zoom_y) # Similar to Ginga get_scale() + self.zoom_center = (center_x, center_y) def reset_limits(self, *event): if self.reference_data is None: # Nothing to do