Skip to content

Commit 9a705b7

Browse files
authored
Merge pull request #1 from cpparts/CST-248-orientation_investigation
add: changing viewer rotation
2 parents 048c9f8 + 58caaaa commit 9a705b7

8 files changed

Lines changed: 220 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
## [Unreleased]
1818

19+
## Added
20+
21+
- `rotation` parameter can now be called to rotate the ipyaladin viewer [#146]
22+
1923
## [0.6.0]
2024

2125
## Added

examples/02_Base_Commands.ipynb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,31 @@
8888
"aladin.fov"
8989
]
9090
},
91+
{
92+
"cell_type": "markdown",
93+
"metadata": {},
94+
"source": [
95+
"The orientation of the view (view center to north pole angle in degrees) can be set"
96+
]
97+
},
98+
{
99+
"cell_type": "code",
100+
"execution_count": null,
101+
"metadata": {},
102+
"outputs": [],
103+
"source": [
104+
"aladin.rotation = 180"
105+
]
106+
},
107+
{
108+
"cell_type": "code",
109+
"execution_count": null,
110+
"metadata": {},
111+
"outputs": [],
112+
"source": [
113+
"aladin.rotation"
114+
]
115+
},
91116
{
92117
"cell_type": "markdown",
93118
"metadata": {},
@@ -134,7 +159,7 @@
134159
"cell_type": "markdown",
135160
"metadata": {},
136161
"source": [
137-
"The target and field of view can be set with astropy objects"
162+
"The target, rotation, and field of view can be set with astropy objects"
138163
]
139164
},
140165
{
@@ -146,6 +171,15 @@
146171
"aladin.target = SkyCoord(\"12h00m00s\", \"-30d00m00s\", frame=\"icrs\")"
147172
]
148173
},
174+
{
175+
"cell_type": "code",
176+
"execution_count": null,
177+
"metadata": {},
178+
"outputs": [],
179+
"source": [
180+
"aladin.rotation = Angle(0, \"deg\")"
181+
]
182+
},
149183
{
150184
"cell_type": "code",
151185
"execution_count": null,

examples/11_Extracting_information_from_the_view.ipynb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"aladin.survey = \"CDS/P/PLANCK/R2/HFI/color\"\n",
8282
"aladin.target = \"LMC\"\n",
8383
"aladin.frame = \"Galactic\"\n",
84-
"aladin.fov = 50"
84+
"aladin.fov = 50\n",
85+
"aladin.rotation = 90"
8586
]
8687
},
8788
{
@@ -124,6 +125,26 @@
124125
"aladin.fov_xy # Recover the current field of view for the x and y axis"
125126
]
126127
},
128+
{
129+
"cell_type": "markdown",
130+
"id": "fcd830c2",
131+
"metadata": {},
132+
"source": [
133+
"## Getting the rotation\n",
134+
"\n",
135+
"The rotation of the view (view center to north pole angle in degrees) can be found with:"
136+
]
137+
},
138+
{
139+
"cell_type": "code",
140+
"execution_count": null,
141+
"id": "8c20e7c1",
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"aladin.rotation # Recover the current rotation of the view"
146+
]
147+
},
127148
{
128149
"cell_type": "markdown",
129150
"id": "5ef087ca59d890ce",

js/models/event_handler.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ export default class EventHandler {
4646
this.model.set("_wcs", this.aladin.getViewWCS());
4747
}
4848

49+
/**
50+
* Updates the view center rotation in the model.
51+
* WARNING: This method doesn't call model.save_changes()!
52+
*/
53+
updateRotation() {
54+
if (!this.isLastDiv()) return;
55+
this.model.set("_rotation", this.aladin.getRotation());
56+
}
57+
4958
/**
5059
* Updates the 2-axis FoV in the model.
5160
* WARNING: This method don't call model.save_changes()!
@@ -138,6 +147,15 @@ export default class EventHandler {
138147
this.model.save_changes();
139148
});
140149

150+
/* Rotation control */
151+
this.model.on("change:_rotation", () => {
152+
this.updateRotation(this.model.get("_rotation"), this.aladinDiv);
153+
// Update WCS and FoV only if this is the last div
154+
this.updateWCS();
155+
this.update2AxisFoV();
156+
this.model.save_changes();
157+
});
158+
141159
/* Aladin callbacks */
142160

143161
this.aladin.on("cooFrameChanged", () => {
@@ -184,6 +202,17 @@ export default class EventHandler {
184202
}
185203
});
186204

205+
this.aladin.on("rotationChanged", (object) => {
206+
if (object["data"] !== undefined) {
207+
this.model.send({
208+
event_type: "rotation_changed",
209+
content: {
210+
rotation: object["rotation"],
211+
},
212+
});
213+
}
214+
});
215+
187216
this.aladin.on("objectClicked", (clicked) => {
188217
if (clicked) {
189218
let clickedContent = {
@@ -273,6 +302,7 @@ export default class EventHandler {
273302
this.eventHandlers = {
274303
add_marker: this.messageHandler.handleAddMarker,
275304
change_fov: this.messageHandler.handleChangeFoV,
305+
change_rotation: this.messageHandler.handleChangeRotation,
276306
goto_ra_dec: this.messageHandler.handleGotoRaDec,
277307
save_view_as_image: this.messageHandler.handleSaveViewAsImage,
278308
add_fits: this.messageHandler.handleAddFits,
@@ -302,6 +332,7 @@ export default class EventHandler {
302332
this.model.off("change:_target");
303333
this.model.off("change:_fov");
304334
this.model.off("change:_height");
335+
this.model.off("change:_rotation");
305336
this.model.off("change:coo_frame");
306337
this.model.off("change:survey");
307338
this.model.off("change:overlay_survey");

js/models/message_handler.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export default class MessageHandler {
3838
this.aladin.gotoRaDec(msg["ra"], msg["dec"]);
3939
}
4040

41+
handleChangeRotation(msg) {
42+
this.aladin.setRotation(msg["rotation"]);
43+
}
44+
4145
async handleSaveViewAsImage(msg) {
4246
const path = msg["path"];
4347
const format = msg["format"];

js/widget.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ function initAladinLite(model, el) {
3131
const raDec = model.get("_target").split(" ");
3232
aladin.gotoRaDec(raDec[0], raDec[1]);
3333

34-
// Set current FoV and WCS
34+
// Set current FoV, WCS, and rotation
3535
const twoAxisFoV = { ...aladin.getFov() };
3636
model.set("_fov_xy", {
3737
x: twoAxisFoV[0],
3838
y: twoAxisFoV[1],
3939
});
4040
const wcs = { ...aladin.getViewWCS() };
4141
model.set("_wcs", wcs);
42+
const rotation = { ...aladin.getRotation() };
43+
model.set("_rotation", rotation);
44+
4245
model.set("_is_loaded", true);
4346
model.save_changes();
4447

src/ipyaladin/widget.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class Aladin(anywidget.AnyWidget):
143143
# Options for the view initialization
144144
_init_options = traitlets.Dict().tag(sync=True)
145145
_height = Int(400).tag(sync=True, init_option=True)
146+
_rotation = Float(0).tag(sync=True, init_option=True)
146147
_target = Unicode(
147148
"0 0",
148149
help="A private trait that stores the current target of the widget in a string."
@@ -220,8 +221,12 @@ def __init__(self, *args: any, **init_options: any) -> None:
220221
# https://github.com/jupyter-widgets/ipywidgets/blob/main/python/ipywidgets/ipywidgets/widgets/domwidget.py
221222
for key in ["layout", "tabbable", "tooltip"]:
222223
init_options.pop(key, None)
224+
init_options["north_pole_orientation"] = init_options.get(
225+
"north_pole_orientation", init_options.get("rotation", 0)
226+
)
223227
# some init options are properties here
224228
self.height = init_options.get("height", self._height)
229+
self.rotation = init_options.get("north_pole_orientation", self._rotation)
225230
self.target = init_options.get("target", self._target)
226231
self.fov = init_options.get("fov", self._fov)
227232
# apply different default options from Aladin-Lite
@@ -294,6 +299,38 @@ def height(self, height: int) -> None:
294299
self._fov_xy = {}
295300
self._height = height
296301

302+
@property
303+
def rotation(self) -> float:
304+
"""The center rotation of the widget.
305+
306+
This is the view center to north pole angle in degrees.
307+
This is equivalent to getting the 3rd Euler angle.
308+
Positive angles rotates the view in the counter clockwise
309+
order (or towards the east).
310+
311+
It can be set with either a float number in degrees
312+
or an astropy.coordinates.Angle object.
313+
314+
Returns
315+
-------
316+
astropy.coordinates.Angle
317+
An astropy.coordinates.Angle object representing the center
318+
rotation of the widget in degrees. The default rotation is
319+
0 degrees.
320+
"""
321+
return Angle(self._rotation, unit="deg")
322+
323+
@rotation.setter
324+
def rotation(self, rotation: Union[float, Angle]) -> None:
325+
if isinstance(rotation, Angle):
326+
rotation = rotation.deg
327+
if np.isclose(self._rotation, rotation):
328+
return
329+
self._wcs = {}
330+
self._fov_xy = {}
331+
self._rotation = rotation
332+
self.send({"event_name": "change_rotation", "rotation": rotation})
333+
297334
@property
298335
def wcs(self) -> WCS:
299336
"""The world coordinate system corresponding to the current view of ipyaladin.

src/tests/test_aladin.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,89 @@ def test_aladin_angle_fov_set(angle: float) -> None:
129129
assert aladin.fov.deg == angle_fov.deg
130130

131131

132+
test_aladin_float_rotation = [
133+
0,
134+
360,
135+
180,
136+
-180,
137+
720,
138+
]
139+
140+
141+
@pytest.mark.parametrize("angle", test_aladin_float_rotation)
142+
def test_aladin_float_rotation_set(angle: float) -> None:
143+
"""Test setting the rotation of an Aladin object with a float.
144+
145+
Parameters
146+
----------
147+
angle : float
148+
The angle to set.
149+
150+
"""
151+
aladin.rotation = angle
152+
assert aladin.rotation.deg == angle
153+
154+
155+
@pytest.mark.parametrize("angle", test_aladin_float_rotation)
156+
def test_aladin_angle_rotation_set(angle: float) -> None:
157+
"""Test setting the rotation of an Aladin object with an Angle object.
158+
159+
Parameters
160+
----------
161+
angle : float
162+
The angle to set.
163+
164+
"""
165+
angle_rotation = Angle(angle, unit="deg")
166+
aladin.rotation = angle_rotation
167+
assert aladin.rotation.deg == angle_rotation.deg
168+
169+
170+
@pytest.mark.parametrize("angle", test_aladin_float_rotation)
171+
def test_aladin_init_rotation(angle: float) -> None:
172+
"""Test initializing an Aladin object with rotation set.
173+
174+
Parameters
175+
----------
176+
angle : float
177+
The angle to set.
178+
179+
"""
180+
test_aladin = Aladin(rotation=angle)
181+
assert test_aladin.rotation.deg == angle
182+
183+
184+
@pytest.mark.parametrize("angle", test_aladin_float_rotation)
185+
def test_aladin_init_north_pole_orientation(angle: float) -> None:
186+
"""Test initializing Aladin object with north_pole_orientation set.
187+
188+
Parameters
189+
----------
190+
angle : float
191+
The angle to set.
192+
193+
"""
194+
test_aladin = Aladin(north_pole_orientation=angle)
195+
assert test_aladin.rotation.deg == angle
196+
197+
198+
@pytest.mark.parametrize("angle", test_aladin_float_rotation)
199+
def test_aladin_init_north_pole_orientation_overrides_rotation(angle: float) -> None:
200+
"""
201+
Test init Aladin object with north_pole_orientation and rotation set.
202+
203+
north_pole_orientation should override rotation.
204+
205+
Parameters
206+
----------
207+
angle : float
208+
The angle to set.
209+
210+
"""
211+
test_aladin = Aladin(north_pole_orientation=angle, rotation=180)
212+
assert test_aladin.rotation.deg == angle
213+
214+
132215
test_stcs_iterables = [
133216
"CIRCLE ICRS 258.93205686 43.13632863 0.625",
134217
[

0 commit comments

Comments
 (0)