diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e24300..6c13155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add function `save_view_as_image` to save the view as an image file (#108) - Support planetary objects for ipyaladin targets (#103) - new method `add_marker` to add a marker to the view (#111) +- Add function `add_hips` to add HiPS to the view (#84) +- Add function `remove_layer` to remove a layer from the view (#84) +- Add function `set_layer_opacity` to set the opacity of a layer (#84) ### Deprecated diff --git a/examples/02_Base_Commands.ipynb b/examples/02_Base_Commands.ipynb index acbef60..30368cb 100644 --- a/examples/02_Base_Commands.ipynb +++ b/examples/02_Base_Commands.ipynb @@ -101,8 +101,8 @@ "metadata": {}, "outputs": [], "source": [ - "aladin.overlay_survey = \"P/allWISE/color\"\n", - "aladin.overlay_survey_opacity = 0.5" + "aladin.add_hips(\"P/allWISE/color\", name=\"overlay\")\n", + "aladin.set_layer_opacity(\"overlay\", 0.5)" ] }, { @@ -137,6 +137,15 @@ "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" + ] + }, { "cell_type": "code", "execution_count": null, @@ -177,7 +186,8 @@ "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." + "Here we will add markers for Messier objects M1 to M10.", + "You can get all layers identified by their name." ] }, { @@ -202,7 +212,24 @@ " )\n", "aladin.add_markers(markers, name=\"M1-M10\", color=\"pink\", shape=\"cross\", source_size=15)\n", "aladin.target = \"M1\"\n", - "aladin.fov = 0.2" + "aladin.fov = 0.2", + "aladin.layers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And remove a layer by its name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aladin.remove_layer(\"M31\")" ] } ], diff --git a/js/models/event_handler.js b/js/models/event_handler.js index fc90d51..daead9c 100644 --- a/js/models/event_handler.js +++ b/js/models/event_handler.js @@ -129,6 +129,72 @@ export default class EventHandler { this.aladin.setFoV(fov); }); + /* Survey control */ + const jsSurveyLock = new Lock(); + const pySurveyLock = new Lock(); + + this.model.on("change:survey", () => { + if (jsSurveyLock.locked) { + jsSurveyLock.unlock(); + return; + } + pySurveyLock.lock(); + this.aladin.setImageSurvey(this.model.get("survey")); + }); + + /* Overlay survey control */ + const jsOverlaySurveyLock = new Lock(); + const pyOverlaySurveyLock = new Lock(); + + this.model.on("change:overlay_survey", () => { + if (jsOverlaySurveyLock.locked) { + jsOverlaySurveyLock.unlock(); + return; + } + pyOverlaySurveyLock.lock(); + this.aladin.setOverlayImageLayer(this.model.get("overlay_survey")); + }); + + this.aladin.on("layerChanged", (imageLayer, layerName, state) => { + // If the layer is added or removed, update the layers traitlets + if (state === "ADDED") { + let layers = this.model.get("layers") || {}; + // If the object is not copied, the model will not detect the change + layers = { ...layers }; + if (imageLayer.url.startsWith("blob:")) + layers[layerName] = imageLayer.name; + else layers[layerName] = imageLayer.url; + this.model.set("layers", layers); + + // If the layer is added, update the WCS, FoV, survey and overlay survey + if (layerName === "base") { + this.updateWCS(); + this.model.set("_survey_body", imageLayer.hipsBody || "sky"); + this.model.set("_base_layer_last_view", imageLayer.url); + if (pySurveyLock.locked) { + pySurveyLock.unlock(); + return; + } + jsSurveyLock.lock(); + this.model.set("survey", imageLayer.url); + } else if (layerName === "overlay") { + if (pyOverlaySurveyLock.locked) { + pyOverlaySurveyLock.unlock(); + return; + } + jsOverlaySurveyLock.lock(); + this.model.set("overlay_survey", imageLayer.url); + } + } else if (state === "REMOVED") { + let layers = this.model.get("layers") || {}; + // If the object is not copied, the model will not detect the change + layers = { ...layers }; + delete layers[layerName]; + this.model.set("layers", layers); + } + this.model.save_changes(); + }); + /* Div control */ this.model.on("change:_height", () => { let height = this.model.get("_height"); @@ -151,15 +217,6 @@ export default class EventHandler { this.model.save_changes(); }); - this.aladin.on("layerChanged", (imageLayer, layerName, state) => { - if (layerName === "base") - this.model.set("_survey_body", imageLayer.hipsBody || "sky"); - if (layerName !== "base" || state !== "ADDED") return; - this.updateWCS(); - this.model.set("_base_layer_last_view", imageLayer.id); - this.model.save_changes(); - }); - this.aladin.on("resizeChanged", (width, height) => { // Skip resize event when the div is hidden if (width === 1 && height === 1) { @@ -253,14 +310,6 @@ export default class EventHandler { this.aladin.setFrame(this.model.get("coo_frame")); }); - this.model.on("change:survey", () => { - this.aladin.setImageSurvey(this.model.get("survey")); - }); - - this.model.on("change:overlay_survey", () => { - this.aladin.setOverlayImageLayer(this.model.get("overlay_survey")); - }); - this.model.on("change:overlay_survey_opacity", () => { this.aladin .getOverlayImageLayer() @@ -272,6 +321,9 @@ export default class EventHandler { change_fov: this.messageHandler.handleChangeFoV, goto_ra_dec: this.messageHandler.handleGotoRaDec, save_view_as_image: this.messageHandler.handleSaveViewAsImage, + add_hips: this.messageHandler.handleAddHips, + remove_layer: this.messageHandler.handleRemoveLayer, + set_layer_opacity: this.messageHandler.handleSetLayerOpacity, add_fits: this.messageHandler.handleAddFits, add_catalog_from_URL: this.messageHandler.handleAddCatalogFromURL, add_MOC_from_URL: this.messageHandler.handleAddMOCFromURL, diff --git a/js/models/message_handler.js b/js/models/message_handler.js index 748b19d..bd318a1 100644 --- a/js/models/message_handler.js +++ b/js/models/message_handler.js @@ -56,6 +56,30 @@ export default class MessageHandler { ); } + handleAddHips(msg) { + const options = convertOptionNamesToCamelCase(msg["options"] || {}); + if (!options.name) + options.name = `hips_${String(++imageCount).padStart(3, "0")}`; + const hipsIdOrUrl = msg["hips"]; + const imageHips = A.imageHiPS(hipsIdOrUrl, options); + this.aladin.setOverlayImageLayer(imageHips, options.name); + } + + handleRemoveLayer(msg) { + const layerName = msg["name"]; + this.aladin.removeImageLayer(layerName); + } + + handleSetLayerOpacity(msg) { + const layerName = msg["name"]; + const opacity = msg["opacity"]; + if (layerName === "overlay") { + this.model.set("overlay_survey_opacity", opacity); + this.model.save_changes(); + } + this.aladin.getOverlayImageLayer(layerName).setAlpha(opacity); + } + handleAddFits(msg, buffers) { const options = convertOptionNamesToCamelCase(msg["options"] || {}); if (!options.name) diff --git a/src/ipyaladin/widget.py b/src/ipyaladin/widget.py index 5a4c342..bdab6bb 100644 --- a/src/ipyaladin/widget.py +++ b/src/ipyaladin/widget.py @@ -260,6 +260,12 @@ class Aladin(anywidget.AnyWidget): "sky", help="The body name of the base layer survey, 'sky' for the sky survey", ).tag(sync=True, init_option=True) + # Surveys management + layers = traitlets.Dict( + {}, + help="A dictionary of surveys to add to the widget. The keys are the names of " + "the surveys and the values are the URLs of the surveys.", + ).tag(sync=True) overlay_survey = Unicode("").tag(sync=True, init_option=True) overlay_survey_opacity = Float(0.0).tag(sync=True, init_option=True) _base_layer_last_view = Unicode( @@ -538,6 +544,59 @@ def add_markers( } ) + def add_hips(self, hips: str, **options: any) -> None: + """Add a HiPS to the Aladin Lite widget. + + Parameters + ---------- + hips : str + The HiPS to add to the widget as a URL or a CDS id + `_ + options : keyword arguments + The options for the HiPS. See `Aladin Lite's HiPS options + `_ + + """ + self.send( + { + "event_name": "add_hips", + "hips": hips, + "options": options, + } + ) + + def remove_layer(self, name: str) -> None: + """Remove a layer from the Aladin Lite widget. + + Parameters + ---------- + name : str + The name of the layer to remove. + + """ + if name == "base": + raise ValueError("The base layer cannot be removed.") + self.send({"event_name": "remove_layer", "name": name}) + + def set_layer_opacity(self, name: str, opacity: float) -> None: + """Set the opacity of a layer in the Aladin Lite widget. + + Parameters + ---------- + name : str + The name of the layer to set the opacity. + opacity : float + The opacity value to set. + + """ + self.send( + { + "event_name": "set_layer_opacity", + "name": name, + "opacity": opacity, + } + ) + def _save_file(self, path: str, buffer: bytes) -> None: """Save a file from a buffer. @@ -686,8 +745,13 @@ def add_fits(self, fits: Union[str, Path, HDUList], **image_options: any) -> Non self._wcs = {} self.send( - {"event_name": "add_fits", "options": image_options}, - buffers=[fits_bytes.getvalue()], + { + "event_name": "add_fits", + "options": image_options, + }, + buffers=[ + fits_bytes.getvalue(), + ], ) # MOCs