diff --git a/CHANGELOG.md b/CHANGELOG.md index 446f044..9e24300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add selected sources export as `astropy.Table` list with property `selected_objects` (#100) - Add function `get_view_as_fits` to export the view as a `astropy.io.fits.HDUList` (#98) - Add function `save_view_as_image` to save the view as an image file (#108) -- Support for planetary objects for Aladin Lite target (#103) +- Support planetary objects for ipyaladin targets (#103) +- new method `add_marker` to add a marker to the view (#111) ### Deprecated diff --git a/examples/02_Base_Commands.ipynb b/examples/02_Base_Commands.ipynb index 6c2ae00..acbef60 100644 --- a/examples/02_Base_Commands.ipynb +++ b/examples/02_Base_Commands.ipynb @@ -14,7 +14,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ipyaladin import Aladin\n", + "from astropy.coordinates import Angle, SkyCoord\n", + "from ipyaladin import Aladin, Marker\n", "from pathlib import Path" ] }, @@ -23,24 +24,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A list of all available commands can be displayed as such." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(dir(Aladin))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A few of them are illustrated in the next cells. Let's first, create the widget with a few initial parameters:" + "`ipyaladin`'s full list of methods can be found in the documentation [here](https://cds-astro.github.io/ipyaladin/autoapi/ipyaladin/widget/index.html). A few of them are illustrated in the next cells. Let's first, create the widget with a few initial parameters:" ] }, { @@ -64,9 +48,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "aladin.target = \"sgr a*\"" @@ -74,12 +56,12 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "aladin.target" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -91,9 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "aladin.fov = 2" @@ -118,9 +98,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "aladin.overlay_survey = \"P/allWISE/color\"\n", @@ -137,9 +115,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "aladin.coo_frame = \"ICRSd\" # ICRS, and angles expressed in degrees" @@ -161,15 +137,6 @@ "The target and field of view can be set with astropy objects" ] }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "from astropy.coordinates import Angle, SkyCoord" - ], - "outputs": [], - "execution_count": null - }, { "cell_type": "code", "execution_count": null, @@ -204,32 +171,42 @@ "source": [ "aladin.add_fits(Path(\"images/m31.fits\"), name=\"M31\", opacity=0.5)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can add markers to the view of the widget with custom popup title and description.\n", + "Here we will add markers for Messier objects M1 to M10." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "markers = []\n", + "for i in range(1, 11):\n", + " name = f\"M{i}\"\n", + " markers.append(\n", + " Marker(\n", + " position=name,\n", + " title=name,\n", + " description=(\n", + " ' '\n", + " \"Read more on SIMBAD\"\n", + " ),\n", + " )\n", + " )\n", + "aladin.add_markers(markers, name=\"M1-M10\", color=\"pink\", shape=\"cross\", source_size=15)\n", + "aladin.target = \"M1\"\n", + "aladin.fov = 0.2" + ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" - }, - "vscode": { - "interpreter": { - "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" - } - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 4 } diff --git a/js/models/event_handler.js b/js/models/event_handler.js index f36fbdb..fc90d51 100644 --- a/js/models/event_handler.js +++ b/js/models/event_handler.js @@ -268,6 +268,7 @@ export default class EventHandler { }); this.eventHandlers = { + add_marker: this.messageHandler.handleAddMarker, change_fov: this.messageHandler.handleChangeFoV, goto_ra_dec: this.messageHandler.handleGotoRaDec, save_view_as_image: this.messageHandler.handleSaveViewAsImage, diff --git a/js/models/message_handler.js b/js/models/message_handler.js index b3def3a..748b19d 100644 --- a/js/models/message_handler.js +++ b/js/models/message_handler.js @@ -9,6 +9,26 @@ export default class MessageHandler { this.model = model; } + handleAddMarker(msg) { + const options = convertOptionNamesToCamelCase(msg["options"] || {}); + // default name + if (!options.name) options.name = "markers"; + // create catalog + const catalog = A.catalog(options); + this.aladin.addCatalog(catalog); + const pythonMarkers = msg["markers"]; + const markers = []; + for (const marker of pythonMarkers) { + markers.push( + A.marker(marker["lon"], marker["lat"], { + popupTitle: marker["title"], + popupDesc: marker["description"], + }), + ); + } + catalog.addSources(markers); + } + handleChangeFoV(msg) { this.aladin.setFoV(msg["fov"]); } diff --git a/src/ipyaladin/__init__.py b/src/ipyaladin/__init__.py index 6161b84..eba7d08 100644 --- a/src/ipyaladin/__init__.py +++ b/src/ipyaladin/__init__.py @@ -1,4 +1,5 @@ """Top-level package for ipyaladin.""" from .widget import Aladin # noqa: F401 +from .elements.marker import Marker # noqa: F401 from .__about__ import __version__, __aladin_lite_version__ # noqa: F401 diff --git a/src/test/__init__.py b/src/ipyaladin/elements/__init__.py similarity index 100% rename from src/test/__init__.py rename to src/ipyaladin/elements/__init__.py diff --git a/src/ipyaladin/elements/marker.py b/src/ipyaladin/elements/marker.py new file mode 100644 index 0000000..789d88e --- /dev/null +++ b/src/ipyaladin/elements/marker.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import Tuple, Union + +from astropy.coordinates import SkyCoord, Longitude, Latitude +from ipyaladin.utils._coordinate_parser import _parse_coordinate_string + + +@dataclass +class Marker: + """A class representing a marker in Aladin Lite.""" + + def __init__( + self, + position: Union[str, SkyCoord, Tuple[Longitude, Latitude]], + title: str, + description: str, + ) -> None: + self.title = title + self.description = description + if isinstance(position, SkyCoord): + self.lon = position.ra.deg + self.lat = position.dec.deg + elif isinstance(position, str): + self.lon, self.lat = _parse_coordinate_string(position) + elif ( + isinstance(position, Tuple) + and isinstance(position[0], Longitude) + and isinstance(position[1], Latitude) + ): + self.lon = position[0].deg + self.lat = position[1].deg diff --git a/src/ipyaladin/widget.py b/src/ipyaladin/widget.py index a5e9d05..22494a4 100644 --- a/src/ipyaladin/widget.py +++ b/src/ipyaladin/widget.py @@ -6,6 +6,7 @@ """ from collections.abc import Callable +from dataclasses import asdict import io import pathlib from json import JSONDecodeError @@ -27,6 +28,7 @@ from .utils.exceptions import WidgetReducedError, WidgetNotReadyError from .utils._coordinate_parser import _parse_coordinate_string +from .elements.marker import Marker try: from regions import ( @@ -281,7 +283,7 @@ class Aladin(anywidget.AnyWidget): "is reduced in size when hidden.", ).tag(sync=True) - init_options = traitlets.List(trait=Any()).tag(sync=True) + init_options = traitlets.List(trait=traitlets.Any()).tag(sync=True) @default("init_options") def _init_options(self) -> List[str]: @@ -507,6 +509,36 @@ def target(self, target: Union[str, SkyCoord, Tuple[float, float]]) -> None: } ) + def add_markers( + self, markers: Union[Marker, List[Marker]], **catalog_options: any + ) -> None: + """Add markers to the Aladin Lite widget. + + Markers have a popup window that appear when they're clicked on. + + Parameters + ---------- + markers : Marker or list[Marker] + The marker(s) to add to the widget. It can be given as a single `Marker` + object or as a list of `Marker` objects. + catalog_options : any + The options for the catalog. See the `Aladin Lite catalog options + `_ + + See Also + -------- + add_table: also adds points, but without popup window. + """ + if not isinstance(markers, list): + markers = [markers] + self.send( + { + "event_name": "add_marker", + "markers": [asdict(marker) for marker in markers], + "options": catalog_options, + } + ) + def _save_file(self, path: str, buffer: bytes) -> None: """Save a file from a buffer. @@ -776,6 +808,10 @@ def add_table(self, table: Union[QTable, Table], **table_options: any) -> None: table options `_ + See Also + -------- + add_markers: adds markers with a popup window when clicked + """ table_bytes = io.BytesIO() table.write(table_bytes, format="votable") diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test/test_aladin.py b/src/tests/test_aladin.py similarity index 100% rename from src/test/test_aladin.py rename to src/tests/test_aladin.py diff --git a/src/test/test_coordinate_parser.py b/src/tests/test_coordinate_parser.py similarity index 100% rename from src/test/test_coordinate_parser.py rename to src/tests/test_coordinate_parser.py