Files
aladin-lite/src/js/Aladin.js
2024-03-26 11:21:18 +01:00

2199 lines
78 KiB
JavaScript

// Copyright 2013 - UDS/CNRS
// The Aladin Lite program is distributed under the terms
// of the GNU General Public License version 3.
//
// This file is part of Aladin Lite.
//
// Aladin Lite is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Aladin Lite is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// The GNU General Public License is available in COPYING file
// along with Aladin Lite.
//
/******************************************************************************
* Aladin Lite project
*
* File Aladin.js (main class)
* Facade to expose Aladin Lite methods
*
* Author: Thomas Boch[CDS], Matthieu Baumann[CDS]
*
*****************************************************************************/
import { version } from './../../package.json';
import { View } from "./View.js";
import { Utils } from "./Utils";
import { Overlay } from "./Overlay.js";
import { Logger } from "./Logger.js";
import { ProgressiveCat } from "./ProgressiveCat.js";
import { Sesame } from "./Sesame.js";
import { PlanetaryFeaturesNameResolver } from "./PlanetaryFeaturesNameResolver.js";
import { CooFrameEnum } from "./CooFrameEnum.js";
import { MeasurementTable } from "./MeasurementTable.js";
import { ImageSurvey } from "./ImageSurvey.js";
import { Coo } from "./libs/astro/coo.js";
import { CooConversion } from "./CooConversion.js";
import { ProjectionEnum } from "./ProjectionEnum.js";
import { ALEvent } from "./events/ALEvent.js";
import { Color } from './Color.js';
import { ImageFITS } from "./ImageFITS.js";
import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js";
import { SAMPConnector } from "./vo/samp.js";
import { Reticle } from "./Reticle.js";
// GUI
import { AladinLogo } from "./gui/AladinLogo.js";
import { Location } from "./gui/Location.js";
import { FoV } from "./gui/FoV.js";
import { ShareActionButton } from "./gui/Button/ShareView.js";
import { ContextMenu } from "./gui/Widgets/ContextMenu.js";
import { Popup } from "./Popup.js";
import A from "./A.js";
import { StatusBarBox } from "./gui/Box/StatusBarBox.js";
import { FullScreenActionButton } from "./gui/Button/FullScreen.js";
import { ProjectionActionButton } from "./gui/Button/Projection.js";
// features
import { SettingsButton } from "./gui/Button/Settings";
import { SimbadPointer } from "./gui/Button/SimbadPointer";
import { OverlayStackButton } from "./gui/Button/OverlayStack";
import { GridEnabler } from './gui/Button/GridEnabler';
import { CooFrame } from './gui/Input/CooFrame';
/**
* @typedef {Object} AladinOptions
* @description Options for configuring the Aladin Lite instance.
*
* @property {string} [survey="CDS/P/DSS2/color"] URL or ID of the survey to use
* @property {string[]} [surveyUrl]
* Array of URLs for the survey images. This replaces the survey parameter.
* @property {string} [target="0 +0"] - Target coordinates for the initial view.
* @property {CooFrame} [cooFrame="J2000"] - Coordinate frame.
* @property {number} [fov=60] - Field of view in degrees.
* @property {string} [backgroundColor="rgb(60, 60, 60)"] - Background color in RGB format.
*
* @property {boolean} [showZoomControl=false] - Whether to show the zoom control toolbar.
* This element belongs to the FoV UI thus its CSS class is `aladin-fov`
* @property {boolean} [showLayersControl=true] - Whether to show the layers control toolbar.
* CSS class for that button is `aladin-stack-control`
* @property {boolean} [showFullscreenControl=true] - Whether to show the fullscreen control toolbar.
* CSS class for that button is `aladin-fullScreen-control`
* @property {boolean} [showSimbadPointerControl=false] - Whether to show the Simbad pointer control toolbar.
* CSS class for that button is `aladin-simbadPointer-control`
* @property {boolean} [showCooGridControl=false] - Whether to show the coordinate grid control toolbar.
* CSS class for that button is `aladin-grid-control`
* @property {boolean} [showSettingsControl=false] - Whether to show the settings control toolbar.
* CSS class for that button is `aladin-settings-control`
* @property {boolean} [showShareControl=false] - Whether to show the share control toolbar.
* CSS class for that button is `aladin-share-control`
* @property {boolean} [showStatusBar=true] - Whether to show the status bar. Enabled by default.
* CSS class for that button is `aladin-status-bar`
* @property {boolean} [showFrame=true] - Whether to show the viewport frame.
* CSS class for that button is `aladin-cooFrame`
* @property {boolean} [showFov=true] - Whether to show the field of view indicator.
* CSS class for that button is `aladin-fov`
* @property {boolean} [showCooLocation=true] - Whether to show the coordinate location indicator.
* CSS class for that button is `aladin-location`
* @property {boolean} [showProjectionControl=true] - Whether to show the projection control toolbar.
* CSS class for that button is `aladin-projection-control`
* @property {boolean} [showContextMenu=false] - Whether to show the context menu.
* @property {boolean} [showReticle=true] - Whether to show the reticle.
* @property {boolean} [showCatalog=true] - Whether to show the catalog.
* @property {boolean} [showCooGrid=true] - Whether the coordinates grid should be shown at startup.
*
* @property {boolean} [fullScreen=false] - Whether to start in full-screen mode.
* @property {string} [reticleColor="rgb(178, 50, 178)"] - Color of the reticle in RGB format.
* @property {number} [reticleSize=22] - Size of the reticle.
*
* @property {string} [gridColor="rgb(178, 50, 178)"] - Color of the grid in RGB format.
* Is overshadowed by gridOptions.color if defined.
* @property {number} [gridOpacity=0.8] - Opacity of the grid (0 to 1).
* Is overshadowed by gridOptions.opacity if defined.
* @property {Object} [gridOptions] - More options for the grid.
* @property {string} [gridOptions.color="rgb(178, 50, 178)"] - Color of the grid. Can be specified as a named color
* (see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/named-color| named colors}),
* as rgb (ex: "rgb(178, 50, 178)"), or as a hex color (ex: "#86D6AE").
* @property {number} [gridOptions.thickness=2] - The thickness of the grid, in pixels.
* @property {number} [gridOptions.opacity=0.8] - Opacity of the grid and labels. It is comprised between 0 and 1.
* @property {boolean} [gridOptions.showLabels=true] - Whether the grid has labels.
* @property {number} [gridOptions.labelSize=15] - The font size of the labels.
*
* @property {string} [projection="SIN"] - Projection type. Can be 'SIN' for orthographic, 'MOL' for mollweide, 'AIT' for hammer-aitoff, 'ZEA' for zenital equal-area or 'MER' for mercator
* @property {boolean} [log=true] - Whether to log events.
* @property {boolean} [samp=false] - Whether to enable SAMP (Simple Application Messaging Protocol).
* @property {boolean} [realFullscreen=false] - Whether to use real fullscreen mode.
* @property {boolean} [pixelateCanvas=true] - Whether to pixelate the canvas.
*/
/**
* @typedef {string} CooFrame
* String with possible values: 'equatorial', 'ICRS', 'ICRSd', 'j2000', 'gal, 'galactic'
*/
export let Aladin = (function () {
/**
* Creates an instance of the Aladin interactive sky atlas.
*
* @class
* @constructs Aladin
* @param {string|HTMLElement} aladinDiv - The ID of the HTML element or the HTML element itself
* where the Aladin sky atlas will be rendered.
* @param {AladinOptions} requestedOptions - Options to customize the behavior and appearance of the Aladin atlas.
* @throws {Error} Throws an error if aladinDiv is not provided or is invalid.
*
* @example
* // Usage example:
* import { A } from 'aladin-lite';
*
* let aladin = A.Aladin('#aladin-lite-div', { option1: 'value1', option2: 'value2' });
*/
var Aladin = function (aladinDiv, requestedOptions) {
this.callbacksByEventName = {}; // we store the callback functions (on 'zoomChanged', 'positionChanged', ...) here
// check that aladinDiv exists, stop immediately otherwise
if (!aladinDiv) {
console.error("Aladin div has not been found. Please check its name!");
return;
}
this.wasm = null;
this.aladinDiv = aladinDiv;
const self = this;
// if not options was set, try to retrieve them from the query string
if (requestedOptions === undefined) {
requestedOptions = this.getOptionsFromQueryString();
}
requestedOptions = requestedOptions || {};
// 'fov' option was previsouly called 'zoom'
if ('zoom' in requestedOptions) {
var fovValue = requestedOptions.zoom;
delete requestedOptions.zoom;
requestedOptions.fov = fovValue;
}
// merge with default options
var options = {
gridOptions: {}
};
// 'gridOptions' is an object, so it need it own loop
if ('gridOptions' in requestedOptions) {
for (var key in Aladin.DEFAULT_OPTIONS.gridOptions) {
if (requestedOptions.gridOptions[key] === undefined) {
options.gridOptions[key] = Aladin.DEFAULT_OPTIONS.gridOptions[key]
}
}
}
for (var key in Aladin.DEFAULT_OPTIONS) {
if (requestedOptions[key] !== undefined) {
options[key] = requestedOptions[key];
}
else {
options[key] = Aladin.DEFAULT_OPTIONS[key];
}
}
for (var key in requestedOptions) {
if (Aladin.DEFAULT_OPTIONS[key] === undefined) {
options[key] = requestedOptions[key];
}
}
this.options = options;
this.reduceDeformations = true;
// Init the measurement table
this.measurementTable = new MeasurementTable(this);
// parent div
aladinDiv.classList.add("aladin-container");
// set different options
// Reticle
this.view = new View(this);
// Aladin logo
new AladinLogo(this.aladinDiv);
this.reticle = new Reticle(this.options, this);
this.popup = new Popup(this.aladinDiv, this.view);
this.ui = [];
// Background color
if (options.backgroundColor) {
this.backgroundColor = options.backgroundColor;
this.setBackgroundColor(this.backgroundColor)
}
// Grid
let gridOptions = options.gridOptions;
// color and opacity can be defined by two variables. The item in gridOptions
// should take precedence.
gridOptions["color"] = options.gridOptions.color || options.gridColor;
gridOptions["opacity"] = options.gridOptions.opacity || options.gridOpacity;
if (options && options.showCooGrid) {
gridOptions.enabled = true;
}
this.setCooGrid(gridOptions);
this.gotoObject(options.target, undefined);
if (options.log) {
var params = options;
params['version'] = Aladin.VERSION;
Logger.log("startup", params);
}
if (options.catalogUrls) {
for (var k = 0, len = options.catalogUrls.length; k < len; k++) {
this.createCatalogFromVOTable(options.catalogUrls[k]);
}
}
if (options.survey) {
if (Array.isArray(options.survey)) {
let i = 0;
options.survey.forEach((rootURLOrId) => {
if (i == 0) {
this.setBaseImageLayer(rootURLOrId);
} else {
this.setOverlayImageLayer(rootURLOrId, Utils.uuidv4());
}
i++;
});
} else if (options.survey === ImageSurvey.DEFAULT_SURVEY_ID) {
// DSS is cached inside ImageSurvey class, no need to provide any further information
const survey = this.createImageSurvey(ImageSurvey.DEFAULT_SURVEY_ID);
this.setBaseImageLayer(survey);
} else {
this.setBaseImageLayer(options.survey)
}
} else {
// Add the image layers
// For that we check the survey key of options
// It can be given as a single string or an array of strings
// for multiple blending surveys
// take in priority the surveyUrl parameter
let url;
if (Array.isArray(options.surveyUrl)) {
// mirrors given, take randomly one
let numMirrors = options.surveyUrl.length;
let id = Math.floor(Math.random() * numMirrors);
url = options.surveyUrl[id]
} else {
url = options.surveyUrl;
}
this.setBaseImageLayer(url);
}
this.view.showCatalog(options.showCatalog);
// FullScreen toolbar icon
this.isInFullscreen = false;
// go to full screen ?
if (options.fullScreen === true) {
// strange behaviour to wait for a sec
self.toggleFullscreen(self.options.realFullscreen);
}
// maximize control
if (options.showFullscreenControl) {
// react to fullscreenchange event to restore initial width/height (if user pressed ESC to go back from full screen)
Utils.on(document, 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function (e) {
var fullscreenElt = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
if (fullscreenElt === null || fullscreenElt === undefined) {
//self.aladinDiv.classList.remove('aladin-fullscreen');
var fullScreenToggledFn = self.callbacksByEventName['fullScreenToggled'];
(typeof fullScreenToggledFn === 'function') && fullScreenToggledFn(self.isInFullscreen);
}
});
}
// set right click context menu
if (options.showContextMenu) {
this.contextMenu = new ContextMenu(this);
this.contextMenu.attach(DefaultActionsForContextMenu.getDefaultActions(this));
}
if (options.samp) {
this.samp = new SAMPConnector(this);
}
this._setupUI(options);
};
Aladin.prototype._setupUI = function(options) {
let self = this;
// Status bar
if (options.showStatusBar) {
this.statusBar = new StatusBarBox(this);
this.addUI(this.statusBar)
}
// Add the frame control
if (options.showFrame) {
this.addUI(new CooFrame(this))
}
// Add the location info
if (options.showCooLocation) {
this.addUI(new Location(this));
}
// Add the FoV info
if (options.showFov || options.showZoomControl) {
this.addUI(new FoV(this, options))
}
////////////////////////////////////////////////////
let stack = new OverlayStackButton(this);
let simbad = new SimbadPointer(this);
let grid = new GridEnabler(this);
this.addUI(stack)
this.addUI(simbad)
this.addUI(grid)
// Add the layers control
if (!options.showLayersControl) {
stack._hide();
}
// Add the simbad pointer control
if (!options.showSimbadPointerControl) {
simbad._hide();
}
// Add the projection control
// Add the coo grid control
if (!options.showCooGridControl) {
grid._hide();
}
// Settings control
if (options.showSettingsControl) {
let settings = new SettingsButton(this, {features: {stack, simbad, grid}});
this.addUI(settings)
}
// share control panel
if (options.showShareControl) {
this.addUI(new ShareActionButton(self));
}
if (options.showProjectionControl) {
this.projBtn = new ProjectionActionButton(this);
this.addUI(this.projBtn)
}
if (options.showFullscreenControl) {
this.addUI(new FullScreenActionButton(self))
}
this._applyMediaQueriesUI();
}
Aladin.prototype._applyMediaQueriesUI = function() {
const applyMediaQuery = function(maxWidth, matchingCallback, unmatchingCallback) {
function mqFunction(x) {
if (x.matches) { // If media query matches
matchingCallback()
} else {
unmatchingCallback()
}
}
// Create a MediaQueryList object
var mq = window.matchMedia("(max-width: " + maxWidth + ")")
// Attach listener function on state changes
mq.addEventListener("change", function() {
mqFunction(mq);
});
mqFunction(mq);
}
let self = this;
applyMediaQuery('48rem', () => {
if (self.projBtn) {
self.projBtn.update({verbosity: 'reduced'})
}
},
() => {
if (self.projBtn) {
self.projBtn.update({verbosity: 'full'})
}
})
}
/**** CONSTANTS ****/
Aladin.VERSION = version;
Aladin.JSONP_PROXY = "https://alaskybis.cds.unistra.fr/cgi/JSONProxy";
//Aladin.JSONP_PROXY = "https://alaskybis.unistra.fr/cgi/JSONProxy";
Aladin.URL_PREVIEWER = 'https://aladin.cds.unistra.fr/AladinLite/';
// access to WASM libraries
Aladin.wasmLibs = {};
Aladin.DEFAULT_OPTIONS = {
survey: ImageSurvey.DEFAULT_SURVEY_ID,
//surveyUrl: ["https://alaskybis.unistra.fr/DSS/DSSColor", "https://alasky.unistra.fr/DSS/DSSColor"],
target: "0 +0",
cooFrame: "J2000",
fov: 60,
backgroundColor: "rgb(60, 60, 60)",
// Zoom toolbar
showZoomControl: false,
// Menu toolbar
showLayersControl: true,
showFullscreenControl: true,
showSimbadPointerControl: false,
showCooGridControl: false,
showSettingsControl: false,
// Share toolbar
showShareControl: false,
// Viewport toolbar
showFrame: true,
showFov: true,
showCooLocation: true,
showProjectionControl: true,
// Other UI elements
showContextMenu: false,
showStatusBar: true,
// Internal
showReticle: true,
showCatalog: true, // TODO: still used ??
fullScreen: false,
reticleColor: "rgb(178, 50, 178)",
reticleSize: 22,
gridColor: "rgb(178, 50, 178)",
gridOpacity: 0.8,
gridOptions: {
enabled: false,
showLabels: true,
thickness: 2,
labelSize: 15
},
projection: 'SIN',
log: true,
samp: false,
realFullscreen: false,
pixelateCanvas: true
};
// realFullscreen: AL div expands not only to the size of its parent, but takes the whole available screen estate
Aladin.prototype.toggleFullscreen = function (realFullscreen) {
let self = this;
realFullscreen = Boolean(realFullscreen);
self.isInFullscreen = !self.isInFullscreen;
ContextMenu.hideAll()
//this.fullScreenBtn.attr('title', isInFullscreen ? 'Restore original size' : 'Full screen');
if (this.aladinDiv.classList.contains('aladin-fullscreen')) {
this.aladinDiv.classList.remove('aladin-fullscreen');
} else {
this.aladinDiv.classList.add('aladin-fullscreen');
}
if (realFullscreen) {
// go to "real" full screen mode
if (self.isInFullscreen) {
var d = this.aladinDiv;
if (d.requestFullscreen) {
d.requestFullscreen();
}
else if (d.webkitRequestFullscreen) {
d.webkitRequestFullscreen();
}
else if (d.mozRequestFullScreen) { // notice the difference in capitalization for Mozilla functions ...
d.mozRequestFullScreen();
}
else if (d.msRequestFullscreen) {
d.msRequestFullscreen();
}
}
// exit from "real" full screen mode
else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
}
else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
// Delay the fixLayoutDimensions layout for firefox
/*setTimeout(function () {
self.view.fixLayoutDimensions();
}, 1000);*/
// force call to zoomChanged callback
var fovChangedFn = self.callbacksByEventName['zoomChanged'];
(typeof fovChangedFn === 'function') && fovChangedFn(self.view.fov);
var fullScreenToggledFn = self.callbacksByEventName['fullScreenToggled'];
(typeof fullScreenToggledFn === 'function') && fullScreenToggledFn(isInFullscreen);
};
Aladin.prototype.getOptionsFromQueryString = function () {
var options = {};
var requestedTarget = Utils.urlParam('target');
if (requestedTarget) {
options.target = requestedTarget;
}
var requestedFrame = Utils.urlParam('frame');
if (requestedFrame && CooFrameEnum[requestedFrame]) {
options.frame = requestedFrame;
}
var requestedSurveyId = Utils.urlParam('survey');
if (requestedSurveyId && ImageSurvey.getSurveyInfoFromId(requestedSurveyId)) {
options.survey = requestedSurveyId;
}
var requestedZoom = Utils.urlParam('zoom');
if (requestedZoom && requestedZoom > 0 && requestedZoom < 180) {
options.zoom = requestedZoom;
}
var requestedShowreticle = Utils.urlParam('showReticle');
if (requestedShowreticle) {
options.showReticle = requestedShowreticle.toLowerCase() == 'true';
}
var requestedCooFrame = Utils.urlParam('cooFrame');
if (requestedCooFrame) {
options.cooFrame = requestedCooFrame;
}
var requestedFullscreen = Utils.urlParam('fullScreen');
if (requestedFullscreen !== undefined) {
options.fullScreen = requestedFullscreen;
}
return options;
};
/**
* Sets the field of view (FoV) of the Aladin instance to the specified angle in degrees.
*
* @memberof Aladin
* @param {number} FoV - The angle of the field of view in degrees.
*
* @example
* let aladin = A.aladin('#aladin-lite-div');
* aladin.setFoV(60);
*/
Aladin.prototype.setFoV = function (FoV) {
this.view.setZoom(FoV);
};
Aladin.prototype.setFov = Aladin.prototype.setFoV;
// @API
// (experimental) try to adjust the FoV to the given object name. Does nothing if object is not known from Simbad
Aladin.prototype.adjustFovForObject = function (objectName) {
var self = this;
this.getFovForObject(objectName, function (fovDegrees) {
self.setFoV(fovDegrees);
});
};
Aladin.prototype.getFovForObject = Aladin.prototype.getFoVForObject = function (objectName, callback) {
var query = "SELECT galdim_majaxis, V FROM basic JOIN ident ON oid=ident.oidref JOIN allfluxes ON oid=allfluxes.oidref WHERE id='" + objectName + "'";
var url = '//simbad.u-strasbg.fr/simbad/sim-tap/sync?query=' + encodeURIComponent(query) + '&request=doQuery&lang=adql&format=json&phase=run';
Utils.fetch({
url,
method: 'GET',
dataType: 'json',
success: (result) => {
var defaultFov = 4 / 60; // 4 arcmin
var fov = defaultFov;
if ('data' in result && result.data.length > 0) {
var galdimMajAxis = Utils.isNumber(result.data[0][0]) ? result.data[0][0] / 60.0 : null; // result gives galdim in arcmin
var magV = Utils.isNumber(result.data[0][1]) ? result.data[0][1] : null;
if (galdimMajAxis !== null) {
fov = 2 * galdimMajAxis;
}
else if (magV !== null) {
if (magV < 10) {
fov = 2 * Math.pow(2.0, (6 - magV / 2.0)) / 60;
}
}
}
(typeof callback === 'function') && callback(fov);
}
})
};
/**
* Sets the coordinate frame of the Aladin instance to the specified frame.
*
* @memberof Aladin
* @param {string} frameName - The name of the coordinate frame. Possible values: 'J2000', 'J2000d', 'GALACTIC'.
*
* @example
* // Set the coordinate frame to 'J2000'
* const aladin = A.aladin('#aladin-lite-div');
* aladin.setFrame('J2000');
*/
Aladin.prototype.setFrame = function (frameName) {
if (!frameName) {
return;
}
var newFrame = CooFrameEnum.fromString(frameName, CooFrameEnum.J2000);
if (newFrame == this.view.cooFrame) {
return;
}
this.view.changeFrame(newFrame);
var frameChangedFunction = this.callbacksByEventName['cooFrameChanged'];
if (typeof frameChangedFunction === 'function') {
frameChangedFunction(newFrame.label);
}
};
/**
* Sets the projection of the Aladin instance to the specified type.
*
* @memberof Aladin
* @param {string} projection The type of projection to set. Possible values
* <br>"TAN" (Gnomonic projection)
* <br>"STG" (Stereographic projection)
* <br>"SIN" (Orthographic projection)
* <br>"ZEA" (Zenital equal-area projection)
* <br>"MER" (Mercator projection)
* <br>"AIT" (Hammer-Aitoff projection)
* <br>"MOL" (Mollweide projection)
*
* @example
* // Set the projection to 'orthographic'
* let aladin = A.aladin('#aladin-lite-div');
* aladin.setProjection('SIN');
*/
Aladin.prototype.setProjection = function (projection) {
if (!projection) {
return;
}
this.view.setProjection(projection);
ALEvent.PROJECTION_CHANGED.dispatchedTo(this.aladinDiv, {projection: projection});
};
/**
* Append a message to the status bar with a specific duration
*
* @memberof Aladin
* @param {Object} options - The message to display
* @param {string} options.id - The id of the message, is useful for removing unlimited time messages
* @param {string} options.message - The message to display
* @param {string|number} options.duration - The duration of the message. Accepts a time in milliseconds or 'unlimited'
* @param {string} options.type - The type of the message. Can be 'loading', 'tooltip', 'info'
*
* @example
*
* aladin.addStatusBarMessage({
* duration: 10000,
* type: 'info',
* message: 'Aladin Lite v3.3 is out. New features available:<ul><li>New Button, Box objects</li><li>Polygonal, circular selection</li></ul>'
* })
*/
Aladin.prototype.addStatusBarMessage = function(options) {
if (this.statusBar) {
this.statusBar.appendMessage(options)
}
};
Aladin.prototype.getProjectionName = function() {
const self = this;
let projName = undefined;
for (let key in ProjectionEnum) {
if (ProjectionEnum[key] == self.view.projection) {
projName = key;
break;
}
};
return projName;
};``
/**
* Returns the current coordinate system: possible values are 'J2000', 'J2000d', and 'Galactic' .
*
* @memberof Aladin
* @returns {string} The current coordinate system: possible values are 'J2000', 'J2000d', and 'Galactic' .
*
* @example
* const aladin = A.aladin('#aladin-lite-div', {cooFrame: 'galactic'});
* let cooFrame = aladin.getFrame();
* assert(cooFrame, 'galactic')
*/
Aladin.prototype.getFrame = function() {
return this.view.cooFrame.label;
}
/**
* Moves the Aladin instance to the specified astronomical object.
*
* @memberof Aladin
* @param {string} targetName - The name or identifier of the astronomical object to move to.
* @param {Object} [callbackOptions] - Optional callback options.
* @param {function} [callbackOptions.success] - The callback function to execute on successful navigation.
* @param {function} [callbackOptions.error] - The callback function to execute on error during navigation.
*
* @example
* // Move to the astronomical object named 'M42' with callbacks
* const aladinInstance = A.aladin('#aladin-lite-div');
* aladinInstance.gotoObject('M42', {
* success: () => {
* console.log('Successfully moved to M42.');
* },
* error: (err) => {
* console.error('Error moving to M42:', err);
* }
* });
*/
Aladin.prototype.gotoObject = function (targetName, callbackOptions) {
let successCallback = undefined;
let errorCallback = undefined;
if (typeof callbackOptions === 'object') {
if (callbackOptions.hasOwnProperty('success')) {
successCallback = callbackOptions.success;
}
if (callbackOptions.hasOwnProperty('error')) {
errorCallback = callbackOptions.error;
}
}
// this is for compatibility reason with the previous method signature which was function(targetName, errorCallback)
else if (typeof callbackOptions === 'function') {
errorCallback = callbackOptions;
}
var isObjectName = /[a-zA-Z]/.test(targetName);
// try to parse as a position
if (!isObjectName) {
var coo = new Coo();
coo.parse(targetName);
// Convert from view coo sys to icrs
const [ra, dec] = this.wasm.viewToICRSCooSys(coo.lon, coo.lat);
this.view.pointTo(ra, dec);
(typeof successCallback === 'function') && successCallback(this.getRaDec());
}
// ask resolution by Sesame
else {
var self = this;
// sky case
(async () => {
let baseImageLayer;
if (this.getBaseImageLayer()) {
baseImageLayer = await this.getBaseImageLayer().query;
}
if (this.getBaseImageLayer() === undefined || !baseImageLayer.isPlanetaryBody()) {
Sesame.resolve(targetName,
function (data) { // success callback
// Location given in icrs at J2000
const coo = data.coo;
self.view.pointTo(coo.jradeg, coo.jdedeg);
(typeof successCallback === 'function') && successCallback(self.getRaDec());
},
function (data) { // errror callback
if (console) {
console.log("Could not resolve object name " + targetName);
console.log(data);
}
(typeof errorCallback === 'function') && errorCallback();
}
);
}
// planetary case
else {
const body = baseImageLayer.hipsBody;
PlanetaryFeaturesNameResolver.resolve(targetName, body,
function (data) { // success callback
self.view.pointTo(data.lon, data.lat);
(typeof successCallback === 'function') && successCallback(self.getRaDec());
},
function (data) { // error callback
if (console) {
console.log("Could not resolve object name " + targetName);
console.log(data);
}
(typeof errorCallback === 'function') && errorCallback();
}
);
}
})();
}
};
/**
* Moves the Aladin instance to the specified position.
*
* @memberof Aladin
* @param {number} lon - longitude in degrees
* @param {number} lat - latitude in degrees
* @param {string} frame - Optional callback options.
*
* @example
* // Move to position
* const aladin = A.aladin('#aladin-lite-div');
* aladin.gotoPosition(20, 10, "galactic");
*/
Aladin.prototype.gotoPosition = function (lon, lat, frame = undefined) {
var radec;
// convert the frame from string to CooFrameEnum
if (frame) {
frame = CooFrameEnum.fromString(this.options.cooFrame, CooFrameEnum.J2000);
}
// both are CooFrameEnum
let positionGivenFrame = frame || this.view.cooFrame;
// First, convert to J2000 if needed
if (positionGivenFrame === CooFrameEnum.GAL) {
radec = CooConversion.GalacticToJ2000([lon, lat]);
} else {
radec = [lon, lat];
}
this.view.pointTo(radec[0], radec[1]);
};
var idTimeoutAnim;
var doAnimation = function (aladin) {
if (idTimeoutAnim) {
clearTimeout(idTimeoutAnim)
}
var params = aladin.animationParams;
if (params == null || !params['running']) {
return;
}
var now = new Date().getTime();
// this is the animation end: set the view to the end position, and call complete callback
if (now > params['end']) {
aladin.gotoRaDec(params['raEnd'], params['decEnd']);
if (params['complete']) {
params['complete']();
}
return;
}
// compute current position
var fraction = (now - params['start']) / (params['end'] - params['start']);
var curPos = intermediatePoint(params['raStart'], params['decStart'], params['raEnd'], params['decEnd'], fraction);
var curRa = curPos[0];
var curDec = curPos[1];
//var curRa = params['raStart'] + (params['raEnd'] - params['raStart']) * (now-params['start']) / (params['end'] - params['start']);
//var curDec = params['decStart'] + (params['decEnd'] - params['decStart']) * (now-params['start']) / (params['end'] - params['start']);
aladin.gotoRaDec(curRa, curDec);
idTimeoutAnim = setTimeout(function () { doAnimation(aladin); }, 10);
};
/*
* Stop all animations that have been initiated by animateToRaDec or by zoomToFoV
* @API
*
*/
Aladin.prototype.stopAnimation = function () {
if (this.zoomAnimationParams) {
this.zoomAnimationParams['running'] = false;
}
if (this.animationParams) {
this.animationParams['running'] = false;
}
}
/*
* animate smoothly from the current position to the given ra, dec
*
* the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
*
* complete: a function to call once the animation has completed
*
* @API
*
*/
Aladin.prototype.animateToRaDec = function (ra, dec, duration, complete) {
duration = duration || 5;
this.animationParams = null;
var animationParams = {};
animationParams['start'] = new Date().getTime();
animationParams['end'] = new Date().getTime() + 1000 * duration;
var raDec = this.getRaDec();
animationParams['raStart'] = raDec[0];
animationParams['decStart'] = raDec[1];
animationParams['raEnd'] = ra;
animationParams['decEnd'] = dec;
animationParams['complete'] = complete;
animationParams['running'] = true;
this.animationParams = animationParams;
doAnimation(this);
};
var doZoomAnimation = function (aladin) {
var params = aladin.zoomAnimationParams;
if (params == null || !params['running']) {
return;
}
var now = new Date().getTime();
// this is the zoom animation end: set the view to the end fov, and call complete callback
if (now > params['end']) {
aladin.setFoV(params['fovEnd']);
if (params['complete']) {
params['complete']();
}
return;
}
// compute current position
var fraction = (now - params['start']) / (params['end'] - params['start']);
var curFov = params['fovStart'] + (params['fovEnd'] - params['fovStart']) * Math.sqrt(fraction);
aladin.setFoV(curFov);
setTimeout(function () { doZoomAnimation(aladin); }, 50);
};
/*
* zoom smoothly from the current FoV to the given new fov to the given ra, dec
*
* the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
*
* complete: a function to call once the animation has completed
*
* @API
*
*/
Aladin.prototype.zoomToFoV = function (fov, duration, complete) {
duration = duration || 5;
this.zoomAnimationParams = null;
var zoomAnimationParams = {};
zoomAnimationParams['start'] = new Date().getTime();
zoomAnimationParams['end'] = new Date().getTime() + 1000 * duration;
var fovArray = this.getFov();
zoomAnimationParams['fovStart'] = Math.max(fovArray[0], fovArray[1]);
zoomAnimationParams['fovEnd'] = fov;
zoomAnimationParams['complete'] = complete;
zoomAnimationParams['running'] = true;
this.zoomAnimationParams = zoomAnimationParams;
doZoomAnimation(this);
};
/**
* Compute intermediate point between points (lng1, lat1) and (lng2, lat2)
* at distance fraction times the total distance (fraction between 0 and 1)
*
* Return intermediate points in degrees
*
*/
function intermediatePoint(lng1, lat1, lng2, lat2, fraction) {
function degToRad(d) {
return d * Math.PI / 180;
}
function radToDeg(r) {
return r * 180 / Math.PI;
}
var lat1 = degToRad(lat1);
var lng1 = degToRad(lng1);
var lat2 = degToRad(lat2);
var lng2 = degToRad(lng2);
var d = 2 * Math.asin(
Math.sqrt(Math.pow((Math.sin((lat1 - lat2) / 2)),
2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.pow(Math.sin((lng1 - lng2) / 2), 2)));
var A = Math.sin((1 - fraction) * d) / Math.sin(d);
var B = Math.sin(fraction * d) / Math.sin(d);
var x = A * Math.cos(lat1) * Math.cos(lng1) + B *
Math.cos(lat2) * Math.cos(lng2);
var y = A * Math.cos(lat1) * Math.sin(lng1) + B *
Math.cos(lat2) * Math.sin(lng2);
var z = A * Math.sin(lat1) + B * Math.sin(lat2);
var lon = Math.atan2(y, x);
var lat = Math.atan2(z, Math.sqrt(Math.pow(x, 2) +
Math.pow(y, 2)));
return [radToDeg(lon), radToDeg(lat)];
};
/**
* Gets the current [Right Ascension, Declination] position of the center of the Aladin view.
*
* This method returns the celestial coordinates of the center of the Aladin view in the International
* Celestial Reference System (ICRS) or J2000 equatorial coordinates.
*
* @memberof Aladin
* @returns {number[]} - An array representing the [Right Ascension, Declination] coordinates in degrees.
* The first element is the Right Ascension (RA), and the second element is the Declination (Dec).
*/
Aladin.prototype.getRaDec = function () {
let radec = this.wasm.getCenter(); // This is given in the frame of the view
// We must convert it to ICRS
const radec_j2000 = this.wasm.viewToICRSCooSys(radec[0], radec[1]);
if (radec_j2000[0]<0) {
return [radec_j2000[0] + 360.0, radec_j2000[1]];
}
return radec_j2000;
};
/**
* point to a given position, expressed as a ra,dec coordinate
*
* @API
*/
Aladin.prototype.gotoRaDec = function (ra, dec) {
this.view.pointTo(ra, dec);
};
Aladin.prototype.showHealpixGrid = function (show) {
this.view.showHealpixGrid(show);
};
Aladin.prototype.healpixGrid = function () {
return this.view.displayHpxGrid;
};
Aladin.prototype.showSurvey = function (show) {
this.view.showSurvey(show);
};
Aladin.prototype.showCatalog = function (show) {
this.view.showCatalog(show);
};
Aladin.prototype.showReticle = function (show) {
this.reticle.update({show})
};
Aladin.prototype.getReticle = function () {
return this.reticle;
};
// these 4 methods should be merged into a unique "add" method
Aladin.prototype.addCatalog = function (catalog) {
this.view.addCatalog(catalog);
ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(this.aladinDiv, {layer: catalog});
};
Aladin.prototype.addOverlay = function (overlay) {
this.view.addOverlay(overlay);
ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(this.aladinDiv, {layer: overlay});
};
Aladin.prototype.addMOC = function (moc) {
this.view.addMOC(moc);
};
Aladin.prototype.addUI = function (ui) {
this.ui.push(ui);
ui.attachTo(this.aladinDiv)
// as the ui is pushed to the dom, setting position may need the aladin instance to work
// so we recompute it
if (ui.options) {
ui.update({position: {...ui.options.position, aladin: this}})
}
};
// @API
Aladin.prototype.findLayerByUUID = function(uuid) {
const result = this.view.allOverlayLayers.filter(layer => layer.uuid===uuid);
if (result.length==0) {
return null;
}
return result[0];
}
Aladin.prototype.removeLayers = function () {
this.view.removeLayers();
};
// @API
Aladin.prototype.removeLayer = function(layer) {
this.view.removeLayer(layer);
};
/**
* @deprecated
* Creates and return an image survey (HiPS) object
* Please use {@link A.imageHiPS} instead for creating a new survey image
*
* @memberof Aladin
* @param {string} id - Mandatory unique identifier for the survey.
* @param {string} [name] - A convinient name for the survey, optional
* @param {string} url - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>Or it can be a "CDS ID" that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* </ul>
* @param {string} [cooFrame] - Values accepted: 'equatorial', 'icrs', 'icrsd', 'j2000', 'gal', 'galactic'
* @param {number} [maxOrder] - The maximum HEALPix order of the HiPS, i.e the HEALPix order of the most refined tile images of the HiPS.
* @param {ImageSurveyOptions} [options] - Options describing the survey
* @returns {ImageSurvey} A HiPS image object.
*/
Aladin.prototype.createImageSurvey = function(id, name, url, cooFrame, maxOrder, options) {
let surveyOptions = ImageSurvey.cache[id];
if (!surveyOptions) {
surveyOptions = {url, name, maxOrder, cooFrame, ...options};
ImageSurvey.cache[id] = surveyOptions;
}
return new ImageSurvey(id, surveyOptions.url, surveyOptions);
};
/**
* @deprecated
* Creates and return an image survey (HiPS) object.
* Please use {@link A.imageHiPS} instead for creating a new survey image
*
* @function createImageSurvey
* @memberof Aladin
* @static
* @param {string} id - Mandatory unique identifier for the survey.
* @param {string} [name] - A convinient name for the survey, optional
* @param {string} url - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>Or it can be a "CDS ID" that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* </ul>
* @param {string} [cooFrame] - Values accepted: 'equatorial', 'icrs', 'icrsd', 'j2000', 'gal', 'galactic'
* @param {number} [maxOrder] - The maximum HEALPix order of the HiPS, i.e the HEALPix order of the most refined tile images of the HiPS.
* @param {ImageSurveyOptions} [options] - Options describing the survey
* @returns {ImageSurvey} A HiPS image object.
*/
Aladin.createImageSurvey = Aladin.prototype.createImageSurvey;
Aladin.prototype.createImageFITS = function(url, name, options, successCallback, errorCallback) {
try {
url = new URL(url);
} catch(e) {
// The url could be created
url = Utils.getAbsoluteURL(url)
url = new URL(url);
}
// Do not use proxy with CORS headers until we solve that: https://github.com/MattiasBuelens/wasm-streams/issues/20
//url = Utils.handleCORSNotSameOrigin(url);
let image = ImageFITS.cache[url];
if (!image) {
image = new ImageFITS(url, name, options, successCallback, errorCallback)
ImageFITS.cache[url] = image;
}
return image;
};
/**
* Creates a FITS image object
*
* @function createImageFITS
* @memberof Aladin
* @static
* @param {string} url - The url of the fits.
* @param {string} [name] - The url of the fits.
* @param {ImageSurveyOptions} [options] - Options for rendering the image
* @param {function} [success] - A success callback
* @param {function} [error] - A success callback
* @returns {ImageSurvey} A FITS image object.
*/
Aladin.createImageFITS = Aladin.prototype.createImageFITS;
/**
* @deprecated
* Create a new layer from an url or CDS ID.
* Please use {@link A.imageHiPS} instead for creating a new survey image
*
* @memberof Aladin
* @static
* @param {string} url - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>Or it can be a "CDS ID" that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* </ul>
* @param {ImageSurveyOptions} [options] - Options for rendering the image
* @param {function} [success] - A success callback
* @param {function} [error] - A success callback
* @returns {ImageSurvey} A FITS image object.
*/
Aladin.prototype.newImageSurvey = function(url, options) {
const id = url;
return A.imageHiPS(id, url, options);
}
/**
* Add a new HiPS layer to the view on top of the others
*
* @memberof Aladin
* @param {string|ImageSurvey} [survey="CDS/P/DSS2/color"] - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* <li>3. It can also be an {@link ImageSurvey} HiPS object created from {@link A.imageHiPS}</li>
* </ul>
* By default, the {@link https://alasky.cds.unistra.fr/DSS/DSSColor/|Digital Sky Survey 2} survey will be displayed
*/
Aladin.prototype.addNewImageLayer = function(survey="CDS/P/DSS2/color") {
let layerName = Utils.uuidv4();
this.setOverlayImageLayer(survey, layerName);
}
/**
* Change the base layer of the view
*
* It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageSurvey}/{@link ImageFITS} given
*
* @memberof Aladin
* @param {string|ImageSurvey|ImageFITS} urlOrHiPSOrFITS - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* <li>3. A {@link ImageSurvey} HiPS object created from {@link A.imageHiPS}</li>
* <li>4. A {@link ImageFITS} FITS image object</li>
* </ul>
*/
Aladin.prototype.setImageLayer = function(imageLayer) {
this.setBaseImageLayer(imageLayer);
};
/**
* Change the base layer of the view
*
* It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageSurvey}/{@link ImageFITS} given
*
* @memberof Aladin
* @param {string|ImageSurvey|ImageFITS} urlOrHiPSOrFITS - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* <li>3. A {@link ImageSurvey} HiPS object created from {@link A.imageHiPS}</li>
* <li>4. A {@link ImageFITS} FITS image object</li>
* </ul>
*/
Aladin.prototype.setImageSurvey = Aladin.prototype.setImageLayer;
// @param imageSurvey : ImageSurvey object or image survey identifier
// @api
// @old
Aladin.prototype.setBackgroundColor = function(rgb) {
this.backgroundColor = new Color(rgb);
// Once the wasm is ready, send the color to change it
ALEvent.AL_USE_WASM.dispatchedTo(this.aladinDiv, {callback: (wasm) => {
wasm.setBackgroundColor(this.backgroundColor);
ALEvent.BACKGROUND_COLOR_CHANGED.dispatchedTo(this.aladinDiv, {color: this.backgroundColor});
}})
};
Aladin.prototype.getBackgroundColor = function() {
return this.backgroundColor;
};
// @api
Aladin.prototype.removeImageLayer = function(layer) {
this.view.removeImageLayer(layer);
};
/**
* Change the base layer of the view
*
* @memberof Aladin
* @param {string|ImageSurvey|ImageFITS} urlOrHiPSOrFITS - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* <li>3. A {@link ImageSurvey} HiPS object created from {@link A.imageHiPS}</li>
* <li>4. A {@link ImageFITS} FITS image object</li>
* </ul>
*/
Aladin.prototype.setBaseImageLayer = function(urlOrHiPSOrFITS) {
return this.setOverlayImageLayer(urlOrHiPSOrFITS, "base");
};
// @api
Aladin.prototype.getBaseImageLayer = function () {
return this.view.getImageLayer("base");
};
/**
* Add a new HiPS/FITS image layer in the view
*
* @memberof Aladin
* @param {string|ImageSurvey|ImageFITS} urlOrHiPSOrFITS - Can be:
* <ul>
* <li>1. An url that refers to a HiPS.</li>
* <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
* <li>3. A {@link ImageSurvey} HiPS object created from {@link A.imageHiPS}</li>
* <li>4. A {@link ImageFITS} FITS image object</li>
* </ul>
* @param {string} [layer="overlay"] - A layer name. By default 'overlay' is chosen and it is destined to be plot
* on top the 'base' layer. If the layer is already present in the view, it will be replaced by the new HiPS/FITS image given here.
*/
Aladin.prototype.setOverlayImageLayer = function (urlOrHiPSOrFITS, layer = "overlay") {
let imageLayer;
// 1. User gives an ID
if (typeof urlOrHiPSOrFITS === "string") {
const idOrUrl = urlOrHiPSOrFITS;
imageLayer = A.imageHiPS(idOrUrl, idOrUrl);
// 2. User gives a non resolved promise
} else {
imageLayer = urlOrHiPSOrFITS;
}
return this.view.setOverlayImageLayer(imageLayer, layer);
};
// @api
Aladin.prototype.getOverlayImageLayer = function(layer = "overlay") {
const survey = this.view.getImageLayer(layer);
return survey;
};
// @api
Aladin.prototype.increaseZoom = function () {
this.view.increaseZoom(0.01);
};
Aladin.prototype.decreaseZoom = function () {
this.view.decreaseZoom(0.01);
};
Aladin.prototype.setRotation = function(rotation) {
this.view.setRotation(rotation);
}
// @api
// Set the current layer that is targeted
// Rightclicking for changing the cuts is done the targeted layer
Aladin.prototype.selectLayer = function (layer) {
this.view.selectLayer(layer);
}
Aladin.prototype.getSelectedLayer = function () {
return this.view.selectedLayer;
}
// Get the list of overlays
Aladin.prototype.getOverlays = function () {
return this.view.allOverlayLayers;
}
// Get the list of overlays
Aladin.prototype.getImageOverlays = function () {
return this.view.overlayLayers;
}
Aladin.prototype.isHpxGridDisplayed = function () {
return this.view.displayHpxGrid;
}
Aladin.prototype.isReticleDisplayed = function () {
return this.reticle.isVisible();
}
Aladin.prototype.createProgressiveCatalog = function (url, frame, maxOrder, options) {
return new ProgressiveCat(url, frame, maxOrder, options);
};
Aladin.prototype.createOverlay = function (options) {
return new Overlay(options);
};
// Select corresponds to rectangular selection
Aladin.AVAILABLE_CALLBACKS = [
'select',
'objectClicked',
'objectHovered',
'objectHoveredStop',
'footprintClicked',
'footprintHovered',
'positionChanged',
'zoomChanged',
'click',
'rightClickMove',
'mouseMove',
'fullScreenToggled',
'cooFrameChanged'
];
// API
//
// setting callbacks
Aladin.prototype.on = function (what, myFunction) {
if (Aladin.AVAILABLE_CALLBACKS.indexOf(what) < 0) {
return;
}
this.callbacksByEventName[what] = myFunction;
if (what === "positionChanged") {
// tell the backend about that callback
// because it needs to be called when the inertia is done
ALEvent.AL_USE_WASM.dispatchedTo(this.aladinDiv, {callback: (wasm) => {
let myFunctionThrottled = Utils.throttle(
myFunction,
View.CALLBACKS_THROTTLE_TIME_MS,
);
wasm.setCallbackPositionChanged(myFunctionThrottled);
}})
}
};
Aladin.prototype.addListener = function(alEventName, customFn) {
new ALEvent(alEventName).listenedBy(this.aladinDiv, customFn);
};
Aladin.prototype.selectObjects = function(objects) {
this.view.selectObjects(objects)
};
/**
* Enters selection mode
*
* @memberof Aladin
* @param {string} mode=rect - The mode of selection, can be either, 'rect', 'poly', or 'circle'
* @param {function} callback - A function called once the selection has been done
*
* @example
* // Creates and add a MOC from the user polygonal selection
* aladin.select('poly', p => {
* try {
* let ra = []
* let dec = []
* for (const v of p.vertices) {
* let [lon, lat] = aladin.pix2world(v.x, v.y);
* ra.push(lon)
* dec.push(lat)
* }
*
* let moc = A.MOCFromPolygon(
* {ra, dec},
* {name: 'poly', lineWidth: 3.0, color: 'pink'},
* );
* aladin.addMOC(moc)
* } catch(_) {
* alert('Selection covers a region out of the projection definition domain.');
* }
*})
*/
Aladin.prototype.select = async function (mode = 'rect', callback) {
await this.reticle.loaded;
this.fire('selectstart', {mode, callback});
};
Aladin.prototype.fire = function (what, params) {
if (what === 'selectstart') {
const {mode, callback} = params;
this.view.startSelection(mode, callback);
}
else if (what === 'simbad') {
this.view.setMode(View.TOOL_SIMBAD_POINTER);
} else if (what === 'default') {
this.view.setMode(View.PAN);
}
};
Aladin.prototype.hideBoxes = function () {
if (this.boxes) {
for (var k = 0; k < this.boxes.length; k++) {
if (typeof this.boxes[k].hide === "function") {
this.boxes[k].hide();
}
}
}
};
// TODO : LayerBox (or Stack?) must be extracted as a separate object
Aladin.prototype.showLayerBox = function () {
this.stack.showImageLayerBox();
};
/**
* Sets the coordinate grid options for the Aladin Lite view.
*
* This method allows you to customize the appearance of the coordinate grid in the Aladin Lite view.
*
* @memberof Aladin
* @param {Object} options - Options to customize the coordinate grid.
* @param {string} [options.color] - The color of the coordinate grid.
* @param {number} [options.opacity] - The opacity of the coordinate grid (value between 0 and 1).
* @param {number} [options.labelSize] - The size of the coordinate grid labels in pixels.
* @param {number} [options.thickness] - The thickness of the coordinate grid lines.
* @param {boolean} [options.enabled] - If true, the coordinate grid is enabled; otherwise, it is disabled.
*
* @example
* // Set the coordinate grid color to red
* aladin.setCooGrid({ color: 'red' });
*
* // Enable the coordinate grid
* aladin.setCooGrid({ enabled: true });
*/
Aladin.prototype.setCooGrid = function(options) {
if (options.color) {
// 1. the user has maybe given some
options.color = new Color(options.color)
// 3. convert from 0-255 to 0-1
options.color.r /= 255;
options.color.g /= 255;
options.color.b /= 255;
}
this.view.setGridConfig(options);
}
Aladin.prototype.getGridOptions = function() {
return this.view.getGridConfig();
}
Aladin.prototype.showCooGrid = function () {
this.view.setGridConfig({enabled: true});
};
Aladin.prototype.hideCooGrid = function() {
this.view.setGridConfig({enabled: false});
}
Aladin.prototype.layerByName = function (name) {
var c = this.view.allOverlayLayers;
for (var k = 0; k < c.length; k++) {
if (name == c[k].name) {
return c[k];
}
}
return null;
};
// TODO : integrate somehow into API ?
Aladin.prototype.exportAsPNG = function (downloadFile = false) {
(async () => {
const url = await this.getViewDataURL();
if (downloadFile) {
Utils.download(url, "screenshot");
} else {
// open a new window
var w = window.open();
w.document.write('<img src="' + url + '" width="' + this.view.width + 'px">');
w.document.title = 'Aladin Lite snapshot';
}
})();
};
/**
* Return the current view as a data URL (base64-formatted string)
* Parameters:
* - options (optional): object with attributs
* * format (optional): 'image/png' or 'image/jpeg'
* * width: width in pixels of the image to output
* * height: height in pixels of the image to output
*
* @API
*/
Aladin.prototype.getViewDataURL = async function (options) {
var options = options || {};
// support for old API signature
if (typeof options !== 'object') {
var imgFormat = options;
options = { format: imgFormat };
}
const canvasDataURL = await this.view.getCanvasDataURL(options.format, options.width, options.height);
return canvasDataURL;
}
/**
* Return the current view WCS as a key-value dictionary
* Can be useful in coordination with getViewDataURL
*
* NOTE + TODO : Rotations are not implemented yet
*
* @API
*/
Aladin.prototype.getViewWCS = function () {
// get general view properties
const center = this.wasm.getCenter();
const fov = this.getFov();
const width = this.view.width;
const height = this.view.height;
// get values common for all
let cdelt1 = fov[0] / width;
const cdelt2 = fov[1] / height;
const projectionName = this.getProjectionName();
if (projectionName == "FEYE")
return "Fish eye projection is not supported by WCS standards.";
// reversed longitude case
if (this.getBaseImageLayer().longitudeReversed) {
cdelt1 = -cdelt1;
}
// solar system object dict from planetary fits standard
// https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2018EA000388
const solarSystemObjects = {
"earth": "EA",
"moon": "SE",
"mercury": "ME",
"venus": "VE",
"mars": "MA",
"jupiter": "JU",
"saturn": "SA",
"uranus": "UR",
"neptune": "NE",
// satellites other than the Moon
"satellite": "ST" // not findable in the hips properties?
};
// we define a generic LON LAT keyword for unknown body types
let cooType1 = "LON--";
let cooType2 = "LAT--";
// just in case it would be equatorial
let radecsys;
if (this.getBaseImageLayer().isPlanetaryBody()) {
const body = this.getBaseImageLayer().hipsBody
if (body in solarSystemObjects) {
cooType1 = `${solarSystemObjects[body]}LN-`;
cooType2 = `${solarSystemObjects[body]}LT-`;
}
} else {
switch (this.getFrame()) {
case "J2000":
case "J2000d":
cooType1 = "RA---";
cooType2 = "DEC--";
radecsys = "ICRS ";
break;
case "Galactic":
cooType1 = "GLON-";
cooType2 = "GLAT-";
}
}
const WCS = {
NAXIS: 2,
NAXIS1: width,
NAXIS2: height,
CRPIX1: width / 2 + 0.5,
CRPIX2: height / 2 + 0.5,
CRVAL1: center[0],
CRVAL2: center[1],
CTYPE1: cooType1 + projectionName,
CTYPE2: cooType2 + projectionName,
CUNIT1: "deg ",
CUNIT2: "deg ",
CDELT1: cdelt1,
CDELT2: cdelt2
};
// handle the case of equatorial coordinates that need
// the radecsys keyword
if (radecsys == "ICRS ")
WCS.RADECSYS = radecsys;
return WCS;
}
/**
* Restrict the FoV range between a min and a max value
*
* @memberof Aladin
* @param {number} minFoV - in degrees when zoom in at max. If undefined, the zooming in is not limited
* @param {number} maxFoV - in degrees when zoom out at max. If undefined, the zooming out is not limited
*
* @example
* let aladin = A.aladin('#aladin-lite-div');
* aladin.setFoVRange(30, 60);
*/
Aladin.prototype.setFoVRange = function (minFoV, maxFoV) {
if (minFoV > maxFoV) {
var tmp = minFoV;
minFoV = maxFoV;
maxFoV = tmp;
}
this.view.minFoV = minFoV;
this.view.maxFoV = maxFoV;
// reset the field of view
this.setFoV(this.view.fov)
};
Aladin.prototype.setFOVRange = Aladin.prototype.setFoVRange;
/**
* Transform pixel coordinates to world coordinates.
*
* The origin (0,0) of pixel coordinates is at the top-left corner of the Aladin Lite view.
*
* @memberof Aladin
* @param {number} x - The x-coordinate in pixel coordinates.
* @param {number} y - The y-coordinate in pixel coordinates.
* @param {CooFrame} [frame] - The frame in which we want to retrieve the coordinates.
* If not given, the frame chosen is the one from the view
*
* @returns {number[]} - An array representing the [Right Ascension, Declination] coordinates in degrees in the `frame`.
* If not specified, returns the coo in the frame of the current view.
*
* @throws {Error} Throws an error if an issue occurs during the transformation.
*/
Aladin.prototype.pix2world = function (x, y, frame) {
let radec = this.view.wasm.screenToWorld(x, y);
frame = frame || this.view.cooFrame.label;
frame = CooFrameEnum.fromString(frame, CooFrameEnum.J2000);
if (frame !== this.view.cooFrame) {
if (frame.label === 'Galactic') {
console.warn('Conversion from icrs to galactic not yet impl')
} else {
radec = this.view.wasm.viewToICRSCooSys(radec[0], radec[1]);
}
}
let [ra, dec] = radec;
if (ra < 0) {
return [ra + 360.0, dec];
}
return [ra, dec];
};
/**
* Transform world coordinates to pixel coordinates in the view.
*
* @memberof Aladin
* @param {number} ra - The Right Ascension (RA) coordinate in degrees. Frame considered is the current view frame
* @param {number} dec - The Declination (Dec) coordinate in degrees. Frame considered is the current view frame
*
* @returns {number[]} - An array representing the [x, y] coordinates in pixel coordinates in the view.
*
* @throws {Error} Throws an error if an issue occurs during the transformation.
*/
Aladin.prototype.world2pix = function (ra, dec) {
return this.view.wasm.worldToScreen(ra, dec);
};
/**
* Get the angular distance in degrees between two locations
*
* @memberof Aladin
* @param {number} x1 - The x-coordinate of the first pixel coordinates.
* @param {number} y1 - The y-coordinate of the first pixel coordinates.
* @param {number} x2 - The x-coordinate of the second pixel coordinates.
* @param {number} y2 - The y-coordinate of the second pixel coordinates.
* @param {CooFrame} [frame] - The frame in which we want to retrieve the coordinates.
* If not given, the frame chosen is the one from the view
*
* @returns {number} - The angular distance between the two pixel coordinates in degrees
*
* @throws {Error} Throws an error if an issue occurs during the transformation.
*/
Aladin.prototype.angularDist = function (x1, y1, x2, y2, frame) {
const [ra1, dec1] = this.pix2world(x1, y1, frame);
const [ra2, dec2] = this.pix2world(x2, y2, frame);
return this.wasm.angularDist(ra1, dec1, ra2, dec2);
};
/**
* Gets a set of points along the current Field of View (FoV) corners.
*
* @memberof Aladin
* @param {number} nbSteps - The number of points to return along each side (the total number of points returned is 4 * nbSteps).
*
* @returns {number[][]} - A set of positions along the current FoV with the following format: [[ra1, dec1], [ra2, dec2], ..., [ra_n, dec_n]].
* The positions will be given in degrees
*
* @throws {Error} Throws an error if an issue occurs during the transformation.
*
*/
Aladin.prototype.getFoVCorners = function (nbSteps) {
// default value: 1
if (!nbSteps || nbSteps < 1) {
nbSteps = 1;
}
var points = [];
var x1, y1, x2, y2;
for (var k = 0; k < 4; k++) {
x1 = (k == 0 || k == 3) ? 0 : this.view.width - 1;
y1 = (k < 2) ? 0 : this.view.height - 1;
x2 = (k < 2) ? this.view.width - 1 : 0;
y2 = (k == 1 || k == 2) ? this.view.height - 1 : 0;
for (var step = 0; step < nbSteps; step++) {
let radec = this.wasm.screenToWorld(x1 + step / nbSteps * (x2 - x1), y1 + step / nbSteps * (y2 - y1));
points.push(radec);
}
}
return points;
};
/**
* Gets the current Field of View (FoV) size in degrees as a 2-element array.
*
* @memberof Aladin
* @returns {number[]} - A 2-element array representing the current FoV size in degrees. The first element is the FoV width,
* and the second element is the FoV height.
*/
Aladin.prototype.getFov = function () {
// can go up to 1000 deg
var fovX = this.view.fov;
var s = this.getSize();
// constrain to the projection definition domain
fovX = Math.min(fovX, this.view.projection.fov);
var fovY = s[1] / s[0] * fovX;
fovY = Math.min(fovY, 180);
// TODO : take into account AITOFF projection where fov can be larger than 180
return [fovX, fovY];
};
Aladin.prototype.getFoV = Aladin.prototype.getFov;
/**
* Returns the size in pixels for the Aladin view
*
* @memberof Aladin
* @returns {number[]} - A 2-element array representing the current Aladin view size in pixels. The first element is the width,
* and the second element is the height.
*/
Aladin.prototype.getSize = function () {
return [this.view.width, this.view.height];
};
/**
* Returns the HTML div element
*
* @memberof Aladin
* @return {HTMLElement} - The aladin lite div HTML element
*/
Aladin.prototype.getParentDiv = function () {
return this.aladinDiv;
};
// @API
/*
* return a Box GUI element to insert content
*/
/*Aladin.prototype.box = function (options) {
var box = new Box(options);
box.$parentDiv.appendTo(this.aladinDiv);
return box;
};*/
// @API
/*
* show popup at ra, dec position with given title and content
*
* If circleRadius, the corresponding circle will also be plotted
*/
Aladin.prototype.showPopup = function (ra, dec, title, content, circleRadius) {
this.view.catalogForPopup.removeAll();
this.view.overlayForPopup.removeAll();
let marker;
if (circleRadius !== undefined) {
this.view.overlayForPopup.add(A.circle(ra, dec, circleRadius, {fillColor: 'rgba(255, 0, 0, 0.2)'}));
marker = A.marker(ra, dec, { popupTitle: title, popupDesc: content, useMarkerDefaultIcon: true });
}
else {
marker = A.marker(ra, dec, { popupTitle: title, popupDesc: content, useMarkerDefaultIcon: false });
}
this.view.catalogForPopup.addSources(marker);
this.view.overlayForPopup.show();
this.view.catalogForPopup.show();
this.popup.setTitle(title);
this.popup.setText(content);
this.popup.setSource(marker);
this.popup.show();
};
// @API
/*
* hide popup
*/
Aladin.prototype.hidePopup = function () {
this.popup.hide();
};
// @API
/*
* return a URL allowing to share the current view
*/
Aladin.prototype.getShareURL = function () {
var radec = this.getRaDec();
var coo = new Coo();
coo.prec = 7;
coo.lon = radec[0];
coo.lat = radec[1];
return Aladin.URL_PREVIEWER + '?target=' + encodeURIComponent(coo.format('s')) +
'&fov=' + this.getFov()[0].toFixed(2) + '&survey=' + encodeURIComponent(this.getBaseImageLayer().id || this.getBaseImageLayer().rootUrl);
};
// @API
/*
* return, as a string, the HTML embed code
*/
Aladin.prototype.getEmbedCode = function () {
var radec = this.getRaDec();
var coo = new Coo();
coo.prec = 7;
coo.lon = radec[0];
coo.lat = radec[1];
var survey = this.getBaseImageLayer().url;
var fov = this.getFov()[0];
let s = '';
const NL = "\n";
s += '<div id="aladin-lite-div" style="width:400px;height:400px;"></div>' + NL;
s += '<script src="https://aladin.cds.unistra.fr/AladinLite/api/v3/latest/aladin.js" charset="utf-8"></script>' + NL;
s += '<script>' + NL;
s += "let aladin;" + NL + "A.init.then(() => {" + NL + " aladin = A.aladin('#aladin-lite-div', {survey: '" + survey + "', fov: " + fov.toFixed(2) + ', target: "' + coo.format('s') + '"});' + NL + '});' + NL;
s += '</script>';
return s;
};
/**
* Display a JPEG image in the Aladin Lite view.
*
* @memberof Aladin
* @param {string} url - The URL of the JPEG image.
* @param {Object} options - Options to customize the display. Can include the following properties:
* @param {string} options.label - A label for the displayed image.
* @param {number} options.order - The desired HEALPix order format.
* @param {boolean} options.nocache - True if you want to disable the cache
* @param {number} options.transparency - Opacity of the image rendered in aladin lite. Between 0 and 1.
* @param {Function} successCallback - The callback function to be executed on a successful display.
* The callback gives the ra, dec, and fov of the image;
* @param {Function} errorCallback - The callback function to be executed if an error occurs during display.
*
* @example
* aladin.displayJPG(
* // the JPG to transform to HiPS
* 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg',
* {
* transparency: 0.6,
* label: 'NOIRLab image'
* },
* (ra, dec, fov) => {
* // your code here
* })
*);
*/
Aladin.prototype.displayFITS = function (
url,
options,
successCallback,
errorCallback,
layer = "base"
) {
successCallback = successCallback || ((ra, dec, fov, _) => {
this.gotoRaDec(ra, dec);
this.setFoV(fov);
});
const imageFits = this.createImageFITS(url, url, options, successCallback, errorCallback);
return this.setOverlayImageLayer(imageFits, layer);
};
/**
* Display a JPEG image in the Aladin Lite view.
*
* @memberof Aladin
* @param {string} url - The URL of the JPEG image.
* @param {Object} options - Options to customize the display. Can include the following properties:
* @param {string} options.label - A label for the displayed image.
* @param {number} options.order - The desired HEALPix order format.
* @param {boolean} options.nocache - True if you want to disable the cache
* @param {number} options.transparency - Opacity of the image rendered in aladin lite. Between 0 and 1.
* @param {Function} successCallback - The callback function to be executed on a successful display.
* The callback gives the ra, dec, and fov of the image;
* @param {Function} errorCallback - The callback function to be executed if an error occurs during display.
*
* @example
* aladin.displayJPG(
* // the JPG to transform to HiPS
* 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg',
* {
* transparency: 0.6,
* label: 'NOIRLab image'
* },
* (ra, dec, fov) => {
* // your code here
* })
*);
*/
Aladin.prototype.displayJPG = function (url, options, successCallback, errorCallback) {
options = options || {};
options.color = true;
options.label = options.label || "JPG/PNG image";
options.outputFormat = 'png';
options = options || {};
var data = { url };
if (options.color) {
data.color = true;
}
if (options.outputFormat) {
data.format = options.outputFormat;
}
if (options.order) {
data.order = options.order;
}
if (options.nocache) {
data.nocache = options.nocache;
}
let self = this;
const request = ( url, params = {}, method = 'GET' ) => {
let options = {
method
};
if ( 'GET' === method ) {
url += '?' + ( new URLSearchParams( params ) ).toString();
} else {
options.body = JSON.stringify( params );
}
return fetch( url, options ).then( response => response.json() );
};
const get = ( url, params ) => request( url, params, 'GET' );
get('https://alasky.unistra.fr/cgi/fits2HiPS', data)
.then(async (response) => {
if (response.status != 'success') {
console.error('An error occured: ' + response.message);
if (errorCallback) {
errorCallback(response.message);
}
return;
}
var label = options.label;
var meta = response.data.meta;
const survey = self.createImageSurvey(response.data.url, label, response.data.url);
self.setOverlayImageLayer(survey, "overlay");
var transparency = (options && options.transparency) || 1.0;
survey.setOpacity(transparency);
var executeDefaultSuccessAction = true;
if (successCallback) {
executeDefaultSuccessAction = successCallback(meta.ra, meta.dec, meta.fov);
}
if (executeDefaultSuccessAction === true) {
self.wasm.setCenter(meta.ra, meta.dec);
self.setFoV(meta.fov);
}
// TODO! set an image survey once the already loaded surveys
// are READY! Otherwise it can lead to some congestion and avoid
// downloading the base tiles of the other surveys loading!
// This has to be fixed in the backend but a fast fix is just to wait
// before setting a new image survey
});
};
Aladin.prototype.displayPNG = Aladin.prototype.displayJPG;
/*
Aladin.prototype.setReduceDeformations = function (reduce) {
this.reduceDeformations = reduce;
this.view.requestRedraw();
}
*/
return Aladin;
})();