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 17 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 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
41 changes: 28 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,6 @@ function render({ model, el }) {

return () => {
// need to unsubscribe the listeners
model.off("change: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
63 changes: 61 additions & 2 deletions src/ipyaladin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import importlib.metadata
import pathlib
from typing import ClassVar
Xen0Xys marked this conversation as resolved.
Show resolved Hide resolved
import warnings

from astropy.coordinates import SkyCoord

import anywidget

from typing import ClassVar
from traitlets import (
Float,
Int,
Expand All @@ -16,6 +19,7 @@
default,
Undefined,
)
from .coordinate_parser import parse_coordinate_string

try:
__version__ = importlib.metadata.version("ipyaladin")
Expand All @@ -29,7 +33,16 @@ 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="this trait is used belong the target property "
"to store the current target of Aladin Lite",
).tag(sync=True, init_option=True)
shared_target = Unicode(
"0 0",
help="this trait is destined to be used with jslink widget function "
"to link two Aladin Lite widgets target 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,6 +119,51 @@ 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:
"""
Get the target of the Aladin Lite widget.
:return: astropy.coordinates.SkyCoord object
"""
ra, dec = self._target.split(" ")
return SkyCoord(
ra=ra,
dec=dec,
frame="icrs",
unit="deg",
)

@target.setter
def target(self, target: str or SkyCoord):
"""
Set the target of the Aladin Lite widget.
:param target: string or astropy.coordinates.SkyCoord object
:return: None
"""
if isinstance(target, str): # If the target is a string, parse it
sc = parse_coordinate_string(target)
self._target = f"{sc.icrs.ra.deg} {sc.icrs.dec.deg}"
self.send(
{
"event_name": "goto_ra_dec",
"ra": sc.icrs.ra.deg,
"dec": sc.icrs.dec.deg,
}
)
elif isinstance(target, SkyCoord): # If the target is a SkyCoord object
self._target = f"{target.icrs.ra.deg} {target.icrs.dec.deg}"
Xen0Xys marked this conversation as resolved.
Show resolved Hide resolved
self.send(
{
"event_name": "goto_ra_dec",
"ra": target.icrs.ra.deg,
"dec": target.icrs.dec.deg,
}
)
else:
raise ValueError(
"target must be a string or an astropy.coordinates.SkyCoord object"
)

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:
Expand Down
31 changes: 31 additions & 0 deletions src/ipyaladin/coordinate_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from astropy.coordinates import SkyCoord, Angle
import re


def parse_coordinate_string(string: str) -> SkyCoord:
regex = r"^[0-9][0-9: hmsd.+-]+$"
if not re.match(regex, string):
return SkyCoord.from_name(string)
coordinates: tuple[str, str] = _split_coordinate_string(string)
ra: Angle or None = None
dec: Angle = Angle(coordinates[1], unit="deg")
if _is_hour_angle_string(coordinates[0]):
ra = Angle(coordinates[0], unit="hour")
else:
ra = Angle(coordinates[0], unit="deg")
return SkyCoord(ra=ra, dec=dec, frame="icrs")


def _split_coordinate_string(coo: str) -> tuple[str, str]:
regex = r"[\s°]"
parts = re.split(regex, coo)
parts = [part for part in parts if part]
middle = len(parts) // 2
first_part = " ".join(parts[:middle])
second_part = " ".join(parts[middle:])
return first_part, second_part


def _is_hour_angle_string(coo: str) -> bool:
regex = r"[hms°: ]"
return bool(re.search(regex, coo))
Loading