diff --git a/CHANGELOG.md b/CHANGELOG.md index fd380bca..ddb56d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for `astropy.coordinates.SkyCoord` for assigning and reading the `target` property (#80) +- Support for `astropy.coordinates.Angle` for reading the `fov` property (#83) ### Fixed @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Change the jslink target trait from `target` to `shared_target` (#80) +- Change the jslink fov trait from `fov` to `shared_fov` (#83) ## [0.3.0] @@ -37,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the last clicked object (ra, dec, and catalog content) - Each ipyaladin version now point to a specific Aladin-lite version instead of the latest available version - add_table takes new arguments (documented here https://cds-astro.github.io/aladin-lite/Catalog.Catalog.html) -- the new method `add_moc` can take mocpy.MOC objects, URLs, or the dictionnary serialization of a MOC. This will replace `moc_from_URL` and `moc_from_dict` in the future. +- the new method `add_moc` can take mocpy.MOC objects, URLs, or the dictionary serialization of a MOC. This will replace `moc_from_URL` and `moc_from_dict` in the future. ## [0.2.6] diff --git a/README.md b/README.md index c310e552..0dd1bfd4 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Ipyaladin brings [Aladin-lite](https://github.com/cds-astro/aladin-lite) into no Correspondence table between ipyaladin versions and Aladin-lite versions: -| ipyaladin | Aladin-Lite | -| --------- | ----------- | -| 0.3.0 | 3.3.3-dev | +| ipyaladin | Aladin-Lite | +| ---------- | ----------- | +| 0.3.0 | 3.3.3-dev | +| unreleased | 3.4.0-beta | diff --git a/examples/10_Advanced-GUI.ipynb b/examples/10_Advanced-GUI.ipynb index e28d939c..dfdab0bc 100644 --- a/examples/10_Advanced-GUI.ipynb +++ b/examples/10_Advanced-GUI.ipynb @@ -92,7 +92,7 @@ "\n", "\n", "zoom_slider = widgets.FloatSlider(\n", - " value=180 / aladin.fov,\n", + " value=180 / aladin.fov.deg,\n", " min=1,\n", " max=400,\n", " step=1,\n", diff --git a/examples/6_Linked-widgets.ipynb b/examples/6_Linked-widgets.ipynb index 966b5fc4..f31212f9 100644 --- a/examples/6_Linked-widgets.ipynb +++ b/examples/6_Linked-widgets.ipynb @@ -31,8 +31,8 @@ "widgets.jslink((b, \"shared_target\"), (c, \"shared_target\"))\n", "\n", "# synchronize FoV (zoom level) between 3 widgets\n", - "widgets.jslink((a, \"fov\"), (b, \"fov\"))\n", - "widgets.jslink((b, \"fov\"), (c, \"fov\"))\n", + "widgets.jslink((a, \"shared_fov\"), (b, \"shared_fov\"))\n", + "widgets.jslink((b, \"shared_fov\"), (c, \"shared_fov\"))\n", "\n", "items = [a, b, c]\n", "\n", diff --git a/examples/7_on-click-callback.ipynb b/examples/7_on-click-callback.ipynb index 128f748e..90b1af0e 100644 --- a/examples/7_on-click-callback.ipynb +++ b/examples/7_on-click-callback.ipynb @@ -49,7 +49,7 @@ " dec,\n", " ra,\n", " dec,\n", - " aladin.fov / 10,\n", + " aladin.fov.deg / 10,\n", " )\n", "\n", " r = requests.get(\n", diff --git a/js/widget.js b/js/widget.js index 8be72e91..3d997108 100644 --- a/js/widget.js +++ b/js/widget.js @@ -1,4 +1,4 @@ -import A from "https://esm.sh/aladin-lite@3.3.3-beta"; +import A from "https://esm.sh/aladin-lite@3.4.0-beta"; import "./widget.css"; let idxView = 0; @@ -85,24 +85,25 @@ function render({ model, el }) { let fov_js = false; aladin.on("zoomChanged", (fov) => { - if (!fov_py) { - fov_js = true; - // fov MUST be cast into float in order to be sent to the model - model.set("fov", parseFloat(fov.toFixed(5))); - model.save_changes(); - } else { + if (fov_py) { fov_py = false; + return; } + fov_js = true; + // fov MUST be cast into float in order to be sent to the model + model.set("_fov", parseFloat(fov.toFixed(5))); + model.set("shared_fov", parseFloat(fov.toFixed(5))); + model.save_changes(); }); - model.on("change:fov", () => { - if (!fov_js) { - fov_py = true; - let fov = model.get("fov"); - aladin.setFoV(fov); - } else { + model.on("change:shared_fov", () => { + if (fov_js) { fov_js = false; + return; } + fov_py = true; + let fov = model.get("shared_fov"); + aladin.setFoV(fov); }); /* Div control */ @@ -193,6 +194,9 @@ function render({ model, el }) { model.on("msg:custom", (msg) => { let options = {}; switch (msg["event_name"]) { + case "change_fov": + aladin.setFoV(msg["fov"]); + break; case "goto_ra_dec": const ra = msg["ra"]; const dec = msg["dec"]; @@ -252,7 +256,7 @@ function render({ model, el }) { return () => { // need to unsubscribe the listeners model.off("change:shared_target"); - model.off("change:fov"); + model.off("change:shared_fov"); model.off("change:height"); model.off("change:coo_frame"); model.off("change:survey"); diff --git a/src/ipyaladin/__init__.py b/src/ipyaladin/__init__.py index 6c9fbe99..96a1133b 100644 --- a/src/ipyaladin/__init__.py +++ b/src/ipyaladin/__init__.py @@ -4,7 +4,7 @@ import warnings import anywidget -from astropy.coordinates import SkyCoord +from astropy.coordinates import SkyCoord, Angle from traitlets import ( Float, Int, @@ -43,7 +43,17 @@ class Aladin(anywidget.AnyWidget): help="A trait that can be used with `~ipywidgets.widgets.jslink`" "to link two Aladin Lite widgets targets together", ).tag(sync=True) - fov = Float(60.0).tag(sync=True, init_option=True) + _fov = Float( + 60.0, + help="A private trait that stores the current field of view of the widget." + " Its public version is the 'fov' property that returns an " + "`~astropy.units.Angle` object", + ).tag(sync=True, init_option=True) + shared_fov = Float( + 60.0, + help="A trait that can be used with `~ipywidgets.widgets.jslink`" + "to link two Aladin Lite widgets field of view together", + ).tag(sync=True) survey = Unicode("https://alaskybis.unistra.fr/DSS/DSSColor").tag( sync=True, init_option=True ) @@ -99,6 +109,7 @@ def _init_options(self): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.target = kwargs.get("target", "0 0") + self.fov = kwargs.get("fov", 60.0) self.on_msg(self._handle_custom_message) def _handle_custom_message(self, model, message, list_of_buffers): # noqa: ARG002 @@ -119,6 +130,28 @@ def _handle_custom_message(self, model, message, list_of_buffers): # noqa: ARG0 elif event_type == "select" and "select" in self.listener_callback: self.listener_callback["select"](message_content) + @property + def fov(self) -> Angle: + """The field of view of the Aladin Lite widget along the horizontal axis. + + It can be set with either a float in degrees + or an `~astropy.units.Angle` object. + + Returns + ------- + Angle + An astropy.units.Angle object representing the field of view. + + """ + return Angle(self._fov, unit="deg") + + @fov.setter + def fov(self, fov: Union[float, Angle]): + if isinstance(fov, Angle): + fov = fov.deg + self._fov = fov + self.send({"event_name": "change_fov", "fov": fov}) + @property def target(self) -> SkyCoord: """The target of the Aladin Lite widget. diff --git a/src/test/test_aladin.py b/src/test/test_aladin.py new file mode 100644 index 00000000..e7eba792 --- /dev/null +++ b/src/test/test_aladin.py @@ -0,0 +1,107 @@ +import pytest +from astropy.coordinates import Angle + +from ipyaladin import Aladin, parse_coordinate_string + +test_aladin_string_target = [ + "M 31", + "sgr a*", + "α Centauri", # noqa RUF001 + "* 17 Com", + "1:12:43.2 31:12:43", + "1:12:43.2 +31:12:43", + "1:12:43.2 -31:12:43", + "1 12 43.2 31 12 43", + "1 12 43.2 +31 12 43", + "1 12 43.2 -31 12 43", + "1h12m43.2s 1d12m43s", + "1h12m43.2s +1d12m43s", + "1h12m43.2s -1d12m43s", + "42.67 25.48", + "42.67 +25.48", + "42.67 -25.48", + "0 0", + "J42.67 25.48", + "G42.67 25.48", + "B42.67 25.48", + "J12 30 45 +45 30 15", + "J03 15 20 -10 20 30", + "G120.5 -45.7", + "G90 0", + "B60 30", + "B120 -45", +] + + +@pytest.mark.parametrize("target", test_aladin_string_target) +def test_aladin_string_target_set(target): + """Test setting the target of an Aladin object with a string or a SkyCoord object. + + Parameters + ---------- + target : str + The target string. + + """ + aladin = Aladin() + aladin.target = target + parsed_target = parse_coordinate_string(target) + assert aladin.target.icrs.ra.deg == parsed_target.icrs.ra.deg + assert aladin.target.icrs.dec.deg == parsed_target.icrs.dec.deg + + +@pytest.mark.parametrize("target", test_aladin_string_target) +def test_aladin_sky_coord_target_set(target): + """Test setting and getting the target of an Aladin object with a SkyCoord object. + + Parameters + ---------- + target : str + The target string. + + """ + sc_target = parse_coordinate_string(target) + aladin = Aladin() + aladin.target = sc_target + assert aladin.target.icrs.ra.deg == sc_target.icrs.ra.deg + assert aladin.target.icrs.dec.deg == sc_target.icrs.dec.deg + + +test_aladin_float_fov = [ + 0, + 360, + 180, + -180, + 720, +] + + +@pytest.mark.parametrize("angle", test_aladin_float_fov) +def test_aladin_float_fov_set(angle): + """Test setting the angle of an Aladin object with a float. + + Parameters + ---------- + angle : float + The angle to set. + + """ + aladin = Aladin() + aladin.fov = angle + assert aladin.fov.deg == angle + + +@pytest.mark.parametrize("angle", test_aladin_float_fov) +def test_aladin_angle_fov_set(angle): + """Test setting the angle of an Aladin object with an Angle object. + + Parameters + ---------- + angle : float + The angle to set. + + """ + angle_fov = Angle(angle, unit="deg") + aladin = Aladin() + aladin.fov = angle_fov + assert aladin.fov.deg == angle_fov.deg diff --git a/src/test/test_coordinate_parser.py b/src/test/test_coordinate_parser.py index 6d4f78f1..e0cd3ff6 100644 --- a/src/test/test_coordinate_parser.py +++ b/src/test/test_coordinate_parser.py @@ -1,4 +1,3 @@ -from ipyaladin import Aladin from ipyaladin.coordinate_parser import ( parse_coordinate_string, _split_coordinate_string, @@ -188,67 +187,3 @@ def test_parse_coordinate_string(inp, expected): """ assert parse_coordinate_string(inp) == expected - - -test_aladin_string_target = [ - "M 31", - "sgr a*", - "α Centauri", # noqa RUF001 - "* 17 Com", - "1:12:43.2 31:12:43", - "1:12:43.2 +31:12:43", - "1:12:43.2 -31:12:43", - "1 12 43.2 31 12 43", - "1 12 43.2 +31 12 43", - "1 12 43.2 -31 12 43", - "1h12m43.2s 1d12m43s", - "1h12m43.2s +1d12m43s", - "1h12m43.2s -1d12m43s", - "42.67 25.48", - "42.67 +25.48", - "42.67 -25.48", - "0 0", - "J42.67 25.48", - "G42.67 25.48", - "B42.67 25.48", - "J12 30 45 +45 30 15", - "J03 15 20 -10 20 30", - "G120.5 -45.7", - "G90 0", - "B60 30", - "B120 -45", -] - - -@pytest.mark.parametrize("target", test_aladin_string_target) -def test_aladin_string_target_set(target): - """Test setting the target of an Aladin object with a string or a SkyCoord object. - - Parameters - ---------- - target : str - The target string. - - """ - aladin = Aladin() - aladin.target = target - parsed_target = parse_coordinate_string(target) - assert aladin.target.icrs.ra.deg == parsed_target.icrs.ra.deg - assert aladin.target.icrs.dec.deg == parsed_target.icrs.dec.deg - - -@pytest.mark.parametrize("target", test_aladin_string_target) -def test_aladin_sky_coord_target_set(target): - """Test setting and getting the target of an Aladin object with a SkyCoord object. - - Parameters - ---------- - target : str - The target string. - - """ - sc_target = parse_coordinate_string(target) - aladin = Aladin() - aladin.target = sc_target - assert aladin.target.icrs.ra.deg == sc_target.icrs.ra.deg - assert aladin.target.icrs.dec.deg == sc_target.icrs.dec.deg