def _reference_data_changed(self, *args): # This signal can get emitted if just the choices but not the actual # reference data change, so we check here that the reference data has # actually changed if self.reference_data is not getattr(self, '_last_reference_data', None): self._last_reference_data = self.reference_data # Note that we deliberately use nested delay_callback here, because # we want to make sure that x_att_world and y_att_world both get # updated first, then x_att and y_att can be changed, before # subsequent events are fired. with delay_callback(self, 'x_att', 'y_att'): with delay_callback(self, 'x_att_world', 'y_att_world', 'slices'): if self._display_world: self.xw_att_helper.pixel_coord = False self.yw_att_helper.pixel_coord = False self.xw_att_helper.world_coord = True self.yw_att_helper.world_coord = True else: self.xw_att_helper.pixel_coord = True self.yw_att_helper.pixel_coord = True self.xw_att_helper.world_coord = False self.yw_att_helper.world_coord = False self._update_combo_att() self._set_default_slices() # We need to make sure that we update x_att and y_att # at the same time before any other callbacks get called, # so we do this here manually. self._on_xatt_world_change() self._on_yatt_world_change()
def test_delay_callback_nested(): test = MagicMock() stub = Stub() add_callback(stub, 'prop1', test) with delay_callback(stub, 'prop1'): with delay_callback(stub, 'prop1'): stub.prop1 = 100 stub.prop1 = 200 stub.prop1 = 300 assert test.call_count == 0 assert test.call_count == 0 test.assert_called_once_with(300)
def test_delay_only_calls_if_changed(): stub = Stub() test = MagicMock() add_callback(stub, 'prop1', test) with delay_callback(stub, 'prop1'): pass assert test.call_count == 0 val = stub.prop1 with delay_callback(stub, 'prop1'): stub.prop1 = val assert test.call_count == 0
def update_limits(self, update_profile=True): with delay_callback(self, 'v_min', 'v_max'): if update_profile: self.update_profile(update_limits=False) if self._profile_cache is not None and len(self._profile_cache[1]) > 0: self.v_min = np.nanmin(self._profile_cache[1]) self.v_max = np.nanmax(self._profile_cache[1])
def press(self, event): if not self.active or not event.inaxes: return self.pressed = True x_min, x_max = self._axes.get_xlim() x_range = abs(x_max - x_min) if self.state.x_min is None or self.state.x_max is None: self.mode = 'move-x-max' with delay_callback(self.state, 'x_min', 'x_max'): self.state.x_min = event.xdata self.state.x_max = event.xdata elif abs(event.xdata - self.state.x_min) / x_range < PICK_THRESH: self.mode = 'move-x-min' elif abs(event.xdata - self.state.x_max) / x_range < PICK_THRESH: self.mode = 'move-x-max' elif (event.xdata > self.state.x_min) is (event.xdata < self.state.x_max): self.mode = 'move' self.move_params = (event.xdata, self.state.x_min, self.state.x_max) else: self.mode = 'move-x-max' self.state.x_min = event.xdata
def _reset_x_limits(self, *event): # NOTE: we don't use AttributeLimitsHelper because we need to avoid # trying to get the minimum of *all* the world coordinates in the # dataset. Instead, we use the same approach as in the layer state below # and in the case of world coordinates we use online the spine of the # data. if self.reference_data is None or self.x_att_pixel is None: return data = self.reference_data if self.x_att in data.pixel_component_ids: x_min, x_max = -0.5, data.shape[self.x_att.axis] - 0.5 else: axis = data.world_component_ids.index(self.x_att) axis_view = [0] * data.ndim axis_view[axis] = slice(None) axis_values = data[self.x_att, tuple(axis_view)] x_min, x_max = np.nanmin(axis_values), np.nanmax(axis_values) with delay_callback(self, 'x_min', 'x_max'): self.x_min = x_min self.x_max = x_max
def test_delay_callback_not_called_if_unmodified(): test = MagicMock() stub = Stub() add_callback(stub, 'prop1', test) with delay_callback(stub, 'prop1'): pass assert test.call_count == 0
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") image = viewer.state.reference_data if (image is None or viewer.shape is None or viewer.state.x_att is None or viewer.state.y_att is None): # pragma: no cover return # Zoom on X and Y will auto-adjust. if val == 'fit': # Similar to ImageViewerState.reset_limits() in Glue. new_x_min = 0 new_x_max = image.shape[viewer.state.x_att.axis] else: cur_xcen = (viewer.state.x_min + viewer.state.x_max) * 0.5 new_dx = viewer.shape[1] * 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()
def _reference_data_changed(self, before=None, after=None): # A callback event for reference_data is triggered if the choices change # but the actual selection doesn't - so we avoid resetting the WCS in # this case. if before is after: return for layer in self.layers: layer.reset_cache() # This signal can get emitted if just the choices but not the actual # reference data change, so we check here that the reference data has # actually changed if self.reference_data is not getattr(self, '_last_reference_data', None): self._last_reference_data = self.reference_data with delay_callback(self, 'x_att'): if self.reference_data is None: self.x_att_helper.set_multiple_data([]) else: self.x_att_helper.set_multiple_data([self.reference_data]) if self._display_world: self.x_att_helper.world_coord = True self.x_att = self.reference_data.world_component_ids[0] else: self.x_att_helper.world_coord = False self.x_att = self.reference_data.pixel_component_ids[0] self._update_att() self.reset_limits()
def _handle_image_zoom(self, msg): mos_data = self.app.data_collection['MOS Table'] # trigger zooming the image, if there is an image if mos_data.find_component_id("Images") is not None: if msg.shared_image: center, height = self._zoom_to_object_params(msg) else: try: center, height = self._zoom_to_slit_params(msg) except IndexError: # If there's nothing in the spectrum2d viewer, we can't get slit info return if center is None or height is None: # Can't zoom if we couldn't figure out where to zoom (e.g. if RA/Dec not in table) return imview = self.app.get_viewer("image-viewer") image_axis_ratio = ( (imview.axis_x.scale.max - imview.axis_x.scale.min) / (imview.axis_y.scale.max - imview.axis_y.scale.min)) with delay_callback(imview.state, 'x_min', 'x_max', 'y_min', 'y_max'): imview.state.x_min = center[0] - image_axis_ratio * height imview.state.y_min = center[1] - height imview.state.x_max = center[0] + image_axis_ratio * height imview.state.y_max = center[1] + height
def _reference_data_changed(self, *args): for layer in self.layers: layer.reset_cache() # This signal can get emitted if just the choices but not the actual # reference data change, so we check here that the reference data has # actually changed if self.reference_data is not getattr(self, '_last_reference_data', None): self._last_reference_data = self.reference_data with delay_callback(self, 'x_att'): if self.reference_data is None: self.x_att_helper.set_multiple_data([]) else: self.x_att_helper.set_multiple_data([self.reference_data]) if self._display_world: self.x_att_helper.world_coord = True self.x_att = self.reference_data.world_component_ids[0] else: self.x_att_helper.world_coord = False self.x_att = self.reference_data.pixel_component_ids[0] self._update_att()
def update_view_to_bins(self, *args): """ Update the view to match the histogram interval """ with delay_callback(self, 'x_min', 'x_max'): self.x_min = self.hist_x_min self.x_max = self.hist_x_max
def _on_xy_change(self, *event): if self.viewer_state.x_att is None or self.viewer_state.y_att is None: return if isinstance(self.layer, BaseData): layer = self.layer else: layer = self.layer.data try: x_datetime = layer.get_kind(self.viewer_state.x_att) == 'datetime' except IncompatibleAttribute: x_datetime = False try: y_datetime = layer.get_kind(self.viewer_state.y_att) == 'datetime' except IncompatibleAttribute: y_datetime = False with delay_callback(self, 'xerr_visible', 'yerr_visible', 'vector_visible'): if x_datetime: self.xerr_visible = False if y_datetime: self.yerr_visible = False if x_datetime or y_datetime: self.vector_visible = False
def _on_layer_change(self, layer=None): with delay_callback(self, 'cmap_vmin', 'cmap_vmax', 'size_vmin', 'size_vmax', 'density_map'): self._update_density_map_mode() if self.layer is None: self.cmap_att_helper.set_multiple_data([]) self.size_att_helper.set_multiple_data([]) else: self.cmap_att_helper.set_multiple_data([self.layer]) self.size_att_helper.set_multiple_data([self.layer]) if self.layer is None: self.xerr_att_helper.set_multiple_data([]) self.yerr_att_helper.set_multiple_data([]) else: self.xerr_att_helper.set_multiple_data([self.layer]) self.yerr_att_helper.set_multiple_data([self.layer]) if self.layer is None: self.vx_att_helper.set_multiple_data([]) self.vy_att_helper.set_multiple_data([]) else: self.vx_att_helper.set_multiple_data([self.layer]) self.vy_att_helper.set_multiple_data([self.layer])
def camera_mouse_move(self, event=None): if 1 in event.buttons and keys.SHIFT in event.mouse_event.modifiers: camera = self._vispy_widget.view.camera norm = np.mean(camera._viewbox.size) p1 = event.mouse_event.press_event.pos p2 = event.mouse_event.pos dist = (p1 - p2) / norm * camera._scale_factor dist[1] *= -1 dx, dy, dz = camera._dist_to_trans(dist) with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max', 'z_min', 'z_max'): self.state.x_min = self._initial_position[ 0] + self._width[0] * dx self.state.x_max = self._initial_position[ 1] + self._width[0] * dx self.state.y_min = self._initial_position[ 2] + self._width[1] * dy self.state.y_max = self._initial_position[ 3] + self._width[1] * dy self.state.z_min = self._initial_position[ 4] + self._width[2] * dz self.state.z_max = self._initial_position[ 5] + self._width[2] * dz event.handled = True
def camera_mouse_wheel(self, event=None): scale = (1.1**-event.delta[1]) with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max', 'z_min', 'z_max'): xmid = 0.5 * (self.state.x_min + self.state.x_max) dx = (self.state.x_max - xmid) * scale self.state.x_min = xmid - dx self.state.x_max = xmid + dx ymid = 0.5 * (self.state.y_min + self.state.y_max) dy = (self.state.y_max - ymid) * scale self.state.y_min = ymid - dy self.state.y_max = ymid + dy zmid = 0.5 * (self.state.z_min + self.state.z_max) dz = (self.state.z_max - zmid) * scale self.state.z_min = zmid - dz self.state.z_max = zmid + dz self._update_clip() event.handled = True
def _on_data_change(self, *args): links = self.visible_links with delay_callback(self, 'current_link'): LinkEditorState.current_link.set_choices(self, links) if len(links) > 0: self.current_link = links[0]
def on_limits_change(self, *args): for viewer in self.viewer.session.application.viewers: if viewer is not self.viewer: with delay_callback(viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'): viewer.state.x_min = self.viewer.state.x_min viewer.state.x_max = self.viewer.state.x_max viewer.state.y_min = self.viewer.state.y_min viewer.state.y_max = self.viewer.state.y_max
def _reset_x_limits(self, *args): if self.x_att is None: return with delay_callback(self, 'hist_x_min', 'hist_x_max', 'x_min', 'x_max', 'x_log'): self.x_lim_helper.percentile = 100 self.x_lim_helper.update_values(force=True) self.update_bins_to_view()
def _on_layer_change(self, layer=None): with delay_callback(self, 'vmin', 'vmin'): if self.layer is None: self.att_helper.set_multiple_data([]) else: self.att_helper.set_multiple_data([self.layer])
def flip_data(self, *args): # FIXME: since the links will be the same in the list of current links, # we can make sure we reselect the same one as before - it would be # better if this didn't change in the first place though. _original_current_link = self.current_link with delay_callback(self, 'data1', 'data2'): self.data1, self.data2 = self.data2, self.data1 self.current_link = _original_current_link
def _update_combo_att(self): with delay_callback(self, 'x_att_world', 'y_att_world'): if self.reference_data is None: self.xw_att_helper.set_multiple_data([]) self.yw_att_helper.set_multiple_data([]) else: self.xw_att_helper.set_multiple_data([self.reference_data]) self.yw_att_helper.set_multiple_data([self.reference_data])
def on_mouse_or_key_event(self, data): from jdaviz.configs.imviz.helper import get_top_layer_index event = data['event'] # Note that we throttle this to 200ms here as changing the contrast # and bias it expensive since it forces the whole image to be redrawn if event == 'dragmove': if (time.time() - self._time_last) <= 0.2: return event_x = data['pixel']['x'] event_y = data['pixel']['y'] max_x = self.viewer.shape[1] max_y = self.viewer.shape[0] if ((event_x < 0) or (event_x >= max_x) or (event_y < 0) or (event_y >= max_y)): return # Normalize w.r.t. viewer display from 0 to 1 x = event_x / (max_x - 1) y = event_y / (max_y - 1) # When blinked, first layer might not be top layer i_top = get_top_layer_index(self.viewer) state = self.viewer.layers[i_top].state # bias range 0..1 # contrast range 0..4 with delay_callback(state, 'bias', 'contrast'): state.bias = x state.contrast = y * 4 self._time_last = time.time() elif event == 'dblclick': # When blinked, first layer might not be top layer i_top = get_top_layer_index(self.viewer) state = self.viewer.layers[i_top].state # Restore defaults that are applied on load with delay_callback(state, 'bias', 'contrast'): state.bias = 0.5 state.contrast = 1
def set_limits(self, x_min, x_max, y_min, y_max, z_min, z_max): with delay_callback(self, 'x_min', 'x_max', 'y_min', 'y_max', 'z_min', 'z_max'): self.x_min = x_min self.x_max = x_max self.y_min = y_min self.y_max = y_max self.z_min = z_min self.z_max = z_max
def remove_data(self, data): with delay_callback(self.state, 'layers'): for layer_state in self.state.layers[::-1]: if isinstance(layer_state.layer, BaseData): if layer_state.layer is data: self.state.layers.remove(layer_state) else: if layer_state.layer.data is data: self.state.layers.remove(layer_state)
def _on_layer_change(self, layer=None): with delay_callback(self, 'cmap_vmin', 'cmap_vmax', 'size_vmin', 'size_vmax'): if self.layer is None: self.cmap_att_helper.set_multiple_data([]) self.size_att_helper.set_multiple_data([]) self.img_data_att_helper.set_multiple_data([]) else: self.cmap_att_helper.set_multiple_data([self.layer]) self.size_att_helper.set_multiple_data([self.layer]) self.img_data_att_helper.set_multiple_data([self.layer])
def center_on(self, point): """Centers the view on a particular point. Parameters ---------- point : tuple or `~astropy.coordinates.SkyCoord` If tuple of ``(X, Y)`` is given, it is assumed to be in data coordinates and 0-indexed. Raises ------ AttributeError Sky coordinates are given but image does not have a valid WCS. """ viewer = self.app.get_viewer("viewer-1") i_top = get_top_layer_index(viewer) image = viewer.layers[i_top].layer if isinstance(point, SkyCoord): if data_has_valid_wcs(image): try: pix = image.coords.world_to_pixel(point) # 0-indexed X, Y except NoConvergence as e: # pragma: no cover self.app.hub.broadcast( SnackbarMessage( f'{point} is likely out of bounds: {repr(e)}', color="warning", sender=self.app)) return else: raise AttributeError( f'{getattr(image, "label", None)} does not have a valid WCS' ) else: pix = point # Disallow centering outside of display; image.shape is (Y, X) eps = sys.float_info.epsilon if (not np.all(np.isfinite(pix)) or pix[0] < -eps or pix[0] >= (image.shape[1] + eps) or pix[1] < -eps or pix[1] >= (image.shape[0] + eps)): self.app.hub.broadcast( SnackbarMessage(f'{pix} is out of bounds', color="warning", sender=self.app)) return with delay_callback(viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'): width = viewer.state.x_max - viewer.state.x_min height = viewer.state.y_max - viewer.state.y_min viewer.state.x_min = pix[0] - (width * 0.5) viewer.state.y_min = pix[1] - (height * 0.5) viewer.state.x_max = viewer.state.x_min + width viewer.state.y_max = viewer.state.y_min + height
def update_bins_to_view(self, *args): """ Update the bins to match the current view. """ with delay_callback(self, 'hist_x_min', 'hist_x_max'): if self.x_max > self.x_min: self.hist_x_min = self.x_min self.hist_x_max = self.x_max else: self.hist_x_min = self.x_max self.hist_x_max = self.x_min
def _on_density_map_change(self, *args): # If the density map mode is used, we should disable the lines/errors/vectors if self.density_map: with delay_callback(self, 'line_visible', 'xerr_visible', 'yerr_visible', 'vector_visible'): if self.line_visible: self.line_visible = False if self.xerr_visible: self.xerr_visible = False if self.yerr_visible: self.yerr_visible = False if self.vector_visible: self.vector_visible = False
def move(self, event): if not self.active or not self.pressed or not event.inaxes: return if self.mode == 'move-x-min': self.state.x_min = event.xdata elif self.mode == 'move-x-max': self.state.x_max = event.xdata elif self.mode == 'move': orig_click, orig_x_min, orig_x_max = self.move_params with delay_callback(self.state, 'x_min', 'x_max'): self.state.x_min = orig_x_min + (event.xdata - orig_click) self.state.x_max = orig_x_max + (event.xdata - orig_click)
def test_delay_global_callback(): # Regression test to make sure that delay_callback works for global # callbacks too. state = State() test1 = MagicMock() state.add_callback('a', test1) test2 = MagicMock() state.add_global_callback(test2) with delay_callback(state, 'a'): state.a = 100 assert test1.call_count == 0 assert test2.call_count == 0 test1.assert_called_once_with(100) test2.assert_called_once_with(a=100) test2.reset_mock() with delay_callback(state, 'a'): state.b = 200 assert test2.call_count == 1 test2.assert_called_once_with(b=200) test2.reset_mock() with delay_callback(state, 'a', 'b'): state.a = 300 state.b = 400 assert test2.call_count == 0 test2.assert_called_once_with(a=300, b=400)
def test_delay_global_callback_stub(): # Make sure that adding the global callback delay functionality doesn't # break things when we are dealing with a plain class without HasCallbackProperties stub = Stub() test1 = MagicMock() add_callback(stub, 'prop1', test1) with delay_callback(stub, 'prop1'): stub.prop1 = 100 assert test1.call_count == 0 test1.assert_called_once_with(100)
def test_delay_in_delayed_callback(): # Regression test for a bug that occurred if a delayed callback included # a delay itself. state = State() def callback(*args, **kwargs): with delay_callback(state, 'a'): state.a = 2 state.add_callback('a', callback) with delay_callback(state, 'a', 'b'): state.a = 100
def test_delay_multiple(): stub = Stub() test = MagicMock() test2 = MagicMock() add_callback(stub, 'prop1', test) add_callback(stub, 'prop2', test2) with delay_callback(stub, 'prop1', 'prop2'): stub.prop1 = 50 stub.prop1 = 100 stub.prop2 = 200 assert test.call_count == 0 assert test2.call_count == 0 test.assert_called_once_with(100) test2.assert_called_once_with(200)
def callback(*args, **kwargs): with delay_callback(state, 'a'): state.a = 2