Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions astrowidgets/bqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ def __init__(self, *args, display_width=500, display_aspect_ratio=1):
# to the viewer.
self._viewport_change_source_is_gui = False

# Guards re-entrancy while we programmatically update the viewport.
self._updating_viewport = False

# Provide an Output widget to which prints can be directed for
# debugging.
self._print_out = ipw.Output()
Expand Down Expand Up @@ -472,36 +475,49 @@ def _add_new_single_marker(self, x_mark, y_mark):
# Update the viewport to match changes in the UI
def _init_watch_image_changes(self):
"""
Watch for changes to the image scale, which indicate the user
has either changed the zoom or has panned, and update the zoom_level.
Watch for changes to the image scales, which indicate the user
has either changed the zoom or has panned, and update the stored
viewport (center and field of view) to match.
"""
def update_zoom_level(event):
def update_viewport(event):
"""
Watch for changes in the zoom level from the viewer.
Watch for changes in the viewport (pan or zoom) from the viewer.
"""

old_width = self.get_viewport(sky_or_pixel='pixel', image_label=self._current_image_label)["fov"]
new_width = self._astro_im.get_current_width()
if new_width is None or self._updating_viewport:
# There is no image yet, or this object is in the process
# of changing the zoom, so return
# of changing the viewport, so return
return

# Do nothing if the zoom has not changed
if np.abs(new_width - old_width) > 1e-3:
# Let the zoom_level handler know the GUI itself
# generated this zoom change which means the GUI
# does not need to be updated.
current = self.get_viewport(sky_or_pixel='pixel',
image_label=self._current_image_label)
old_width = current["fov"]
old_center = current["center"]
new_center = self._astro_im.center

width_changed = np.abs(new_width - old_width) > 1e-3
center_changed = (
old_center is None
or np.abs(new_center[0] - old_center[0]) > 1e-3
or np.abs(new_center[1] - old_center[1]) > 1e-3
)

# Do nothing if neither the zoom nor the pan has changed
if width_changed or center_changed:
# Let the viewport handler know the GUI itself generated this
# change, which means the GUI does not need to be updated.
self._viewport_change_source_is_gui = True
self.set_viewport(fov=new_width)

# Observe changes to the maximum of the x scale. Observing the y scale
# or the minimum instead of the maximum is also fine.
x_scale = self._astro_im._scales['x']
self.set_viewport(center=new_center, fov=new_width)

# Observe changes to the maximum of both the x and y scales so that
# horizontal pan, vertical pan, and zoom are all detected. Observing
# the maximum (rather than the minimum) of each scale is sufficient
# because a pan shifts both ends of a scale.
#
# If things seem laggy in the future, check whether throttling
# the updates helps.
x_scale.observe(update_zoom_level, names='max')
for scale in (self._astro_im._scales['x'], self._astro_im._scales['y']):
scale.observe(update_viewport, names='max')

def _interval_and_stretch(self, stretch=None, cuts=None):
"""
Expand Down Expand Up @@ -532,11 +548,16 @@ def _current_image_label(self):

def set_stretch(self, value, image_label=None, **kwargs):
super().set_stretch(value, image_label=image_label, **kwargs)
self._send_data(stretch=value)
# Changing the stretch only affects the color mapping, so leave the
# current viewport (zoom/pan) untouched.
self._send_data(stretch=value, reset_view=False)

def set_cuts(self, value, image_label=None, **kwargs):
super().set_cuts(value, image_label=image_label, **kwargs)
self._send_data(cuts=self.get_cuts(image_label=image_label))
# Changing the cuts only affects the color mapping, so leave the
# current viewport (zoom/pan) untouched.
self._send_data(cuts=self.get_cuts(image_label=image_label),
reset_view=False)

@property
def viewer(self):
Expand Down
29 changes: 28 additions & 1 deletion astrowidgets/tests/test_widget_api_bqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from traitlets import TraitError

from astro_image_display_api import ImageAPITest
from astro_image_display_api.api_test import ImageAPITest
from astro_image_display_api import ImageViewerInterface

_ = pytest.importorskip("bqplot",
Expand All @@ -29,3 +29,30 @@ def test_save(self, tmp_path):
"a running browser.")
def test_save_overwrite(self, tmp_path):
pass

def test_get_viewport_reflects_interactive_pan(self, data):
# Panning in the browser shifts the bqplot scales directly. Simulate
# that here and check that get_viewport reports the new center.
self.image.load_image(data)
self.image.set_viewport(center=(75, 50), fov=100)

scales = self.image._astro_im._scales

# Horizontal pan: shift the x scale. Set min before max so that the
# center is already correct when the 'max' observer fires.
dx = 20
scales['x'].min += dx
scales['x'].max += dx

vp = self.image.get_viewport(sky_or_pixel='pixel')
assert vp['center'][0] == pytest.approx(75 + dx)
assert vp['center'][1] == pytest.approx(50)

# Vertical pan: shift the y scale.
dy = -15
scales['y'].min += dy
scales['y'].max += dy

vp = self.image.get_viewport(sky_or_pixel='pixel')
assert vp['center'][0] == pytest.approx(75 + dx)
assert vp['center'][1] == pytest.approx(50 + dy)
Loading
Loading