diff --git a/js/models/event_handler.js b/js/models/event_handler.js index e98c231..9eb95e7 100644 --- a/js/models/event_handler.js +++ b/js/models/event_handler.js @@ -129,10 +129,69 @@ 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 (layerName !== "base" || state !== "ADDED") return; - this.updateWCS(); - this.model.set("_base_layer_last_view", imageLayer.id); + // 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); + } 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); + } + // If the layer is added, update the WCS, FoV, survey and overlay survey + if (state === "ADDED") { + if (layerName === "base") { + this.updateWCS(); + 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); + } + } this.model.save_changes(); }); @@ -244,14 +303,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() @@ -262,6 +313,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 b3def3a..d10ad09 100644 --- a/js/models/message_handler.js +++ b/js/models/message_handler.js @@ -36,6 +36,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["layer_name"]; + this.aladin.removeImageLayer(layerName); + } + + handleSetLayerOpacity(msg) { + const layerName = msg["layer_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 0266d0a..d2aaf29 100644 --- a/src/ipyaladin/widget.py +++ b/src/ipyaladin/widget.py @@ -203,7 +203,12 @@ class Aladin(anywidget.AnyWidget): # listener callback is on the python side and contains functions to link to events listener_callback: ClassVar[Dict[str, callable]] = {} - # overlay survey + # 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( @@ -389,15 +394,13 @@ def target(self, target: Union[str, SkyCoord]) -> None: } ) - def add_hips(self, hips: str, layer_name: str, **hips_options: any) -> None: + def add_hips(self, hips: str, **hips_options: any) -> None: """Add a HiPS to the Aladin Lite widget. Parameters ---------- hips : str The HiPS to add to the widget. - layer_name : str - The name of the layer. hips_options : keyword arguments The options for the HiPS. See `Aladin Lite's HiPS options `_ @@ -407,7 +410,6 @@ def add_hips(self, hips: str, layer_name: str, **hips_options: any) -> None: { "event_name": "add_hips", "hips": hips, - "layer_name": layer_name, "options": hips_options, } ) @@ -421,9 +423,11 @@ def remove_layer(self, layer_name: str) -> None: The name of the layer to remove. """ + if layer_name == "base": + raise ValueError("The base layer cannot be removed.") self.send({"event_name": "remove_layer", "layer_name": layer_name}) - def set_opacity(self, layer_name: str, opacity: float) -> None: + def set_layer_opacity(self, layer_name: str, opacity: float) -> None: """Set the opacity of a layer in the Aladin Lite widget. Parameters @@ -435,7 +439,11 @@ def set_opacity(self, layer_name: str, opacity: float) -> None: """ self.send( - {"event_name": "set_opacity", "layer_name": layer_name, "opacity": opacity} + { + "event_name": "set_layer_opacity", + "layer_name": layer_name, + "opacity": opacity, + } ) def _save_file(self, path: str, buffer: bytes) -> None: @@ -577,8 +585,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