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

Merge SkyCoord support to Master #80

Merged
merged 28 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bc50a28
:sparkles: Implement SkyCoord support for js side
Xen0Xys Apr 17, 2024
3490795
:sparkles: Implement SkyCoord support for python side
Xen0Xys Apr 17, 2024
8fb0a58
:bug: Fix target support for jslink using shared_target traitlets
Xen0Xys Apr 17, 2024
c9cbc40
:memo: Update example 6 to fix target jslink behavior
Xen0Xys Apr 17, 2024
c229db9
:bug: Fix multi-instances flickering
Xen0Xys Apr 17, 2024
9be2c71
:bug: Fix useless target when used in constructor
Xen0Xys Apr 17, 2024
5680fd1
:sparkles: Implement coordinate string parsing using new coordinate_p…
Xen0Xys Apr 18, 2024
066e71f
:memo: Add minor documentation for widget.js
Xen0Xys Apr 18, 2024
8d99542
:rocket: Add missing target getter type annotation
Xen0Xys Apr 18, 2024
c2e0fda
:bug: Fix coordinate_parser.py splitter
Xen0Xys Apr 18, 2024
63f903d
:white_check_mark: Add tests for coordinate_parser.py and aladin targ…
Xen0Xys Apr 18, 2024
4b8e898
:construction_worker: Add CI for automated python code testing
Xen0Xys Apr 18, 2024
02d138a
:white_check_mark: Add more testing cases for existing tests
Xen0Xys Apr 18, 2024
dec02bc
:bug: Fix initial target when frame is galactic
Xen0Xys Apr 18, 2024
9c36e6b
:art: Use a regex to detect if a string is an object name or a coordi…
Xen0Xys Apr 18, 2024
e9ae30b
:memo: Update CHANGELOG.md
Xen0Xys Apr 18, 2024
a0d2a09
:memo: Add _target and shared_target trait help & remove useless erro…
Xen0Xys Apr 18, 2024
38c6695
:art: Fix python import order
Xen0Xys Apr 18, 2024
4f2f168
:bug: Fix missing event unsubscribe for shared_target
Xen0Xys Apr 18, 2024
f55c2e2
:art: Improve conditional structure for the target setter
Xen0Xys Apr 18, 2024
a271cb4
:memo: Improve the meaning of a sentence in the changelog
Xen0Xys Apr 18, 2024
644b4c3
:art: Improve target setter conditions
Xen0Xys Apr 22, 2024
5c816c6
:sparkles: Add parsing support for coordinate string starting by J, G…
Xen0Xys Apr 22, 2024
cb99a4b
:white_check_mark: Improve testing for new coordinate parsing functions
Xen0Xys Apr 22, 2024
b71946c
:memo: Change docstring format from sphinx to numpy
Xen0Xys Apr 22, 2024
cfc112f
:art: Improve conditional structure for parse_coordinate_string function
Xen0Xys Apr 22, 2024
5e177dd
fix: type annotations union for python <3.9
ManonMarchand Apr 22, 2024
815c6bf
docs: fix doctring style
ManonMarchand Apr 22, 2024
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
25 changes: 25 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: python-tests
on:
push:
branches:
- master
pull_request:
branches:
- master
# Allows to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
python-tests:
runs-on: ubuntu-latest
steps:
- name: "Checkout branch"
uses: actions/checkout@v4
- name: "Set up Python on Ubuntu"
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: "Python codestyle"
run: |
pip install ".[dev]"
pip install pytest
pytest .
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Fixed** for any bug fixes.
- **Security** in case of vulnerabilities.

## [Unreleased]

### Added

- Support for `astropy.coordinates.SkyCoord` for assigning and reading the `target` property (#80)

### Fixed

- Fix asynchronous update for the `target` property (#80)

### Changed

- Change the jslink target trait from `target` to `shared_target` (#80)

## [0.3.0]

### Changed
Expand Down
4 changes: 2 additions & 2 deletions examples/6_Linked-widgets.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"c = Aladin(layout=Layout(width=\"33.33%\"), survey=\"P/2MASS/color\", **cosmetic_options)\n",
"\n",
"# synchronize target between 3 widgets\n",
"widgets.jslink((a, \"target\"), (b, \"target\"))\n",
"widgets.jslink((b, \"target\"), (c, \"target\"))\n",
"widgets.jslink((a, \"shared_target\"), (b, \"shared_target\"))\n",
"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",
Expand Down
42 changes: 29 additions & 13 deletions js/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./widget.css";
let idxView = 0;

function convert_pyname_to_jsname(pyname) {
if (pyname.charAt(0) === "_") pyname = pyname.slice(1);
let temp = pyname.split("_");
for (let i = 1; i < temp.length; i++) {
temp[i] = temp[i].charAt(0).toUpperCase() + temp[i].slice(1);
Expand Down Expand Up @@ -34,6 +35,9 @@ function render({ model, el }) {
let aladin = new A.aladin(aladinDiv, init_options);
idxView += 1;

const ra_dec = init_options["target"].split(" ");
aladin.gotoRaDec(ra_dec[0], ra_dec[1]);

el.appendChild(aladinDiv);

/* ------------------- */
Expand All @@ -46,27 +50,34 @@ function render({ model, el }) {
// the gotoObject call should only happen once. The two booleans prevent the two
// listeners from triggering each other and creating a buggy loop. The same trick
// is also necessary for the field of view.

/* Target control */
let target_js = false;
let target_py = false;

aladin.on("positionChanged", (position) => {
if (!target_py) {
target_js = true;
model.set("target", `${position.ra} ${position.dec}`);
model.save_changes();
} else {
// Event triggered when the user moves the map in Aladin Lite
aladin.on("positionChanged", () => {
if (target_py) {
target_py = false;
return;
}
target_js = true;
const ra_dec = aladin.getRaDec();
model.set("_target", `${ra_dec[0]} ${ra_dec[1]}`);
model.set("shared_target", `${ra_dec[0]} ${ra_dec[1]}`);
model.save_changes();
});

model.on("change:target", () => {
if (!target_js) {
target_py = true;
let target = model.get("target");
aladin.gotoObject(target);
} else {
// Event triggered when the target is changed from the Python side using jslink
model.on("change:shared_target", () => {
if (target_js) {
target_js = false;
return;
}
target_py = true;
const target = model.get("shared_target");
const [ra, dec] = target.split(" ");
aladin.gotoRaDec(ra, dec);
});

/* Field of View control */
Expand Down Expand Up @@ -182,6 +193,11 @@ function render({ model, el }) {
model.on("msg:custom", (msg) => {
let options = {};
switch (msg["event_name"]) {
case "goto_ra_dec":
const ra = msg["ra"];
const dec = msg["dec"];
aladin.gotoRaDec(ra, dec);
break;
case "add_catalog_from_URL":
aladin.addCatalog(A.catalogFromURL(msg["votable_URL"], msg["options"]));
break;
Expand Down Expand Up @@ -235,7 +251,7 @@ function render({ model, el }) {

return () => {
// need to unsubscribe the listeners
model.off("change:target");
model.off("change:shared_target");
model.off("change:fov");
Xen0Xys marked this conversation as resolved.
Show resolved Hide resolved
model.off("change:height");
model.off("change:coo_frame");
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "ipyaladin"
dynamic = ["version"]
dependencies = ["anywidget"]
dependencies = ["anywidget", "astropy"]
readme = "README.md"

[project.optional-dependencies]
Expand Down
129 changes: 106 additions & 23 deletions src/ipyaladin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import importlib.metadata
import pathlib
from typing import ClassVar
Xen0Xys marked this conversation as resolved.
Show resolved Hide resolved
from typing import ClassVar, Union
import warnings

import anywidget
from astropy.coordinates import SkyCoord
from traitlets import (
Float,
Int,
Expand All @@ -17,6 +18,8 @@
Undefined,
)

from .coordinate_parser import parse_coordinate_string

try:
__version__ = importlib.metadata.version("ipyaladin")
except importlib.metadata.PackageNotFoundError:
Expand All @@ -29,7 +32,17 @@ class Aladin(anywidget.AnyWidget):

# Options for the view initialization
height = Int(400).tag(sync=True, init_option=True)
target = Unicode("0 0").tag(sync=True, init_option=True)
_target = Unicode(
"0 0",
help="A private trait that stores the current target of the widget in a string."
" Its public version is the 'target' property that returns an "
"`~astropy.coordinates.SkyCoord` object",
).tag(sync=True, init_option=True)
shared_target = Unicode(
"0 0",
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)
survey = Unicode("https://alaskybis.unistra.fr/DSS/DSSColor").tag(
sync=True, init_option=True
Expand Down Expand Up @@ -85,6 +98,7 @@ def _init_options(self):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.target = kwargs.get("target", "0 0")
self.on_msg(self._handle_custom_message)

def _handle_custom_message(self, model, message, list_of_buffers): # noqa: ARG002
Expand All @@ -105,11 +119,52 @@ 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 target(self) -> SkyCoord:
"""The target of the Aladin Lite widget.

It can be set with either a string of an `~astropy.coordinates.SkyCoord` object.

Returns
-------
SkyCoord
An astropy.coordinates.SkyCoord object representing the target.

"""
ra, dec = self._target.split(" ")
return SkyCoord(
ra=ra,
dec=dec,
frame="icrs",
unit="deg",
)

@target.setter
def target(self, target: Union[str, SkyCoord]):
if isinstance(target, str): # If the target is str, parse it
target = parse_coordinate_string(target)
elif not isinstance(target, SkyCoord): # If the target is not str or SkyCoord
raise ValueError(
"target must be a string or an astropy.coordinates.SkyCoord object"
)
self._target = f"{target.icrs.ra.deg} {target.icrs.dec.deg}"
self.send(
{
"event_name": "goto_ra_dec",
"ra": target.icrs.ra.deg,
"dec": target.icrs.dec.deg,
}
)

def add_catalog_from_URL(self, votable_URL, votable_options=None):
"""load a VOTable table from an url and load its data into the widget
Args:
votable_URL: string url
votable_options: dictionary object"""
"""Load a VOTable table from an url and load its data into the widget.

Parameters
----------
votable_URL: str
votable_options: dict

"""
if votable_options is None:
votable_options = {}
self.send(
Expand All @@ -123,6 +178,17 @@ def add_catalog_from_URL(self, votable_URL, votable_options=None):
# MOCs

def add_moc(self, moc, **moc_options):
"""Add a MOC to the Aladin-Lite widget.

Parameters
----------
moc : `~mocpy.MOC` or str or dict
The MOC can be provided as a `mocpy.MOC` object, as a string containing an
URL where the MOC can be retrieved, or as a dictionary where the keys are
the HEALPix orders and the values are the pixel indices
(ex: {"1":[1,2,4], "2":[12,13,14,21,23,25]}).

"""
if isinstance(moc, dict):
self.send(
{
Expand Down Expand Up @@ -159,10 +225,15 @@ def add_moc(self, moc, **moc_options):
) from imp

def add_moc_from_URL(self, moc_URL, moc_options=None):
"""load a MOC from a URL and display it in Aladin Lite widget
Arguments:
moc_URL: string url
moc_options: dictionary object"""
"""Load a MOC from a URL and display it in Aladin Lite widget.

Parameters
----------
moc_URL: str
An URL to retrieve the MOC from
moc_options: dict

"""
warnings.warn(
"add_moc_from_URL is replaced by add_moc that detects automatically"
"that the MOC was given as an URL.",
Expand All @@ -174,12 +245,16 @@ def add_moc_from_URL(self, moc_URL, moc_options=None):
self.add_moc(moc_URL, **moc_options)

def add_moc_from_dict(self, moc_dict, moc_options=None):
"""load a MOC from a dict object and display it in Aladin Lite widget
Arguments:
moc_dict: the dict containing the MOC cells. Key are the HEALPix orders,
values are the pixel indexes,
eg: {"1":[1,2,4], "2":[12,13,14,21,23,25]}
moc_options: dictionary object"""
"""Load a MOC from a dict object and display it in Aladin Lite widget.

Parameters
----------
moc_dict: dict
It contains the MOC cells. Key are the HEALPix orders, values are the pixel
indexes, eg: {"1":[1,2,4], "2":[12,13,14,21,23,25]}
moc_options: dict

"""
warnings.warn(
"add_moc_from_dict is replaced by add_moc that detects automatically"
"that the MOC was given as a dictionary.",
Expand Down Expand Up @@ -217,8 +292,8 @@ def add_table(self, table, **table_options):
meta={"name": "my sample table"})
>>> aladin.add_table(table)
And the table should appear in the output of Cell 1!
"""

"""
# this library must be installed, and is used in votable operations
# http://www.astropy.org/
import io
Expand All @@ -234,9 +309,10 @@ def add_overlay_from_stcs(self, stc_string, **overlay_options):
Parameters
----------
stc_string: str
The STC-S string.
The STC-S string.
overlay_options: keyword arguments
TODO: documentation"""

"""
self.send(
{
"event_name": "add_overlay_from_stcs",
Expand All @@ -261,10 +337,17 @@ def rectangular_selection(self):
# Adding a listener

def add_listener(self, listener_type, callback):
"""add a listener to the widget
Args:
listener_type: string that can either be 'objectHovered' or 'objClicked'
callback: python function"""
"""Add a listener to the widget.

Parameters
----------
listener_type: str
Can either be 'objectHovered' or 'objClicked'
callback: Callable
A python function to be called when the event corresponding to the
listener_type is detected

"""
if listener_type in {"objectHovered", "object_hovered"}:
self.listener_callback["object_hovered"] = callback
elif listener_type in {"objectClicked", "object_clicked"}:
Expand Down
Loading