Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `projection` parameter can now be called to change the projection of the
ipyaladin viewer [#172]

## Fixed

- Overlays are displayed on the generated HTML static file because it is passed with
traitlets instead of being sent with events. This fixes documentation examples [#151].

## [0.7.0]

## Added
Expand Down
2 changes: 1 addition & 1 deletion examples/12_Planetary_surveys.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"id": "35ea9256",
"metadata": {},
"source": [
"The target does not return a `SkyCoord` object anymore, since we don't represent the sky here. It is a couple of `Longitude` and `Latitude`."
"The target does not return a `SkyCoord` object anymore, since we don't represent the sky here. It is a tuple of `Longitude` and `Latitude`."
]
},
{
Expand Down
86 changes: 77 additions & 9 deletions js/models/event_handler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import MessageHandler from "./message_handler";
import { divNumber, setDivNumber, Lock, setDivHeight } from "../utils";
import {
divNumber,
setDivNumber,
Lock,
setDivHeight,
isLiveSession,
} from "../utils";

export default class EventHandler {
/**
Expand Down Expand Up @@ -245,6 +251,9 @@ export default class EventHandler {
this.model.save_changes();
});

// Detect environment
const isLive = isLiveSession(this.model);

/* ----------------------- */
/* Traits listeners */
/* ----------------------- */
Expand Down Expand Up @@ -280,9 +289,11 @@ export default class EventHandler {
},
/* Rotation control */
_rotation: (rotation) => {
if (rotation === this.aladin.getRotation()) {
return;
}
// And propagate it to Aladin Lite
this.aladin.setRotation(rotation);

// Update WCS and FoV only if this is the last div
this.updateWCS();
this.update2AxisFoV();
Expand Down Expand Up @@ -323,8 +334,65 @@ export default class EventHandler {
overlay.setAlpha(opacity);
}
},
colormap: (colormap) => {
this.aladin.getBaseImageLayer().setColormap(colormap);
},
projection: (projection) => {
this.aladin.setProjection(projection);

this.updateWCS();
this.update2AxisFoV();
this.model.save_changes();
},
};

// Overlays traitlet listening
const handleOverlay = (overlay) => {
if (overlay.action === "add") {
switch (overlay.type) {
case "table":
// A table is transcripted to an ArrayBuffer (thanks to widget_serialization)
this.messageHandler.handleAddTable(overlay);
break;
case "catalog_from_url":
this.messageHandler.handleAddCatalogFromURL(overlay);
break;
case "MOC_from_url":
this.messageHandler.handleAddMOCFromURL(overlay);
break;
case "MOC_from_dict":
this.messageHandler.handleAddMOCFromDict(overlay);
break;
case "regions_stcs":
this.messageHandler.handleAddGraphicOverlay(overlay);
break;
case "regions":
this.messageHandler.handleAddGraphicOverlay(overlay);
break;
case "fits-image":
this.messageHandler.handleAddFitsImage(overlay);
break;
default:
break;
}
} else {
// TODO: remove overlay
}
};

if (isLive) {
// Default behavior: do not listen for overlays but _overlay_patch when ipyaladin
// is connected to a jupyter/jupyterlab session (i.e. not exported to HTML)
this.traitHandlers["_overlay_patch"] = handleOverlay;
} else {
// static HTML file export (i.e. example HTML documentation files or HTML export of a notebook)
this.traitHandlers["overlays"] = (overlays) => {
for (const overlay of overlays) {
handleOverlay(overlay);
}
};
}

for (var trait in this.traitHandlers) {
let handler = this.traitHandlers[trait];
this.model.on("change:" + trait, (_, value) => handler(value));
Expand All @@ -335,15 +403,15 @@ export default class EventHandler {
this.eventHandlers = {
add_marker: this.messageHandler.handleAddMarker,
save_view_as_image: this.messageHandler.handleSaveViewAsImage,
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,
add_overlay: this.messageHandler.handleAddOverlay,
change_colormap: this.messageHandler.handleChangeColormap,
get_JPG_thumbnail: this.messageHandler.handleGetJPGThumbnail,
trigger_selection: this.messageHandler.handleTriggerSelection,
add_table: this.messageHandler.handleAddTable,
//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,
//add_overlay: this.messageHandler.handleAddGraphicOverlay,
//add_table: this.messageHandler.handleAddTable,
//change_colormap: this.messageHandler.handleChangeColormap,
};

this.model.on("msg:custom", (msg, buffers) => {
Expand Down
40 changes: 18 additions & 22 deletions js/models/message_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export default class MessageHandler {
);
}

handleAddFits(msg, buffers) {
const options = convertOptionNamesToCamelCase(msg["options"] || {});
handleAddFitsImage(overlay) {
const options = convertOptionNamesToCamelCase(overlay["options"] || {});
if (!options.name)
options.name = `image_${String(++imageCount).padStart(3, "0")}`;
const buffer = buffers[0];
const buffer = overlay.data.buffer;
const blob = new Blob([buffer], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const image = this.aladin.createImageFITS(url, options, (ra, dec) => {
Expand All @@ -64,25 +64,25 @@ export default class MessageHandler {
this.aladin.setOverlayImageLayer(image, options.name);
}

handleAddCatalogFromURL(msg) {
const options = convertOptionNamesToCamelCase(msg["options"] || {});
this.aladin.addCatalog(A.catalogFromURL(msg["votable_URL"], options));
handleAddCatalogFromURL(catalog) {
const options = convertOptionNamesToCamelCase(catalog["options"] || {});
this.aladin.addCatalog(A.catalogFromURL(catalog["data"], options));
}

handleAddMOCFromURL(msg) {
const options = convertOptionNamesToCamelCase(msg["options"] || {});
this.aladin.addMOC(A.MOCFromURL(msg["moc_URL"], options));
handleAddMOCFromURL(moc) {
const options = convertOptionNamesToCamelCase(moc["options"] || {});
this.aladin.addMOC(A.MOCFromURL(moc["data"], options));
}

handleAddMOCFromDict(msg) {
const options = convertOptionNamesToCamelCase(msg["options"] || {});
this.aladin.addMOC(A.MOCFromJSON(msg["moc_dict"], options));
handleAddMOCFromDict(moc) {
const options = convertOptionNamesToCamelCase(moc["options"] || {});
this.aladin.addMOC(A.MOCFromJSON(moc["data"], options));
}

handleAddOverlay(msg) {
const regions = msg["regions_infos"];
handleAddGraphicOverlay(graphicOverlay) {
const regions = graphicOverlay["data"];
const graphic_options = convertOptionNamesToCamelCase(
msg["graphic_options"] || {},
graphicOverlay["options"] || {},
);
if (!graphic_options["color"]) graphic_options["color"] = "red";
const overlay = A.graphicOverlay(graphic_options);
Expand Down Expand Up @@ -132,10 +132,6 @@ export default class MessageHandler {
}
}

handleChangeColormap(msg) {
this.aladin.getBaseImageLayer().setColormap(msg["colormap"]);
}

handleGetJPGThumbnail() {
this.aladin.exportAsPNG();
}
Expand All @@ -147,15 +143,15 @@ export default class MessageHandler {
this.aladin.select(selectionType);
}

handleAddTable(msg, buffers) {
const options = convertOptionNamesToCamelCase(msg["options"] || {});
handleAddTable(table) {
const options = convertOptionNamesToCamelCase(table["options"] || {});
const circleOptions = convertOptionNamesToCamelCase(
options.circleError || {},
);
const ellipseOptions = convertOptionNamesToCamelCase(
options.ellipseError || {},
);
const buffer = buffers[0].buffer;
const buffer = table.data.buffer;
const decoder = new TextDecoder("utf-8");
const blob = new Blob([decoder.decode(buffer)]);
const url = URL.createObjectURL(blob);
Expand Down
17 changes: 17 additions & 0 deletions js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,28 @@ function setDivHeight(height, div) {
}
}

// This is needed to know whether ipyaladin is run
// from a live session or from a static export (HTML)
// Depending on that, we will listen to specific overlay traitlets.
// See the overlays and _overlay_patch traitlets definition for more informations.
function isLiveSession(model) {
// ipywidgets v7/v8: model.comm is a Comm in live sessions, null/undefined in static HTML
if (model && model.comm && typeof model.comm.send === "function") return true;

// Extra heuristics if needed (some frontends expose a kernel on the manager)
const mgr = model?.widget_manager;
if (mgr?.kernel) return true; // classic jupyter notebook
if (mgr?.context?.sessionContext?.session?.kernel) return true; // JupyterLab

return false; // static export (HTML)
}

export {
snakeCaseToCamelCase,
convertOptionNamesToCamelCase,
Lock,
divNumber,
setDivNumber,
setDivHeight,
isLiveSession,
};
4 changes: 4 additions & 0 deletions js/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ function render({ model, el }) {
const wcs = { ...aladin.getViewWCS() };
model.set("_wcs", wcs);

// send colormap to python
const cmap = aladin.getBaseImageLayer().getColorCfg().colormap;
model.set("colormap", cmap);

// Tell the widget is loaded so that all stored calls waiting can be executed
model.set("_is_loaded", true);
model.save_changes();
Expand Down
25 changes: 24 additions & 1 deletion src/ipyaladin/utils/_region_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from astropy.coordinates.matrix_utilities import rotation_matrix
from astropy.units import Quantity

import traitlets

try:
from regions import (
RectangleSkyRegion,
Expand Down Expand Up @@ -89,7 +91,7 @@ def rectangle_to_polygon_region(region: RectangleSkyRegion) -> PolygonSkyRegion:
)


class RegionInfos:
class RegionInfos(traitlets.TraitType):
"""Extract information from a region.

Attributes
Expand All @@ -113,6 +115,27 @@ def __init__(self, region: Union[str, Region]) -> None:
self.options = {}
self.from_region(region)

# inherited from traitlets.TraitType
def validate(self, obj: any, value: dict) -> dict:
if not isinstance(value, dict):
self.error(obj, value)

required_keys = {"region_type", "infos", "options"}
missing = required_keys - value.keys()
if missing:
raise traitlets.TraitError(f"Missing keys: {missing}")

if not isinstance(value["options"], dict):
self.error(obj, value)

if not isinstance(value["region_type"], str):
self.error(obj, value)

if not isinstance(value["infos"], dict):
self.error(obj, value)

return value

def from_region(self, region: Union[str, Region]) -> None:
"""Parse a region to extract its information.

Expand Down
Loading
Loading