Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas rotation #1983

Merged
merged 33 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bbbdc93
rotate the entire viewer widget
kecnry Jun 9, 2022
fba75ee
WIP: oversized canvas
kecnry Jun 17, 2022
0614424
Expose canvas rotation in new plugin
pllim Jun 21, 2022
9144f26
WIP: Disable zoom limits when canvas rotated
pllim Jun 21, 2022
14ab342
WIP: Investigation went nowhere.
pllim Jun 22, 2022
ac5fb71
use PluginTemplate to enable user api
kecnry Jan 17, 2023
271b133
canvas rotation live-update and slider
kecnry Feb 20, 2023
91a2ec1
rename plugin, cleanup code, enable user API
kecnry Feb 27, 2023
8df7619
rotate compass image along with viewer
kecnry Feb 27, 2023
3870e0c
remove unused codeblock
kecnry Feb 27, 2023
8c7498c
add viewer_requirements to plugin
kecnry Feb 28, 2023
1137d6a
changes entry
kecnry Feb 28, 2023
0d6fbb1
fix multi-viewer support
kecnry Feb 28, 2023
4e6d912
basic plugin docs
kecnry Feb 28, 2023
3addc58
remove oversized canvas attempt, code cleanup
kecnry Feb 28, 2023
7b4e353
support horizontal flipping and north up east left/right presets
kecnry Feb 28, 2023
6d462aa
API docs
kecnry Feb 28, 2023
688dffd
fix styling of plugin description text
kecnry Mar 1, 2023
db7382f
fix failing mastviz test for canvas rotation plugin
kecnry Mar 1, 2023
b7c3f57
only allow orientation methods if data has WCS
kecnry Mar 1, 2023
07a9039
test coverage
kecnry Mar 1, 2023
255624f
handle case where no data loaded in viewer
kecnry Mar 1, 2023
4850c63
fix preset from WCS for non-default image viewer
kecnry Mar 2, 2023
3329ccf
Apply suggestions from code review
kecnry Mar 7, 2023
bc20ae4
consistent internal terminology
kecnry Mar 7, 2023
e326adc
revert hiding zoom box in compass when rotated
kecnry Mar 7, 2023
92018ca
method/button to reset orientation
kecnry Mar 8, 2023
1fcc539
add text in docs explaining how to achieve vertical flip
kecnry Mar 8, 2023
1d21a25
fix reference to resizeViewer
kecnry Mar 8, 2023
94e5d7f
avoid horizontal scrollbars in viewer with rotated canvas
kecnry Mar 14, 2023
da3637e
fix bug for applying existing state when switching viewers
kecnry Mar 15, 2023
023d806
custom compass icons
kecnry Mar 24, 2023
237ea1b
bump bqplot
kecnry Mar 29, 2023
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
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Cubeviz
Imviz
^^^^^

- Table exposing past results in the aperture photometry plugin. [#1985, #2015]

- New canvas rotation plugin to rotate displayed image without affecting actual data. [#1983]

Mosviz
^^^^^^

Expand Down
22 changes: 22 additions & 0 deletions docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ For an image with a valid WCS, the compass will show directions to North (N)
and East (E) for ICRS sky coordinates. It also shows the currently displayed
data label, the X and Y directions, and the zoom box.

Note that when the axes canvas is rotated (by :ref:`rotate-canvas`), the zoom box corresponds
to the set zoom limits, not the extent of the viewer. Instead, the compass image itself is
shown rotated/flipped to the same orientation.

When you have multiple viewers created in Imviz, use the Viewer dropdown menu
to change the active viewer that it tracks.

Expand Down Expand Up @@ -285,6 +289,24 @@ are not stored. To save the current result before submitting a new query, you ca
The table returned from the API above may cover more sources than shown in the currently zoomed-in
portion of the image. Additional steps will be needed to filter out these points, if necessary.

.. _rotate-canvas:

Canvas Rotation
===============

The canvas rotation plugin allows rotating and horizontally flipping the image to any arbitrary
value by rotating the canvas axes themselves. Note that this does not affect the underlying data, and
exporting data to the notebook via the API will therefore not exhibit the same rotation.

The :ref:`imviz-compass` will also rotate (and flip) accordingly, but will show the zoom box
corresponding to the zoom limits, not the region shown in the viewer itself.

Presets are provided to reset the orientation as well as to set north up and east either to the
right or the left, as well as a slider and input to set the angle and a switch to set whether the
axes should be flipped horizontally after applying the rotation (a vertical flip can be achieved
via a 180 deg rotation and a horizontal flip).


.. _imviz-export-plot:

Export Plot
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ Plugins
.. automodapi:: jdaviz.configs.imviz.plugins.links_control.links_control
:no-inheritance-diagram:

.. automodapi:: jdaviz.configs.imviz.plugins.rotate_canvas.rotate_canvas
:no-inheritance-diagram:

.. automodapi:: jdaviz.configs.mosviz.plugins.row_lock.row_lock
:no-inheritance-diagram:

Expand Down
4 changes: 3 additions & 1 deletion jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1270,7 +1270,7 @@ def _viewer_by_reference(self, reference):

Returns
-------
`~glue_jupyter.bqplot.common.BqplotBaseView`
viewer : `~glue_jupyter.bqplot.common.BqplotBaseView`
The viewer class instance.
"""
viewer_item = self._viewer_item_by_reference(reference)
Expand Down Expand Up @@ -1645,6 +1645,8 @@ def _create_viewer_item(self, viewer, vid=None, name=None, reference=None):
'viewer_options': "IPY_MODEL_" + viewer.viewer_options.model_id,
'selected_data_items': {}, # noqa data_id: visibility state (visible, hidden, mixed), READ-ONLY
'visible_layers': {}, # label: {color, label_suffix}, READ-ONLY
'canvas_angle': 0, # canvas rotation clockwise rotation angle in deg
'canvas_flip_horizontal': False, # canvas rotation horizontal flip
'config': self.config, # give viewer access to app config/layout
'data_open': False,
'collapse': True,
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/imviz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ tray:
- imviz-line-profile-xy
- imviz-aper-phot-simple
- imviz-catalogs
- imviz-rotate-canvas
- g-export-plot
viewer_area:
- container: col
Expand Down
3 changes: 2 additions & 1 deletion jdaviz/configs/imviz/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
from .compass import * # noqa
from .aper_phot_simple import * # noqa
from .line_profile_xy import * # noqa
from .catalogs import * # noqa
from .catalogs import * # noqa
from .rotate_canvas import * # noqa
21 changes: 19 additions & 2 deletions jdaviz/configs/imviz/plugins/compass/compass.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from traitlets import Unicode, observe
from traitlets import Bool, Float, Unicode, observe

from jdaviz.core.events import AddDataMessage, RemoveDataMessage
from jdaviz.core.events import AddDataMessage, RemoveDataMessage, CanvasRotationChangedMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin, ViewerSelectMixin
from jdaviz.core.user_api import PluginUserApi
Expand All @@ -26,12 +26,15 @@ class Compass(PluginTemplateMixin, ViewerSelectMixin):
icon = Unicode("").tag(sync=True)
data_label = Unicode("").tag(sync=True)
img_data = Unicode("").tag(sync=True)
canvas_angle = Float(0).tag(sync=True) # set by canvas rotation plugin
canvas_flip_horizontal = Bool(False).tag(sync=True) # set by canvas rotation plugin

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.hub.subscribe(self, AddDataMessage, handler=self._on_viewer_data_changed)
self.hub.subscribe(self, RemoveDataMessage, handler=self._on_viewer_data_changed)
self.hub.subscribe(self, CanvasRotationChangedMessage, handler=self._on_canvas_rotation_changed) # noqa

@property
def user_api(self):
Expand All @@ -46,6 +49,18 @@ def _on_viewer_data_changed(self, msg=None):
viewer = self.viewer.selected_obj
viewer.on_limits_change() # Force redraw

def _on_canvas_rotation_changed(self, msg=None):
viewer_id = msg.viewer_id
if viewer_id != self.viewer_selected:
return

self._set_compass_rotation()

def _set_compass_rotation(self):
viewer_item = self.app._viewer_item_by_id(self.viewer.selected_id)
self.canvas_angle = viewer_item.get('canvas_angle', 0) # noqa
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Not sure why # noqa is needed here.

Suggested change
self.canvas_angle = viewer_item.get('canvas_angle', 0) # noqa
self.canvas_angle = viewer_item.get('canvas_angle', 0)

self.canvas_flip_horizontal = viewer_item.get('canvas_flip_horizontal', False)

@observe("viewer_selected", "plugin_opened")
def _compass_with_new_viewer(self, *args, **kwargs):
if not hasattr(self, 'viewer'):
Expand All @@ -57,6 +72,8 @@ def _compass_with_new_viewer(self, *args, **kwargs):
if vid == self.viewer.selected_id and (self.plugin_opened or kwargs.get('from_show')):
viewer.compass = self
viewer.on_limits_change() # Force redraw

self._set_compass_rotation()
else:
viewer.compass = None

Expand Down
17 changes: 16 additions & 1 deletion jdaviz/configs/imviz/plugins/compass/compass.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,26 @@
</v-chip>
</v-row>

<img class='invert-in-dark' v-if="img_data" :src="`data:image/png;base64,${img_data}`" style="width: 100%; max-width: 400px" />
<img class='invert-in-dark' v-if="img_data" :src="`data:image/png;base64,${img_data}`" :style="'width: 100%; max-width: 400px; margin-top: 50px; transform: rotateY('+viewer_rotateY(canvas_flip_horizontal)+') rotate('+canvas_angle+'deg)'" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯


</j-tray-plugin>
</template>

<script>
module.exports = {
methods: {
viewer_rotateY(canvas_flip_horizontal) {
if (canvas_flip_horizontal) {
return '180deg'
} else {
return '0deg'
}
}
}
};
</script>


<style>
.theme--dark .invert-in-dark {
filter: brightness(0.88) invert(1);
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/plugins/rotate_canvas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .rotate_canvas import * # noqa
136 changes: 136 additions & 0 deletions jdaviz/configs/imviz/plugins/rotate_canvas/rotate_canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import os
from traitlets import Bool, Unicode, observe

from glue_jupyter.common.toolbar_vuetify import read_icon

from jdaviz.configs.imviz.wcs_utils import get_compass_info
from jdaviz.core.custom_traitlets import FloatHandleEmpty
from jdaviz.core.events import AddDataMessage, RemoveDataMessage, CanvasRotationChangedMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin, ViewerSelectMixin
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.tools import ICON_DIR

__all__ = ['RotateCanvas']


@tray_registry('imviz-rotate-canvas', label="Canvas Rotation", viewer_requirements='image')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewer_requirements='image' eh? Are you sure users won't want to rotate spectrum viewer in the future so they don't have to subtract a monotonously increasing/decreasing continuum? 😉

class RotateCanvas(PluginTemplateMixin, ViewerSelectMixin):
"""
See the :ref:`Canvas Rotation Plugin Documentation <rotate-canvas>` for more details.

Only the following attributes and methods are available through the
:ref:`public plugin API <plugin-apis>`:

* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* ``viewer`` (:class:`~jdaviz.core.template_mixin.ViewerSelect`):
Viewer to show orientation/compass information.
* ``angle``:
Angle to rotate the axes canvas, clockwise.
* ``flip_horizontal``:
Whether to flip the canvas horizontally, after applying rotation.
* :meth:`reset`
* :meth:`set_north_up_east_left`
* :meth:`set_north_up_east_right`
"""
template_file = __file__, "rotate_canvas.vue"

angle = FloatHandleEmpty(0).tag(sync=True) # degrees, clockwise
flip_horizontal = Bool(False).tag(sync=True) # horizontal flip applied after rotation
has_wcs = Bool(False).tag(sync=True)

icon_nuer = Unicode(read_icon(os.path.join(ICON_DIR, 'right-east.svg'), 'svg+xml')).tag(sync=True) # noqa
icon_nuel = Unicode(read_icon(os.path.join(ICON_DIR, 'left-east.svg'), 'svg+xml')).tag(sync=True) # noqa

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.hub.subscribe(self, AddDataMessage, handler=self._on_viewer_data_changed)
self.hub.subscribe(self, RemoveDataMessage, handler=self._on_viewer_data_changed)

@property
def user_api(self):
return PluginUserApi(self, expose=('viewer', 'angle', 'flip_horizontal', 'reset',
'set_north_up_east_right', 'set_north_up_east_left'))

@property
def ref_data(self):
return self.app.get_viewer_by_id(self.viewer.selected_id).state.reference_data

def _on_viewer_data_changed(self, msg=None):
if not self.viewer_selected: # pragma: no cover
return
self.has_wcs = getattr(self.ref_data, 'coords', None) is not None

@observe('viewer_selected')
def _viewer_selected_changed(self, *args, **kwargs):
if not hasattr(self, 'viewer'):
return
vid = self.viewer.selected_id
self.angle = self.app._viewer_item_by_id(vid).get('canvas_angle', 0)
self.flip_horizontal = self.app._viewer_item_by_id(vid).get('canvas_flip_horizontal', False)

def _get_wcs_angles(self):
if not self.has_wcs:
raise ValueError("reference data does not have WCS, cannot determine orientation")
ref_data = self.ref_data
if ref_data is None: # pragma: no cover
raise ValueError("no data loaded in viewer, cannot determine orientation")
_, _, _, _, _, _, degn, dege, flip = get_compass_info(ref_data.coords, ref_data.shape)
return degn, dege, flip

def reset(self):
"""
Reset the rotation to an angle of 0 and no flip
"""
self.angle = 0
self.flip_horizontal = False

def set_north_up_east_left(self):
"""
Set the rotation angle and flip to achieve North up and East left according to the reference
image WCS.
"""
degn, dege, flip = self._get_wcs_angles()
self.angle = -degn
self.flip_horizontal = flip

def set_north_up_east_right(self):
"""
Set the rotation angle and flip to achieve North up and East right according to the
reference image WCS.
"""
degn, dege, flip = self._get_wcs_angles()
self.angle = -degn
self.flip_horizontal = not flip

def vue_reset(self, *args, **kwargs):
self.reset() # pragma: no cover

def vue_set_north_up_east_left(self, *args, **kwargs):
self.set_north_up_east_left() # pragma: no cover

def vue_set_north_up_east_right(self, *args, **kwargs):
self.set_north_up_east_right() # pragma: no cover

@observe('angle')
def _angle_changed(self, *args, **kwargs):
try:
angle = float(self.angle)
except ValueError: # pragma: no cover
# empty string, etc
angle = 0

# Rotate selected viewer canvas. This changes zoom too.
self.app._viewer_item_by_id(self.viewer.selected_id)['canvas_angle'] = angle
# broadcast message (used by compass, etc)
self.hub.broadcast(CanvasRotationChangedMessage(self.viewer.selected_id,
angle, self.flip_horizontal, sender=self))

@observe('flip_horizontal')
def _flip_changed(self, *args, **kwargs):
self.app._viewer_item_by_id(self.viewer.selected_id)['canvas_flip_horizontal'] = self.flip_horizontal # noqa
self.hub.broadcast(CanvasRotationChangedMessage(self.viewer.selected_id,
self.angle, self.flip_horizontal,
sender=self))
64 changes: 64 additions & 0 deletions jdaviz/configs/imviz/plugins/rotate_canvas/rotate_canvas.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<j-tray-plugin
description="Rotate viewer canvas to any orientation (note: this does not affect the underlying data)."
:link="'https://jdaviz.readthedocs.io/en/'+vdocs+'/'+config+'/plugins.html#canvas-rotation'"
:popout_button="popout_button">

<plugin-viewer-select
:items="viewer_items"
:selected.sync="viewer_selected"
label="Viewer"
hint="Select viewer."
/>

<v-row>
<span style="line-height: 36px">Presets:</span>
<j-tooltip tooltipcontent="reset rotation and flip">
<v-btn icon @click="reset">
<v-icon>mdi-restore</v-icon>
</v-btn>
</j-tooltip>
<j-tooltip v-if="has_wcs" tooltipcontent="north up, east right">
<v-btn icon @click="set_north_up_east_right">
<img :src="icon_nuer" width="24" class="invert-if-dark" style="opacity: 0.65"/>
</v-btn>
</j-tooltip>
<j-tooltip v-if="has_wcs" tooltipcontent="north up, east left">
<v-btn icon @click="set_north_up_east_left">
<img :src="icon_nuel" width="24" class="invert-if-dark" style="opacity: 0.65"/>
</v-btn>
</j-tooltip>
</v-row>

<v-row>
<v-slider
v-model="angle"
class="align-center"
max="180"
min="-180"
step="1"
color="#00617E"
track-color="#00617E"
thumb-color="#153A4B"
hide-details
>
</v-slider>
</v-row>

<v-row>
<v-col>
<v-text-field
v-model.number="angle"
type="number"
label="Angle"
hint="Rotation angle in degrees clockwise"
></v-text-field>
</v-col>
</v-row>

<v-row>
<v-switch v-model="flip_horizontal" label="Flip horizontally after rotation"></v-switch>
</v-row>

</j-tray-plugin>
</template>
Empty file.
Loading