diff --git a/jdaviz/configs/imviz/helper.py b/jdaviz/configs/imviz/helper.py index 7315fc0f61..5aa5482523 100644 --- a/jdaviz/configs/imviz/helper.py +++ b/jdaviz/configs/imviz/helper.py @@ -209,6 +209,84 @@ def offset_by(self, dx, dy): viewer.state.x_max = viewer.state.x_min + width viewer.state.y_max = viewer.state.y_min + height + @property + def zoom_level(self): + """Zoom level: + + * 1 means real-pixel-size. + * 2 means zoomed in by a factor of 2. + * 0.5 means zoomed out by a factor of 2. + * 'fit' resets the zoom. + + """ + viewer = self.app.get_viewer("viewer-1") + i_top = get_top_layer_index(viewer) + image = viewer.layers[i_top].layer + nx = image.shape[viewer.state.x_att.axis] + ny = image.shape[viewer.state.y_att.axis] + zoom_x = nx / (viewer.state.x_max - viewer.state.x_min) + zoom_y = ny / (viewer.state.y_max - viewer.state.y_min) + + return max(zoom_x, zoom_y) # Similar to Ginga get_scale() + + # Loosely based on glue/viewers/image/state.py + @zoom_level.setter + def zoom_level(self, val): + if ((not isinstance(val, (int, float)) and val != 'fit') or + (isinstance(val, (int, float)) and val <= 0)): + raise ValueError(f'Unsupported zoom level: {val}') + + viewer = self.app.get_viewer("viewer-1") + if (viewer.state.reference_data is None or viewer.state.x_att is None + or viewer.state.y_att is None): + return + + # Want to zoom what is actually visible. + # Zoom on X and Y will auto-adjust. + i_top = get_top_layer_index(viewer) + image = viewer.layers[i_top].layer + nx = image.shape[viewer.state.x_att.axis] + + if val == 'fit': + # Similar to ImageViewerState.reset_limits() in Glue. + new_x_min = 0 + new_x_max = nx + else: + width = viewer.state.x_max - viewer.state.x_min + cur_xcen = viewer.state.x_min + (width * 0.5) + new_dx = nx * 0.5 / val + new_x_min = cur_xcen - new_dx + new_x_max = cur_xcen + new_dx + + with delay_callback(viewer.state, 'x_min', 'x_max'): + viewer.state.x_min = new_x_min - 0.5 + viewer.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. + viewer.state._adjust_limits_aspect() + + # Discussion on why we need two different ways to set zoom at + # https://github.com/astropy/astrowidgets/issues/144 + def zoom(self, val): + """Zoom in or out by the given factor. + + Parameters + ---------- + val : int or float + The zoom level to zoom the image. + See `zoom_level`. + + Raises + ------ + ValueError + Invalid zoom factor. + + """ + if not isinstance(val, (int, float)): + raise ValueError(f"zoom only accepts int or float but got '{val}'") + self.zoom_level = self.zoom_level * val + @property def marker(self): """Marker to use. diff --git a/notebooks/ImvizExample.ipynb b/notebooks/ImvizExample.ipynb index 1e1bb5cf52..88efbc66db 100644 --- a/notebooks/ImvizExample.ipynb +++ b/notebooks/ImvizExample.ipynb @@ -387,6 +387,54 @@ "imviz.offset_by(0.5 * u.arcsec, -1.5 * u.arcsec)" ] }, + { + "cell_type": "markdown", + "id": "e2744c0c-8721-480a-811e-87904373e66f", + "metadata": {}, + "source": [ + "You can programmatically zoom in and out.\n", + "\n", + "Zoom level:\n", + "\n", + "* 1 means real-pixel-size.\n", + "* 2 means zoomed in by a factor of 2.\n", + "* 0.5 means zoomed out by a factor of 2.\n", + "* 'fit' resets the zoom." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1bad9bdf-b959-4b74-9a77-07212095c2ce", + "metadata": {}, + "outputs": [], + "source": [ + "# Get the current zoom level.\n", + "imviz.zoom_level" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a834bcf-7d5a-492b-bc54-740e1500a2f4", + "metadata": {}, + "outputs": [], + "source": [ + "# Set the zoom level directly.\n", + "imviz.zoom_level = 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32288b81-1817-4549-b2e4-5e7f55b0ee3d", + "metadata": {}, + "outputs": [], + "source": [ + "# Set the relative zoom based on current zoom level.\n", + "imviz.zoom(2)" + ] + }, { "cell_type": "markdown", "id": "11fab067-7428-4ce4-bc9b-f5462fe52e2a",