diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ce1523..879a12e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- monochromatic FITS images can be added to the view with `ipyaladin.Aladin.add_fits`. + The method accepts `astropy.io.fits.HDUList`, `pathlib.Path`, or `string` representing paths (#86) + ## [0.4.0] ### Added diff --git a/examples/2_Base_Commands.ipynb b/examples/2_Base_Commands.ipynb index e75a4f46..e7970c2f 100644 --- a/examples/2_Base_Commands.ipynb +++ b/examples/2_Base_Commands.ipynb @@ -14,7 +14,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ipyaladin import Aladin" + "from ipyaladin import Aladin\n", + "from pathlib import Path" ] }, { @@ -73,11 +74,18 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "aladin.target" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The x-axis field of view (fov) can be set" ] }, { @@ -100,6 +108,13 @@ "aladin.fov" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The overlay survey is always on top of the base layer" + ] + }, { "cell_type": "code", "execution_count": null, @@ -112,6 +127,13 @@ "aladin.overlay_survey_opacity = 0.5" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can change the coordinate frame (the choices are `ICRS`, `ICRSd` or `Galactic`)." + ] + }, { "cell_type": "code", "execution_count": null, @@ -136,17 +158,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Some commands can be used with astropy objects" + "The target and field of view can be set with astropy objects" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "from astropy.coordinates import Angle, SkyCoord" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", @@ -165,6 +187,23 @@ "source": [ "aladin.fov = Angle(5, \"deg\")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also add a FITS image to the view of the widget, either as a path (string of pathlib.Path object) or as an\n", + "astropy HDU object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aladin.add_fits(Path(\"images/m31.fits\"), name=\"M31\", opacity=0.5)" + ] } ], "metadata": { diff --git a/js/models/event_handler.js b/js/models/event_handler.js index 8aa6f45b..d3c3bb8c 100644 --- a/js/models/event_handler.js +++ b/js/models/event_handler.js @@ -173,6 +173,7 @@ export default class EventHandler { this.eventHandlers = { change_fov: this.messageHandler.handleChangeFoV, goto_ra_dec: this.messageHandler.handleGotoRaDec, + add_fits: this.messageHandler.handleAddFits, add_catalog_from_URL: this.messageHandler.handleAddCatalogFromURL, add_MOC_from_URL: this.messageHandler.handleAddMOCFromURL, add_MOC_from_dict: this.messageHandler.handleAddMOCFromDict, diff --git a/js/models/message_handler.js b/js/models/message_handler.js index d421c621..b5387d7f 100644 --- a/js/models/message_handler.js +++ b/js/models/message_handler.js @@ -1,6 +1,8 @@ import { convertOptionNamesToCamelCase } from "../utils"; import A from "../aladin_lite"; +let imageCount = 0; + export default class MessageHandler { constructor(aladin) { this.aladin = aladin; @@ -14,6 +16,21 @@ export default class MessageHandler { this.aladin.gotoRaDec(msg["ra"], msg["dec"]); } + handleAddFits(msg, buffers) { + const options = convertOptionNamesToCamelCase(msg["options"] || {}); + if (!options.name) + options.name = `image_${String(++imageCount).padStart(3, "0")}`; + const buffer = buffers[0]; + const blob = new Blob([buffer], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + const image = this.aladin.createImageFITS(url, options, (ra, dec) => { + this.aladin.gotoRaDec(ra, dec); + console.info(`FITS located at ra: ${ra}, dec: ${dec}`); + URL.revokeObjectURL(url); + }); + this.aladin.setOverlayImageLayer(image, options.name); + } + handleAddCatalogFromURL(msg) { const options = convertOptionNamesToCamelCase(msg["options"] || {}); this.aladin.addCatalog(A.catalogFromURL(msg["votable_URL"], options)); diff --git a/js/utils.js b/js/utils.js index 746ba336..41cc3660 100644 --- a/js/utils.js +++ b/js/utils.js @@ -27,14 +27,14 @@ class Lock { locked = false; /** - * Locks the object + * Unlocks the object */ unlock() { this.locked = false; } /** - * Unlocks the object + * Locks the object */ lock() { this.locked = true; diff --git a/src/ipyaladin/widget.py b/src/ipyaladin/widget.py index 17beb478..735fee5a 100644 --- a/src/ipyaladin/widget.py +++ b/src/ipyaladin/widget.py @@ -5,7 +5,9 @@ It allows to display astronomical images and catalogs in an interactive way. """ +import io import pathlib +from pathlib import Path import typing from typing import ClassVar, Union, Final, Optional import warnings @@ -14,6 +16,8 @@ from astropy.table.table import QTable from astropy.table import Table from astropy.coordinates import SkyCoord, Angle +from astropy.io import fits as astropy_fits +from astropy.io.fits import HDUList import traitlets try: @@ -241,6 +245,32 @@ def add_catalog_from_URL( } ) + def add_fits(self, fits: Union[str, Path, HDUList], **image_options: any) -> None: + """Load a FITS file into the widget. + + Parameters + ---------- + fits: a path as a string or `pathlib.Path`, or an `~astropy.io.fits.HDUList` + The FITS file to load into the widget. + image_options: dict + The options for the image. See the Aladin Lite image options: + https://cds-astro.github.io/aladin-lite/global.html#ImageOptions + + """ + is_path = isinstance(fits, (Path, str)) + if is_path: + with astropy_fits.open(fits) as fits_file: + fits_bytes = io.BytesIO() + fits_file.writeto(fits_bytes) + else: + fits_bytes = io.BytesIO() + fits.writeto(fits_bytes) + + self.send( + {"event_name": "add_fits", "options": image_options}, + buffers=[fits_bytes.getvalue()], + ) + # MOCs def add_moc(self, moc: any, **moc_options: any) -> None: @@ -364,8 +394,6 @@ def add_table(self, table: Union[QTable, Table], **table_options: any) -> None: And the table should appear in the output of Cell 1! """ - import io - table_bytes = io.BytesIO() table.write(table_bytes, format="votable") self.send(