diff --git a/CHANGELOG.md b/CHANGELOG.md index 09444a59d..08325f216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.3.3 +* [feat] New `hipsList` option parameter when instancing a new Aladin object. * [feat] Zoom smoothing using hermite cubic interpolation functions * [feat] shape option of Catalog and ProgressiveCat accepts a function returning a Footprint. This allow user to associate a footprint to a specific source diff --git a/assets/icons/add.svg b/assets/icons/add.svg new file mode 100644 index 000000000..92c79aa44 --- /dev/null +++ b/assets/icons/add.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/al-catalog-hips-shape.html b/examples/al-catalog-hips-shape.html index 341f8f884..7d19b0250 100644 --- a/examples/al-catalog-hips-shape.html +++ b/examples/al-catalog-hips-shape.html @@ -10,26 +10,8 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {target: '12 25 41.512 +12 48 47.2', fov: 1, showContextMenu: true}); + aladin = A.aladin('#aladin-lite-div', {target: '12 25 41.512 +12 48 47.2', inertia: false, fov: 1, showContextMenu: true}); // define custom draw function - var drawFunction = function(source, canvasCtx, viewParams) { - canvasCtx.beginPath(); - canvasCtx.arc(source.x, source.y, source.data['coo_err_min'] * 5, 0, 2 * Math.PI, false); - canvasCtx.closePath(); - canvasCtx.strokeStyle = '#c38'; - canvasCtx.lineWidth = 3; - canvasCtx.globalAlpha = 0.7, - canvasCtx.stroke(); - var fov = Math.max(viewParams['fov'][0], viewParams['fov'][1]); - - // object name is displayed only if fov<10° - if (fov>10) { - return; - } - - canvasCtx.globalAlpha = 0.9; - canvasCtx.globalAlpha = 1; - }; var drawFunctionFootprint = function(s) { let a = +s.data.size_maj; @@ -39,8 +21,8 @@ if (!galaxy) return; - let angle = +s.data.size_angle || 0.0; - return A.ellipse(s.ra, s.dec, a / 60, b / 60, angle, {color: 'cyan'}); + let theta = +s.data.size_angle || 0.0; + return A.ellipse(s.ra, s.dec, a / 60, b / 60, theta, {color: 'cyan'}); }; var hips = A.catalogHiPS('https://axel.u-strasbg.fr/HiPSCatService/Simbad', {onClick: 'showTable', name: 'Simbad', color: 'cyan', hoverColor: 'red', shape: drawFunctionFootprint}); diff --git a/examples/al-coronelli.html b/examples/al-coronelli.html index 69593a90a..566d29b69 100644 --- a/examples/al-coronelli.html +++ b/examples/al-coronelli.html @@ -231,7 +231,7 @@ A.init.then(() => { var hipsDir="http://alasky.u-strasbg.fr/CDS_P_Coronelli"; - aladin = A.aladin("#aladin-lite-div", {showSimbadPointerControl: true, realFullscreen: true, fov: 100, allowFullZoomout: true, showReticle: false }); + aladin = A.aladin("#aladin-lite-div", {showSimbadPointerControl: true, expandLayersControl: true, realFullscreen: true, fov: 100, allowFullZoomout: true, showReticle: false }); aladin.createImageSurvey('illenoroC', 'illenoroC', hipsDir, 'equatorial', 4, {imgFormat: 'jpg', longitudeReversed: false}); aladin.createImageSurvey('Coronelli', 'Coronelli', hipsDir, 'equatorial', 4, {imgFormat: 'jpg', longitudeReversed: true}); aladin.setImageSurvey('Coronelli'); diff --git a/examples/al-footprints.html b/examples/al-footprints.html index 7a88cdc3d..0a3790509 100644 --- a/examples/al-footprints.html +++ b/examples/al-footprints.html @@ -34,7 +34,7 @@ console.log("Object hovered stopped: ", object, "mouse coords xy: ", xyMouseCoords.x, xyMouseCoords.y); }) - const cat = A.catalogFromVizieR('B/assocdata/obscore', 'M 1', 100, {onClick: 'showTable', hoverColor: 'purple', limit: 1000}); + const cat = A.catalogFromVizieR('B/assocdata/obscore', 'M 1', 10, {onClick: 'showTable', hoverColor: 'purple', limit: 10000}); aladin.addCatalog(cat); }); diff --git a/examples/al-save-colormap.html b/examples/al-save-colormap.html new file mode 100644 index 000000000..6451864fc --- /dev/null +++ b/examples/al-save-colormap.html @@ -0,0 +1,32 @@ + + + + + + + +
+ + + + + diff --git a/src/css/aladin.css b/src/css/aladin.css index f702ee55f..7195f1aec 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -429,7 +429,7 @@ canvas { } .aladin-input-text.aladin-dark-theme.search { - width: 14rem; + width: 15rem; text-shadow: 0px 0px 2px #000; } @@ -824,6 +824,7 @@ canvas { border-radius: 5px; font-family: monospace; font-size: 1rem; + box-sizing: border-box; } .aladin-input-text.aladin-dark-theme, .aladin-input-number.aladin-dark-theme { @@ -853,18 +854,20 @@ canvas { /* Remove focus outline */ /* Remove IE arrow */ } + .aladin-input-select option { color: inherit; background-color: #320a28; } + .aladin-input-select:focus { outline: none; } + .aladin-input-select::-ms-expand { display: none; } - /* Frames */ .aladin-input-color { appearance: none; @@ -1104,6 +1107,19 @@ canvas { height: 1.7rem; } +.aladin-input-text.aladin-dark-theme.search.aladin-HiPS-search { + width: 100%; + +} + +.aladin-stack-box { + width: 17rem; +} + +.aladin-stack-box .content > * { + margin-bottom: 0.5rem; +} + .aladin-location { position: absolute; top: 0.2rem; @@ -1122,6 +1138,8 @@ canvas { bottom: 0.2rem; left: 0.2rem; + background-color: red; + font-family: monospace; font-size: 1rem; @@ -1129,6 +1147,16 @@ canvas { line-height: 1.7rem; } +.aladin-fov .aladin-zoom-in { + margin-right: 0; + border-right: none; + border-radius: 5px 0px 0px 5px; +} + +.aladin-fov .aladin-zoom-out { + border-radius: 0px 5px 5px 0px; +} + .aladin-status-bar { border-radius: 3px; padding: 0.4rem; diff --git a/src/js/Aladin.js b/src/js/Aladin.js index 721660e88..453de9f4c 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -40,6 +40,7 @@ import { MeasurementTable } from "./MeasurementTable.js"; import { ImageSurvey } from "./ImageSurvey.js"; import { Coo } from "./libs/astro/coo.js"; import { CooConversion } from "./CooConversion.js"; +import { MocServer } from './MocServer'; import { ProjectionEnum } from "./ProjectionEnum.js"; @@ -77,6 +78,10 @@ import { CooFrame } from './gui/Input/CooFrame'; * @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 {Object[]|string[]} [hipsList] A list of predefined HiPS for the Aladin instance. + * This option is used for searching for a HiPS in a list of surveys + * This list can have string item (either a CDS ID or an HiPS url) or an object that describes the HiPS + * more exhaustively. See the example below to see the different form that this item can have to describe a HiPS. * @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. @@ -86,6 +91,8 @@ import { CooFrame } from './gui/Input/CooFrame'; * 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} [expandLayersControl=false] - Whether to show the stack box at starting + * CSS class for the stack box is `aladin-stack-box` * @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. @@ -133,7 +140,53 @@ import { CooFrame } from './gui/Input/CooFrame'; * @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. - */ + * @example + * let aladin = A.aladin({ + target: 'galactic center', + fov: 10, + hipsList: [ + // url + "https://alaskybis.unistra.fr/DSS/DSSColor", + // ID from HiPS list + "CDS/P/2MASS/color", + // Not full HiPS described + { + name: 'DESI Legacy Surveys color (g, r, i, z)', + id: 'CDS/P/DESI-Legacy-Surveys/DR10/color', + }, + // Fully described HiPS + { + name: "DECaPS DR2 color", + url: "https://alasky.cds.unistra.fr/DECaPS/DR2/CDS_P_DECaPS_DR2_color/", + properties: { + creatorDid: "ivo://CDS/P/DECaPS/DR2/color", + maxOrder: 11, + cooFrame: "equatorial", + tileSize: 512, + imgFormat: 'png', + }, + }, + // HiPS with options + { + name: "SDSS9 band-g", + id: "P/SDSS9/g", + properties: { + creatorDid: "ivo://CDS/P/SDSS9/g", + maxOrder: 10, + tileSize: 512, + numBitsPerPixel: 16, + imgFormat: 'fits', + cooFrame: 'equatorial', + }, + options: { + minCut: 0, + maxCut: 1.8, + stretch: 'linear', + colormap: "redtemperature", + } + } + ] +})*/ /** * @typedef {Object} CircleSelection @@ -335,6 +388,84 @@ export let Aladin = (function () { this.setBaseImageLayer(url); } + let hipsList = [].concat(options.hipsList); + + const fillHiPSCache = () => { + for (var survey of hipsList) { + let id, url, name; + let cachedSurvey = {}; + + if (typeof survey === "string") { + try { + url = new URL(survey).href; + } catch(e) { + id = survey; + } + + name = url || id; + } else if (survey instanceof Object) { + if (survey.id) { + id = survey.id; + } + if (survey.url) { + url = survey.url; + } + + name = survey.name || survey.id || survey.url; + + if (id && url) { + console.warn('Both "CDS ID" and url are given for ', survey, '. ID is chosen.') + url = null; + } + + if (survey.properties) { + cachedSurvey = {...cachedSurvey, ...survey.properties} + } + if (survey.options) { + cachedSurvey = {...cachedSurvey, ...survey.options} + } + } else { + console.warn('unable to parse the survey list item: ', survey) + continue; + } + + if (id) { + cachedSurvey['id'] = id; + } + if (url) { + cachedSurvey['url'] = url; + } + if (name) { + cachedSurvey['name'] = name; + } + + // at least id or url is defined + let key = id || url; + + if (!(key in ImageSurvey.cache)) { + ImageSurvey.cache[key] = cachedSurvey + } + } + + ALEvent.HIPS_LIST_UPDATED.dispatchedTo(this.aladinDiv); + } + + if (hipsList.length === 0) { + MocServer.getAllHiPSes() + .then((HiPSes) => { + HiPSes.forEach((h) => { + hipsList.push({ + id: h.ID, + name: h.obs_title + }) + }); + + fillHiPSCache(); + }); + } else { + fillHiPSCache(); + } + this.view.showCatalog(options.showCatalog); // FullScreen toolbar icon @@ -412,6 +543,7 @@ export let Aladin = (function () { if (!options.showLayersControl) { stack._hide(); } + // Add the simbad pointer control if (!options.showSimbadPointerControl) { simbad._hide(); @@ -443,6 +575,10 @@ export let Aladin = (function () { this.addUI(new FullScreenActionButton(self)) } + if (options.expandLayersControl) { + stack.toggle(); + } + this._applyMediaQueriesUI(); } @@ -494,6 +630,8 @@ export let Aladin = (function () { Aladin.wasmLibs = {}; Aladin.DEFAULT_OPTIONS = { survey: ImageSurvey.DEFAULT_SURVEY_ID, + // surveys suggestion list + hipsList: [], //surveyUrl: ["https://alaskybis.unistra.fr/DSS/DSSColor", "https://alasky.unistra.fr/DSS/DSSColor"], target: "0 +0", cooFrame: "J2000", @@ -504,6 +642,7 @@ export let Aladin = (function () { showZoomControl: false, // Menu toolbar showLayersControl: true, + expandLayersControl: false, showFullscreenControl: true, showSimbadPointerControl: false, showCooGridControl: false, @@ -539,7 +678,7 @@ export let Aladin = (function () { log: true, samp: false, realFullscreen: false, - pixelateCanvas: true + pixelateCanvas: true, }; // realFullscreen: AL div expands not only to the size of its parent, but takes the whole available screen estate @@ -851,10 +990,11 @@ export let Aladin = (function () { // 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()); @@ -914,14 +1054,14 @@ export let Aladin = (function () { * @memberof Aladin * @param {number} lon - longitude in degrees * @param {number} lat - latitude in degrees - * @param {string} frame - Optional callback options. + * @param {string} [frame] - The name of the coordinate frame. Possible values: 'j2000d', 'j2000', 'gal', 'icrs'. The given string is case insensitive. * * @example * // Move to position * const aladin = A.aladin('#aladin-lite-div'); * aladin.gotoPosition(20, 10, "galactic"); */ - Aladin.prototype.gotoPosition = function (lon, lat, frame = undefined) { + Aladin.prototype.gotoPosition = function (lon, lat, frame) { var radec; // convert the frame from string to CooFrameEnum if (frame) { @@ -936,7 +1076,7 @@ export let Aladin = (function () { radec = [lon, lat]; } - this.view.pointTo(radec[0], radec[1]); + this.gotoRaDec(radec[0], radec[1]); }; var idTimeoutAnim; @@ -1140,9 +1280,15 @@ export let Aladin = (function () { }; /** - * point to a given position, expressed as a ra,dec coordinate + * Moves the Aladin instance to the specified position given in ICRS frame * - * @API + * @memberof Aladin + * @param {number} ra - Right-ascension in degrees + * @param {number} dec - Declination in degrees + * + * @example + * const aladin = A.aladin('#aladin-lite-div'); + * aladin.gotoRaDec(20, 10); */ Aladin.prototype.gotoRaDec = function (ra, dec) { this.view.pointTo(ra, dec); @@ -1239,7 +1385,15 @@ export let Aladin = (function () { let surveyOptions = ImageSurvey.cache[id]; if (!surveyOptions) { - surveyOptions = {url, name, maxOrder, cooFrame, ...options}; + surveyOptions = {name, maxOrder, cooFrame, ...options}; + + // differenciate url from CDS Id in the url param given + if (!Utils.isUrl(url)) { + surveyOptions.id = url; + } else { + surveyOptions.url = url; + } + ImageSurvey.cache[id] = surveyOptions; } diff --git a/src/js/HiPSProperties.js b/src/js/HiPSProperties.js index 78a1cf988..5e9ec1925 100644 --- a/src/js/HiPSProperties.js +++ b/src/js/HiPSProperties.js @@ -214,14 +214,15 @@ HiPSProperties.getFasterMirrorUrl = function (metadata, currUrl) { newUrlResp = validResponses[0]; } else { // no valid response => we return an error - return Promise.reject('Survey not found. All mirrors urls have been tested:' + urls) + return Promise.reject('All mirrors urls have been tested:' + urls) } // check if there is a big difference from the current one let currUrlResp = validResponses.find((r) => r.baseUrl === currUrl) - + // it may happen that the url requested by the user is too slow hence discarded + // for these cases, we automatically switch to the new fastest url. let urlChosen; - if (Math.abs(currUrlResp.duration - newUrlResp.duration) / Math.min(currUrlResp.duration, newUrlResp.duration) < 0.10) { + if (currUrlResp && Math.abs(currUrlResp.duration - newUrlResp.duration) / Math.min(currUrlResp.duration, newUrlResp.duration) < 0.10) { // there is not enough difference => do not switch urlChosen = currUrlResp.baseUrl; } else { diff --git a/src/js/ImageSurvey.js b/src/js/ImageSurvey.js index b927d5f18..cab089a9a 100644 --- a/src/js/ImageSurvey.js +++ b/src/js/ImageSurvey.js @@ -144,8 +144,7 @@ export let ImageSurvey = (function () { * @class * @constructs ImageSurvey * - * @param {string} id - Mandatory unique identifier for the layer. - * Can be an arbitrary name + * @param {string} id - Mandatory unique identifier for the layer. Can be an arbitrary name * @param {string} url - Can be an url to the survey or a "CDS" ID pointing towards a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here} * @param {ImageSurveyOptions} [options] - The option for the survey * @@ -193,32 +192,29 @@ export let ImageSurvey = (function () { self.query = (async () => { if (isMOCServerToBeQueried) { - let properties; let isCDSId = false; - try { - properties = await HiPSProperties.fetchFromUrl(self.url) - /*.catch((e) => { - // try with the proxy - url = Utils.handleCORSNotSameOrigin(url).href; - - return HiPSProperties.fetchFromUrl(url); - })*/ - .catch(async (e) => { - // url not valid so we try with the id - try { - isCDSId = true; - // the url stores a "CDS ID" we take it prioritaly - // if the url is null, take the id, this is for some tests - // to pass because some users might just give null as url param and a "CDS ID" as id param - let id = self.url || self.id; - return await HiPSProperties.fetchFromID(id); - } catch(e) { - throw e; - } - }) - } catch(e) { - throw e; - } + + let properties = await HiPSProperties.fetchFromUrl(self.url) + /*.catch((e) => { + // try with the proxy + url = Utils.handleCORSNotSameOrigin(url).href; + + return HiPSProperties.fetchFromUrl(url); + })*/ + .catch(async (e) => { + // url not valid so we try with the id + try { + isCDSId = true; + // the url stores a "CDS ID" we take it prioritaly + // if the url is null, take the id, this is for some tests + // to pass because some users might just give null as url param and a "CDS ID" as id param + let id = self.url || self.id; + return await HiPSProperties.fetchFromID(id); + } catch(e) { + throw e; + } + }) + //obsTitle = properties.obs_title; self.creatorDid = properties.creator_did || self.creatorDid; @@ -238,24 +234,25 @@ export let ImageSurvey = (function () { if (self.url !== url) { console.info("Change url of ", self.id, " from ", self.url, " to ", url) + self.url = url; + + // save the new url to the cache + self._saveInCache(); + // If added to the backend, then we need to tell it the url has changed if (self.added) { self.view.wasm.setHiPSUrl(self.creatorDid, url); } - - self.url = url; - - // save the new url to the cache - ImageSurvey.cache[self.id].url = self.url; } }) - /*.catch(e => { + .catch(e => { //alert(e); + console.error(self) console.error(e); // the survey has been added so we remove it from the stack - self.view.removeImageLayer(self.layer) + //self.view.removeImageLayer(self.layer) //throw e; - })*/ + }) } // Max order @@ -402,7 +399,13 @@ export let ImageSurvey = (function () { // append new important infos from the properties queried ...surveyOpt, } - } + + //console.log('new CACHE', ImageSurvey.cache, self.id, surveyOpt, ImageSurvey.cache[self.id], ImageSurvey.cache["CSIRO/P/RACS/mid/I"]) + + // Tell that the HiPS List has been updated + ALEvent.HIPS_LIST_UPDATED.dispatchedTo(this.view.aladin.aladinDiv); + + } /** * Checks if the ImageSurvey represents a planetary body. @@ -704,7 +707,7 @@ export let ImageSurvey = (function () { }, }); - this.added = true; + //this.added = true; return Promise.resolve(this); } @@ -748,7 +751,7 @@ export let ImageSurvey = (function () { // A cache storing directly surveys important information to not query for the properties each time ImageSurvey.cache = { - DSS2_color: { + /*DSS2_color: { creatorDid: "ivo://CDS/P/DSS2/color", name: "DSS colored", url: "https://alasky.cds.unistra.fr/DSS/DSSColor", @@ -913,6 +916,7 @@ export let ImageSurvey = (function () { stretch: 'linear', colormap: "redtemperature", } + */ /* { id: "P/Finkbeiner", diff --git a/src/js/MOC.js b/src/js/MOC.js index 8df6f2e38..b56518a02 100644 --- a/src/js/MOC.js +++ b/src/js/MOC.js @@ -110,7 +110,7 @@ export let MOC = (function() { * set MOC data by parsing a MOC serialized in JSON * (as defined in IVOA MOC document, section 3.1.1) */ - MOC.prototype.parse = function(data, successCallback) { + MOC.prototype.parse = function(data, successCallback, errorCallback) { if (typeof data === 'string' || data instanceof String) { let url = data; this.promiseFetchData = fetch(url) @@ -120,7 +120,7 @@ export let MOC = (function() { } this.successCallback = successCallback; - this.errorCallback = this.errorCallback; + this.errorCallback = errorCallback; }; MOC.prototype.setView = function(view) { @@ -166,7 +166,11 @@ export let MOC = (function() { self.view.requestRedraw(); }) - .catch(e => alert('MOC load error:' + e)) + .catch(e => { + console.error('MOC load error:' + e) + if (self.errorCallback) + self.errorCallback(self); + }) }; MOC.prototype.reportChange = function() { diff --git a/src/js/Utils.ts b/src/js/Utils.ts index 239ee29a5..250f177f5 100644 --- a/src/js/Utils.ts +++ b/src/js/Utils.ts @@ -469,6 +469,14 @@ Utils.fixURLForHTTPS = function (url) { return url } +Utils.isUrl = function(url) { + try { + return new URL(url).href; + } catch(e) { + return undefined; + } +} + // generate an absolute URL from a relative URL // example: getAbsoluteURL('foo/bar/toto') return http://cds.unistra.fr/AL/foo/bar/toto if executed from page http://cds.unistra.fr/AL/ Utils.getAbsoluteURL = function (url) { diff --git a/src/js/View.js b/src/js/View.js index ae579263e..45289c55b 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -488,8 +488,10 @@ export let View = (function () { View.prototype.selectLayer = function (layer) { if (!this.imageLayers.has(layer)) { - throw layer + ' does not exists. So cannot be selected'; + console.warn(layer + ' does not exists. So cannot be selected'); + return; } + this.selectedLayer = layer; }; @@ -1167,7 +1169,7 @@ export let View = (function () { } //requestAnimFrame(moveTo) }*/ - }, 30); + }, 40); } view.throttledTouchPadZoom(); @@ -1578,10 +1580,20 @@ export let View = (function () { if (idxOverlayLayer == -1) { // it does not exist so we add it to the stack this.overlayLayers.push(layerName); + } else { + // it exists + let alreadyPresentImageLayer = this.imageLayers.get(layerName); + alreadyPresentImageLayer.added = false; } + imageLayer.added = true; this.imageLayers.set(layerName, imageLayer); + // select the layer if he is on top + if (idxOverlayLayer == -1) { + this.selectLayer(layerName); + } + ALEvent.HIPS_LAYER_ADDED.dispatchedTo(this.aladinDiv, { layer: imageLayer }); } @@ -1678,11 +1690,6 @@ export let View = (function () { this.imageLayers.delete(layer); this.imageLayers.set(newLayer, imageLayer); - // Change the selected layer if this is the one renamed - /*if (this.selectedLayer === layer) { - this.selectedLayer = newLayer; - }*/ - // Tell the layer hierarchy has changed ALEvent.HIPS_LAYER_RENAMED.dispatchedTo(this.aladinDiv, { layer, newLayer }); } @@ -1734,7 +1741,7 @@ export let View = (function () { this.empty = true; } else if (this.selectedLayer === layer) { // If the layer removed was selected then we select the base layer - this.selectedLayer = 'base'; + this.selectLayer(this.overlayLayers[this.overlayLayers.length - 1]); } ALEvent.HIPS_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer }); @@ -1857,11 +1864,11 @@ export let View = (function () { ra = parseFloat(ra); dec = parseFloat(dec); - if (isNaN(ra) || isNaN(dec)) { + if (!ra || !dec) { return; } this.viewCenter.lon = ra; - this.viewCenter.lat = dec; + this.viewCenter.lat = dec; //this.updateLocation({lon: this.viewCenter.lon, lat: this.viewCenter.lat}); // Put a javascript code here to do some animation @@ -1905,7 +1912,13 @@ export let View = (function () { this.catalogs = []; this.overlays = []; this.mocs = []; + + this.allOverlayLayers.forEach((overlay) => { + ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: overlay }); + }) this.allOverlayLayers = []; + + this.mustClearCatalog = true; this.requestRedraw(); }; @@ -1929,7 +1942,7 @@ export let View = (function () { this.overlays.splice(indexToDelete, 1); } - ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: layer }); + ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer }); this.mustClearCatalog = true; this.requestRedraw(); @@ -2000,7 +2013,7 @@ export let View = (function () { let closest = null; footprints.forEach((footprint) => { - if (!footprint.source.tooSmallFootprint) { + if (!footprint.source || !footprint.source.tooSmallFootprint) { // Hidden footprints are not considered let lineWidth = footprint.getLineWidth(); diff --git a/src/js/events/ALEvent.js b/src/js/events/ALEvent.js index d38713eb8..bc2471bd4 100644 --- a/src/js/events/ALEvent.js +++ b/src/js/events/ALEvent.js @@ -53,6 +53,8 @@ export class ALEvent { static HIPS_LAYER_RENAMED = new ALEvent("AL:HiPSLayer.renamed"); static HIPS_LAYER_SWAP = new ALEvent("AL:HiPSLayer.swap"); + static HIPS_LIST_UPDATED = new ALEvent("AL:HiPSList.updated"); + static HIPS_LAYER_CHANGED = new ALEvent("AL:HiPSLayer.changed"); static GRAPHIC_OVERLAY_LAYER_ADDED = new ALEvent("AL:GraphicOverlayLayer.added"); diff --git a/src/js/gui/Box/CatalogQueryBox.js b/src/js/gui/Box/CatalogQueryBox.js index 7282aa2a0..0f51db302 100644 --- a/src/js/gui/Box/CatalogQueryBox.js +++ b/src/js/gui/Box/CatalogQueryBox.js @@ -131,6 +131,7 @@ import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; }, aladin) super({ + close: false, content: Layout.horizontal({ layout: [inputText, loadBtn] }), @@ -238,10 +239,10 @@ import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; this.loadBtn.update({disable: true}, aladin) } else { let self = this; - let layout = []; + let ctxMenu = []; if (item && item.cs_service_url) { - layout.push({ + ctxMenu.push({ label: 'Cone search', disable: !item.cs_service_url, action(o) { @@ -263,6 +264,8 @@ import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; }) self._hide(); + + self.callback && self.callback(); }, position: { anchor: 'center center', @@ -270,13 +273,12 @@ import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; }) self.box._show(); self.loadBtn.hideMenu() - } }) } if (item && item.hips_service_url) { - layout.push({ + ctxMenu.push({ label: 'HiPS catalogue', disable: !item.hips_service_url, action(o) { @@ -286,15 +288,22 @@ import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; }) self._hide(); + + self.callback && self.callback(); } }) } - this.loadBtn.update({ctxMenu: layout, disable: false}, aladin) + this.loadBtn.update({ctxMenu, disable: false}, aladin) } this.loadBtn.hideMenu() } + attach(options) { + this.callback = options.callback; + super.update(options) + } + _hide() { if (this.box) { this.box.remove(); diff --git a/src/js/gui/Box/HiPSSelectorBox.js b/src/js/gui/Box/HiPSSelectorBox.js index 7b8a2eb54..3be958dc9 100644 --- a/src/js/gui/Box/HiPSSelectorBox.js +++ b/src/js/gui/Box/HiPSSelectorBox.js @@ -79,6 +79,7 @@ import { Input } from "../Widgets/Input.js"; super( { + close: false, content: Layout.horizontal({ layout: [ inputText, diff --git a/src/js/gui/Box/SurveyEditBox.js b/src/js/gui/Box/HiPSSettingsBox.js similarity index 91% rename from src/js/gui/Box/SurveyEditBox.js rename to src/js/gui/Box/HiPSSettingsBox.js index 5dafbc13f..7df1b0daa 100644 --- a/src/js/gui/Box/SurveyEditBox.js +++ b/src/js/gui/Box/HiPSSettingsBox.js @@ -40,15 +40,15 @@ import { ColorCfg } from "../../ColorCfg.js"; import { Layout } from "../Layout.js"; import { Input } from "../Widgets/Input.js"; - export class LayerEditBox extends Box { + export class HiPSSettingsBox extends Box { // Constructor constructor(aladin, options) { - super( - { + super({ cssStyle: { padding: '4px', backgroundColor: 'black', }, + close: false, ...options }, aladin.aladinDiv @@ -162,21 +162,19 @@ import { ColorCfg } from "../../ColorCfg.js"; let layerOpacity = layer.getOpacity() - self.opacitySettingsContent = Layout.horizontal([ - Input.slider({ - tooltip: {content: layerOpacity, position: {direction: 'bottom'}}, - name: 'opacitySlider', - type: 'range', - min: 0.0, - max: 1.0, - value: layerOpacity, - change(e, slider) { - const opacity = +e.target.value; - layer.setOpacity(opacity) - slider.update({value: opacity, tooltip: {content: opacity.toFixed(2), position: {direction: 'bottom'}}}) - } - }), - ]); + self.opacitySettingsContent = Input.slider({ + tooltip: {content: layerOpacity, position: {direction: 'bottom'}}, + name: 'opacitySlider', + type: 'range', + min: 0.0, + max: 1.0, + value: layerOpacity, + change(e, slider) { + const opacity = +e.target.value; + layer.setOpacity(opacity) + slider.update({value: opacity, tooltip: {content: opacity.toFixed(2), position: {direction: 'bottom'}}}) + } + }) let brightness = layer.getColorCfg().getBrightness() let saturation = layer.getColorCfg().getSaturation() @@ -311,10 +309,10 @@ import { ColorCfg } from "../../ColorCfg.js"; _addListeners() { ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, (e) => { - const layerChanged = e.detail.layer; + const hips = e.detail.layer; let selectedLayer = this.options.layer; - if (selectedLayer && layerChanged.layer === selectedLayer.layer) { - let colorCfg = layerChanged.getColorCfg(); + if (selectedLayer && hips.layer === selectedLayer.layer) { + let colorCfg = hips.getColorCfg(); let cmap = colorCfg.getColormap(); let reversed = colorCfg.getReversed(); @@ -323,7 +321,9 @@ import { ColorCfg } from "../../ColorCfg.js"; let [minCut, maxCut] = colorCfg.getCuts(); this.minCutInput.set(+minCut.toFixed(2)); this.maxCutInput.set(+maxCut.toFixed(2)); - this.stretchSelector.update({value: stretch}) + this.stretchSelector.update({value: stretch}); + + this.opacitySettingsContent.set(hips.getOpacity()) } }); } diff --git a/src/js/gui/Box/ShortLivedBox.js b/src/js/gui/Box/ShortLivedBox.js index 27785f032..01aa43c9c 100644 --- a/src/js/gui/Box/ShortLivedBox.js +++ b/src/js/gui/Box/ShortLivedBox.js @@ -35,6 +35,7 @@ export class ShortLivedBox extends Box { constructor(aladin) { super( { + close: false, cssStyle: { color: 'white', backgroundColor: 'black', diff --git a/src/js/gui/Box/StackBox.js b/src/js/gui/Box/StackBox.js new file mode 100644 index 000000000..ef45efce8 --- /dev/null +++ b/src/js/gui/Box/StackBox.js @@ -0,0 +1,874 @@ +// 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 gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ +import { CatalogQueryBox } from "./CatalogQueryBox.js"; + import { ALEvent } from "../../events/ALEvent.js"; + import { Layout } from "../Layout.js"; + import { ContextMenu } from "../Widgets/ContextMenu.js"; + import { ActionButton } from "../Widgets/ActionButton.js"; +import A from "../../A.js"; +import { Utils } from "../../Utils"; +import { View } from "../../View.js"; +import { HiPSSettingsBox } from "./HiPSSettingsBox.js"; +import searchIconUrl from '../../../../assets/icons/search.svg'; +import showIconUrl from '../../../../assets/icons/show.svg'; +import addIconUrl from '../../../../assets/icons/plus.svg'; +import hideIconUrl from '../../../../assets/icons/hide.svg'; +import removeIconUrl from '../../../../assets/icons/remove.svg'; +import settingsIconUrl from '../../../../assets/icons/settings.svg'; +import { ImageFITS } from "../../ImageFITS.js"; +import searchIconImg from '../../../../assets/icons/search.svg'; +import { TogglerActionButton } from "../Button/Toggler.js"; +import { Icon } from "../Widgets/Icon.js"; +import { ImageSurvey } from "../../ImageSurvey.js"; +import { Box } from "../Widgets/Box.js"; +import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; +import { HiPSSearch } from "../Input/HiPSSearch.js"; + +export class OverlayStackBox extends Box { + /*static previewImagesUrl = { + 'AllWISE color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_allWISE_color.jpg', + 'DSS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_DSS2_color.jpg', + 'DSS2 Red (F+R)': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_DSS2_red.jpg', + 'Fermi color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Fermi_color.jpg', + 'GALEXGR6_7 NUV': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_GALEXGR6_7_color.jpg', + 'GLIMPSE360': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_GLIMPSE360.jpg', + 'Halpha': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_VTSS_Ha.jpg', + 'IRAC color I1,I2,I4 - (GLIMPSE, SAGE, SAGE-SMC, SINGS)': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SPITZER_color.jpg', + 'IRIS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_IRIS_color.jpg', + 'Mellinger colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Mellinger_color.jpg', + 'PanSTARRS DR1 color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_PanSTARRS_DR1_color-z-zg-g.jpg', + '2MASS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_2MASS_color.jpg', + 'AKARI colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_AKARI_FIS_Color.jpg', + 'SWIFT': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SWIFT_BAT_FLUX.jpg', + 'VTSS-Ha': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Finkbeiner.jpg', + 'XMM PN colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_XMM_PN_color.jpg', + 'SDSS9 colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SDSS9_color.jpg', + };*/ + static predefinedCats = { + simbad: {url: 'https://axel.u-strasbg.fr/HiPSCatService/SIMBAD', options: {id: 'simbad', name: 'SIMBAD', shape: 'circle', sourceSize: 8, color: '#318d80', onClick: 'showTable'}}, + gaia: {url: 'https://axel.u-strasbg.fr/HiPSCatService/I/355/gaiadr3', options: {id: 'gaia-dr3', name: 'Gaia DR3', shape: 'square', sourceSize: 8, color: '#6baed6', onClick: 'showTable'}}, + twomass: {url: 'https://axel.u-strasbg.fr/HiPSCatService/II/246/out', options: {id: '2mass', name: '2MASS', shape: 'plus', sourceSize: 8, color: '#dd2233', onClick: 'showTable'}} + }; + // Constructor + constructor(aladin) { + super({ + close: false, + header: { + title: 'Stack', + }, + classList: ['aladin-stack-box'], + content: [] + }, + aladin.aladinDiv); + this.aladin = aladin; + + this.mode = 'stack'; + + this._addListeners(); + + this.mocHiPSUrls = {} + this.HiPSui = {} + let self = this; + // Add overlay button + this.addOverlayBtn = new CtxMenuActionButtonOpener({ + icon: { + url: addIconUrl, + size: 'small', + monochrome: true, + }, + tooltip: {content: 'A catalog, MOC or footprint', position: { direction: 'top' }}, + ctxMenu: [ + { + label: 'Catalogue', + subMenu: [ + { + label: { + icon: { + url: 'https://aladin.cds.unistra.fr/AladinLite/logos/SIMBAD.svg', + cssStyle: { + width: '3rem', + height: '3rem', + cursor: 'help', + }, + action(o) { + window.open('https://simbad.cds.unistra.fr/simbad/') + } + }, + content: 'database', + tooltip: {content: 'Click to go to the SIMBAD database', position: {direction: 'bottom'}}, + }, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + //self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStackBox.predefinedCats.simbad.url, OverlayStackBox.predefinedCats.simbad.options); + self.aladin.addCatalog(simbadHiPS); + } + }, + { + label: 'Gaia DR3', + action(o) { + o.stopPropagation(); + o.preventDefault(); + + //self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStackBox.predefinedCats.gaia.url, OverlayStackBox.predefinedCats.gaia.options); + self.aladin.addCatalog(simbadHiPS); + } + }, + { + label: '2MASS', + action(o) { + o.stopPropagation(); + o.preventDefault(); + + //self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStackBox.predefinedCats.twomass.url, OverlayStackBox.predefinedCats.twomass.options); + self.aladin.addCatalog(simbadHiPS); + } + }, + ContextMenu.fileLoaderItem({ + label: 'From a VOTable File', + accept: '.xml,.vot', + action(file) { + let url = URL.createObjectURL(file); + + A.catalogFromURL( + url, + {onClick: 'showTable'}, + (catalog) => { + self.aladin.addCatalog(catalog) + }, + e => alert(e) + ); + } + }), + { + label: { + icon: { + url: searchIconImg, + monochrome: true, + tooltip: {content: 'Find a specific catalogue
in our database...', position: { direction: 'top' }}, + cssStyle: { + cursor: 'help', + }, + }, + content: 'More...' + }, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + if (!self.catBox) { + self.catBox = new CatalogQueryBox(self.aladin); + self.catBox.attach({callback: () => { + self._show(); + }}); + } + + self.catBox._show({position: self.position}); + } + }, + ] + }, + { + label: { + icon: { + url: Icon.dataURLFromSVG({svg: Icon.SVG_ICONS.MOC}), + size: 'small', + tooltip: {content: 'Define a selection coverage', position: {direction: 'bottom'}}, + monochrome: true, + cssStyle: { + cursor: 'pointer', + }, + }, + content: 'MOC' + }, + subMenu: [ + ContextMenu.fileLoaderItem({ + label: 'FITS File', + accept: '.fits', + action(file) { + let url = URL.createObjectURL(file); + + let moc = A.MOCFromURL( + url, + {name: file.name, lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } + }), + { + label: 'From selection', + subMenu: [ + { + label: '◌ Circle', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.preventDefault(); + o.stopPropagation(); + + //self._hide(); + + self.aladin.select('circle', c => { + try { + let [ra, dec] = self.aladin.pix2world(c.x, c.y, 'j2000'); + let radius = self.aladin.angularDist(c.x, c.y, c.x + c.r, c.y); + + // the moc needs a + let moc = A.MOCFromCone( + {ra, dec, radius}, + {name: 'cone', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } catch { + console.error('Circle out of projection. Selection canceled') + } + }) + } + }, + { + label: '⬚ Rect', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + //self._hide(); + + self.aladin.select('rect', r => { + try { + let [ra1, dec1] = self.aladin.pix2world(r.x, r.y, 'j2000'); + let [ra2, dec2] = self.aladin.pix2world(r.x + r.w, r.y, 'j2000'); + let [ra3, dec3] = self.aladin.pix2world(r.x + r.w, r.y + r.h, 'j2000'); + let [ra4, dec4] = self.aladin.pix2world(r.x, r.y + r.h, 'j2000'); + + let moc = A.MOCFromPolygon( + { + ra: [ra1, ra2, ra3, ra4], + dec: [dec1, dec2, dec3, dec4] + }, + {name: 'rect', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } catch(_) { + alert('Selection covers a region out of the projection definition domain.'); + } + }) + } + }, + { + label: '⛉ Polygon', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + //self._hide(); + + self.aladin.select('poly', p => { + try { + let ra = [] + let dec = [] + for (const v of p.vertices) { + let [lon, lat] = self.aladin.pix2world(v.x, v.y, 'j2000'); + ra.push(lon) + dec.push(lat) + } + + let moc = A.MOCFromPolygon( + {ra, dec}, + {name: 'poly', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + + } catch(_) { + alert('Selection covers a region out of the projection definition domain.'); + } + }) + } + }, + ] + } + ] + } + ], + }, this.aladin) + + this.addHiPSBtn = new CtxMenuActionButtonOpener({ + icon: { + url: addIconUrl, + size: 'small', + monochrome: true, + }, + ctxMenu: [ + { + label: { + icon: { + url: searchIconUrl, + monochrome: true, + tooltip: {content: 'From our database...', position: { direction: 'right' }}, + cssStyle: { + cursor: 'help', + }, + }, + content: 'Add new survey' + }, + action: (e) => { + e.stopPropagation(); + e.preventDefault(); + + /*self._hide(); + + self.hipsSelectorBox = new HiPSSelectorBox(self.aladin); + // attach a callback + self.hipsSelectorBox.attach( + (HiPSId) => { + let name = Utils.uuidv4() + self.aladin.setOverlayImageLayer(HiPSId, name) + + self.show(); + } + ); + + self.hipsSelectorBox._show({ + position: self.position, + });*/ + self.aladin.addNewImageLayer() + } + }, + ContextMenu.fileLoaderItem({ + label: 'FITS image file', + accept: '.fits', + action(file) { + let url = URL.createObjectURL(file); + + const image = self.aladin.createImageFITS( + url, + file.name, + undefined, + (ra, dec, fov, _) => { + // Center the view around the new fits object + self.aladin.gotoRaDec(ra, dec); + self.aladin.setFoV(fov * 1.1); + //self.aladin.selectLayer(image.layer); + }, + undefined + ); + + self.aladin.setOverlayImageLayer(image, Utils.uuidv4()) + } + }), + ], + tooltip: { content: 'Add a HiPS or an FITS image', position: {direction: 'top'} }, + }, this.aladin); + + this.update({content: this.createLayout()}); + } + + _addListeners() { + let self = this; + + let updateOverlayList = () => { + let wasHidden = self.isHidden; + self._hide(); + // recompute the ui + // If it is shown, update it + // show will update the content of the stack + self.update({content: self.createLayout()}); + + if (!wasHidden) + self._show(); + }; + + ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_ADDED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_RENAMED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_SWAP.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_REMOVED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, function (e) { + const hips = e.detail.layer; + let ui = self.HiPSui[hips.layer]; + + // change the ui from parameter changes + // show button + const opacity = hips.getOpacity(); + if (opacity !== 0.0) { + ui.showBtn.update({icon: {monochrome: true, url: showIconUrl}, tooltip: {content: 'Hide'}}); + } else { + ui.showBtn.update({icon: {monochrome: true, url: hideIconUrl}, tooltip: {content: 'Show'}}); + } + }); + + updateOverlayList(); + + // Add a listener for HiPS list changes + ALEvent.HIPS_LIST_UPDATED.listenedBy(this.aladin.aladinDiv, () => { + // Recompute the autocompletion as the cache has changed + HiPSSearch.HiPSList = {}; + for (var key in ImageSurvey.cache) { + let HiPS = ImageSurvey.cache[key]; + // search with the name or id + HiPSSearch.HiPSList[HiPS.name] = HiPS; + } + + let keys = Object.keys(HiPSSearch.HiPSList) + // change the autocomplete of all the search input text + for (var key in this.HiPSui) { + let hips = this.HiPSui[key]; + hips.searchInput.setAutocompletionList(keys) + } + }); + } + + _hide() { + for (var key in this.HiPSui) { + let hips = this.HiPSui[key]; + if (hips.settingsBtn.toggled) { + // toggle off + hips.settingsBtn.toggle(); + } + } + + if (this.catBox) { + this.catBox._hide(); + } + + if (this.addOverlayBtn) + this.addOverlayBtn.hideMenu(); + + if (this.addHiPSBtn) + this.addHiPSBtn.hideMenu(); + + super._hide() + } + + createLayout() { + this.HiPSui = {}; + + let layout = [ + Layout.horizontal([this.addOverlayBtn, 'Overlays']) + ]; + + layout = layout.concat(this._createOverlaysList()); + + layout.push(Layout.horizontal({ + layout: [this.addHiPSBtn, 'Surveys'], + })) + layout = layout.concat(this._createSurveysList()); + + return new Layout({layout, classList: ['content']}); + } + + _createOverlaysList() { + let self = this; + + let layout = [] + const overlays = Array.from(this.aladin.getOverlays()).reverse().map((overlay) => { + return overlay; + }); + // list of overlays + for(const overlay of overlays) { + const name = overlay.name; + let showBtn = new ActionButton({ + size: 'small', + icon: { + url: overlay.isShowing ? showIconUrl : hideIconUrl, + monochrome: true, + }, + /*cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + },*/ + tooltip: {content: overlay.isShowing ? 'Hide' : 'Show', position: {direction: 'top'}}, + action(e, btn) { + if (overlay.isShowing) { + overlay.hide() + btn.update({icon: {monochrome: true, url: hideIconUrl}, tooltip: {content: 'Show'}}); + } else { + overlay.show() + btn.update({icon: {monochrome: true, url: showIconUrl}, tooltip: {content: 'Hide'}}); + } + } + }); + + let deleteBtn = new ActionButton({ + icon: { + url: removeIconUrl, + monochrome: true, + }, + size: 'small', + /*cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + },*/ + tooltip: { + content: 'Remove', + position: {direction: 'top'} + }, + action(e) { + self.aladin.removeLayer(overlay) + } + }); + + let item = Layout.horizontal({ + layout: [ + this._addOverlayIcon(overlay), + '
' + name + '
', + Layout.horizontal({layout: [showBtn, deleteBtn]}) + ], + cssStyle: { + textAlign: 'center', + display: 'flex', + alignItems: 'center', + listStyle: 'none', + justifyContent: 'space-between', + width: '100%', + } + }); + + /*if(!Utils.hasTouchScreen()) { + layout.push({ + label: item, + cssStyle, + hover(e) { + showBtn.el.style.visibility = 'visible' + deleteBtn.el.style.visibility = 'visible' + }, + unhover(e) { + showBtn.el.style.visibility = 'hidden' + deleteBtn.el.style.visibility = 'hidden' + }, + }) + } else { + layout.push({ + label: item, + cssStyle + }) + }*/ + layout.push(item) + } + + return layout; + } + + _createSurveysList() { + let self = this; + + const layers = Array.from(self.aladin.getImageOverlays()).reverse().map((name) => { + let overlay = self.aladin.getOverlayImageLayer(name); + return overlay; + }); + // survey list + let selectedLayer = self.aladin.getSelectedLayer(); + + /*if (!layers) { + super.attach(layout); + return; + }*/ + + let layout = []; + const defaultLayers = Object.entries(ImageSurvey.cache).sort(function (e1, e2) { + let a = e1[1] + let b = e2[1] + + if (!a.order) { + return a.name > b.name ? 1 : -1; + } + + return a.maxOrder && a.maxOrder > b.maxOrder ? 1 : -1; + }); + + for(const layer of layers) { + let searchInput = new HiPSSearch(self.aladin, {layer}) + + let deleteBtn = ActionButton.createSmallSizedIconBtn({ + icon: {url: removeIconUrl, monochrome: true}, + + disable: layer.layer === 'base', + tooltip: {content: 'Remove', position: {direction: 'top'}}, + action(e) { + self.aladin.removeImageLayer(layer.layer); + } + }); + + let showBtn = ActionButton.createSmallSizedIconBtn({ + icon: { + url: layer.getOpacity() === 0.0 ? hideIconUrl : showIconUrl, + monochrome: true, + }, + tooltip: {content: layer.getOpacity() === 0.0 ? 'Show' : 'Hide', position: {direction: 'top'}}, + action(e, btn) { + e.preventDefault(); + e.stopPropagation(); + + let opacity = layer.getOpacity(); + if (opacity === 0.0) { + layer.setOpacity(1.0); + btn.update({icon: {monochrome: true, url: showIconUrl}, tooltip: {content: 'Hide'}}); + } else { + layer.setOpacity(0.0); + btn.update({icon: {monochrome: true, url: hideIconUrl}, tooltip: {content: 'Show'}}); + } + } + }); + + let settingsBox = new HiPSSettingsBox(self.aladin); + settingsBox.update({layer}) + settingsBox._hide(); + + let settingsBtn = new TogglerActionButton({ + icon: {url: settingsIconUrl, monochrome: true}, + size: 'small', + tooltip: {content: 'Settings', position: {direction: 'top'}}, + toggled: false, + actionOn: (e) => { + settingsBox._show({position: {nextTo: settingsBtn, direction: 'right', aladin: self.aladin}}); + }, + actionOff: (e) => { + settingsBox._hide(); + }, + }); + + let loadMOCBtn = new ActionButton({ + size: 'small', + + icon: {url: Icon.dataURLFromSVG({svg: Icon.SVG_ICONS.MOC}), monochrome: true}, + tooltip: {content: 'Add coverage', position: {direction: 'top'}}, + toggled: (() => { + let overlays = self.aladin.getOverlays(); + let found = overlays.find((o) => o.type === "moc" && o.name === layer.name); + return found !== undefined; + })(), + action: (e, btn) => { + if (!btn.options.toggled) { + // load the moc + let moc = A.MOCFromURL( + layer.url + '/Moc.fits', + {lineWidth: 3, name: layer.name}, + () => { + self.mocHiPSUrls[layer.url] = moc; + + if (self.aladin.statusBar) { + self.aladin.statusBar.appendMessage({ + message: 'Coverage of ' + layer.name + ' loaded', + duration: 2000, + type: 'info' + }) + } + + btn.update({ + toggled: true, + tooltip: {content: 'Remove coverage', + position: {direction: 'top'}} + }) + } + ); + self.aladin.addMOC(moc) + } else { + // unload the moc + let moc = self.mocHiPSUrls[layer.url]; + self.aladin.removeLayer(moc) + + delete self.mocHiPSUrls[layer.url]; + + if (self.aladin.statusBar) { + self.aladin.statusBar.appendMessage({ + message: 'Coverage of ' + layer.name + ' removed', + duration: 2000, + type: 'info' + }) + } + + btn.update({ + toggled: false, + tooltip: {content: 'Add coverage', position: {direction: 'top'}} + }) + } + }, + }); + + let layerClassName = 'a' + layer.layer.replace(/[.\/ ]/g, '') + + let btns = [showBtn, settingsBtn]; + + if (layer.subtype !== 'fits') { + btns.push(loadMOCBtn) + } + btns.push(deleteBtn) + + let item = Layout.horizontal({ + layout: [ + searchInput, + //'
' + (layer.name) + '
', + Layout.horizontal(btns) + ], + cssStyle: { + display: 'flex', + alignItems: 'center', + listStyle: 'none', + justifyContent: 'space-between', + width: '100%', + } + }); + + layout.push(item); + + if (!(layer.layer in self.HiPSui)) { + self.HiPSui[layer.layer] = { + searchInput, + settingsBox, + settingsBtn, + showBtn, + }; + } + } + + return layout; + } + + /*_findPreviewImageUrl(layer) { + if (layer instanceof ImageFITS) { + return; + } + + if (!layer.creatorDid) { + return; + } + + const creatorDid = layer.creatorDid; + + for (const key in Stack.previewImagesUrl) { + if (creatorDid.includes(key)) { + return Stack.previewImagesUrl[key]; + } + } + // if not found + return layer.url + '/preview.jpg' + }*/ + + _addOverlayIcon(overlay) { + var tooltipText; + var svg = ''; + if (overlay.type == 'catalog' || overlay.type == 'progressivecat') { + var nbSources = overlay.getSources().length; + tooltipText = nbSources + ' source' + (nbSources > 1 ? 's' : ''); + + svg = Icon.SVG_ICONS.CATALOG; + } + else if (overlay.type == 'moc') { + tooltipText = 'Coverage: ' + (100 * overlay.skyFraction()).toFixed(2) + ' % of sky'; + + svg = Icon.SVG_ICONS.MOC; + } + else if (overlay.type == 'overlay') { + svg = Icon.SVG_ICONS.OVERLAY; + } + + let tooltip; + if (tooltipText) { + tooltip = { content: tooltipText, position: {direction: 'bottom'} } + } + + // retrieve SVG icon, and apply the layer color + return new Icon({ + size: 'small', + url: Icon.dataURLFromSVG({svg, color: overlay.color}), + tooltip + }); + } + + _show(options) { + if (!this.aladin) { + return; + } + + this.position = (options && options.position) || this.position; + + if (!this.position) + return; + + this.position.aladin = this.aladin; + + super._show({ + ...options, + ...{position: this.position}, + }) + + const innerHeight = this.aladin.aladinDiv.offsetHeight; + this.element().querySelectorAll(".surveyItem") + .forEach((surveyItem) => { + surveyItem.querySelectorAll(".aladin-context-sub-menu") + // skip the first menu + .forEach((subMenu) => { + subMenu.style.display = 'block' + + let Y = innerHeight - (subMenu.getBoundingClientRect().y - this.aladin.aladinDiv.getBoundingClientRect().y); + subMenu.style.display = 'none' + + subMenu.style.maxHeight = Y + 'px'; + subMenu.style.overflowY = 'scroll'; + }) + }) + } +} diff --git a/src/js/gui/Box/StatusBarBox.js b/src/js/gui/Box/StatusBarBox.js index 75ad049a3..ac81b4809 100644 --- a/src/js/gui/Box/StatusBarBox.js +++ b/src/js/gui/Box/StatusBarBox.js @@ -38,7 +38,7 @@ import { Icon } from "../Widgets/Icon"; export class StatusBarBox extends Box { constructor(aladin, options) { - super(options, aladin.aladinDiv) + super({...options, close: false}, aladin.aladinDiv) this.addClass("aladin-status-bar"); diff --git a/src/js/gui/Button/OverlayStack.js b/src/js/gui/Button/OverlayStack.js index d9e005588..cab4955de 100644 --- a/src/js/gui/Button/OverlayStack.js +++ b/src/js/gui/Button/OverlayStack.js @@ -20,6 +20,8 @@ import { CtxMenuActionButtonOpener } from "./CtxMenuOpener"; import stackOverlayIconUrl from './../../../../assets/icons/stack.svg'; import { OverlayStack } from "../CtxMenu/OverlayStack"; +import { OverlayStackBox } from "../Box/StackBox"; +import { TogglerActionButton } from "./Toggler"; /****************************************************************************** * Aladin Lite project * @@ -35,14 +37,15 @@ import { OverlayStack } from "../CtxMenu/OverlayStack"; * Class representing a Tabs layout * @extends CtxMenuActionButtonOpener */ - export class OverlayStackButton extends CtxMenuActionButtonOpener { + export class OverlayStackButton extends TogglerActionButton { /** * UI responsible for displaying the viewport infos * @param {Aladin} aladin - The aladin instance. */ constructor(aladin, options) { let self; - let stack = new OverlayStack(aladin); + let stack = new OverlayStackBox(aladin); + super({ icon: { size: 'medium', @@ -56,7 +59,18 @@ import { OverlayStack } from "../CtxMenu/OverlayStack"; direction: 'top right' } }, - ctxMenu: stack, + toggled: false, + actionOn: (e) => { + stack._show({ + position: { + nextTo: self, + direction: 'right' + } + }) + }, + actionOff: (e) => { + stack._hide() + }, ...options }, aladin); diff --git a/src/js/gui/Button/Toggler.js b/src/js/gui/Button/Toggler.js index e878a8f03..009507917 100644 --- a/src/js/gui/Button/Toggler.js +++ b/src/js/gui/Button/Toggler.js @@ -43,20 +43,26 @@ export class TogglerActionButton extends ActionButton { ...options, toggled, action(o) { - toggled = !toggled; - - self.update({toggled, tooltip: toggled ? options.tooltipOn : options.tooltipOff}) - if (toggled && options.actionOn) { - options.actionOn(o) - } - - if (!toggled && options.actionOff) { - options.actionOff(o) - } - - options.action && options.action(o) + self.toggle(o); } }) + this.toggled = toggled; + self = this; } + + toggle(o) { + this.toggled = !this.toggled; + + if (this.toggled && this.options.actionOn) { + this.options.actionOn(o) + } + + if (!this.toggled && this.options.actionOff) { + this.options.actionOff(o) + } + + // once the actions has been executed, modify the styling + this.update({toggled: this.toggled, tooltip: this.toggled ? this.options.tooltipOn : this.options.tooltipOff}) + } } \ No newline at end of file diff --git a/src/js/gui/CtxMenu/OverlayStack.js b/src/js/gui/CtxMenu/OverlayStack.js index b0385f23a..f88ba3685 100644 --- a/src/js/gui/CtxMenu/OverlayStack.js +++ b/src/js/gui/CtxMenu/OverlayStack.js @@ -35,7 +35,7 @@ import { CatalogQueryBox } from "../Box/CatalogQueryBox.js"; import A from "../../A.js"; import { Utils } from "../../../js/Utils"; import { View } from "../../View.js"; -import { LayerEditBox } from "../Box/SurveyEditBox.js"; +import { HiPSSettingsBox } from "../Box/HiPSSettingsBox.js"; import { HiPSSelectorBox } from "../Box/HiPSSelectorBox.js"; import searchIconUrl from '../../../../assets/icons/search.svg'; import showIconUrl from '../../../../assets/icons/show.svg'; @@ -456,70 +456,6 @@ export class OverlayStack extends ContextMenu { } } - layout.push({ - label: 'Add survey', - subMenu: [ - { - label: { - icon: { - url: searchIconUrl, - monochrome: true, - tooltip: {content: 'From our database...', position: { direction: 'right' }}, - cssStyle: { - cursor: 'help', - }, - }, - content: 'Search for a survey' - }, - action: (e) => { - e.stopPropagation(); - e.preventDefault(); - - self._hide(); - - self.hipsSelectorBox = new HiPSSelectorBox(self.aladin); - // attach a callback - self.hipsSelectorBox.attach( - (HiPSId) => { - let name = Utils.uuidv4() - self.aladin.setOverlayImageLayer(HiPSId, name) - - self.show(); - } - ); - - self.hipsSelectorBox._show({ - position: self.position, - }); - - self.mode = 'hips'; - } - }, - ContextMenu.fileLoaderItem({ - label: 'FITS image file', - accept: '.fits', - action(file) { - let url = URL.createObjectURL(file); - - const image = self.aladin.createImageFITS( - url, - file.name, - undefined, - (ra, dec, fov, _) => { - // Center the view around the new fits object - self.aladin.gotoRaDec(ra, dec); - self.aladin.setFoV(fov * 1.1); - //self.aladin.selectLayer(image.layer); - }, - undefined - ); - - self.aladin.setOverlayImageLayer(image, Utils.uuidv4()) - } - }), - ] - }) - // survey list let selectedLayer = self.aladin.getSelectedLayer(); @@ -669,7 +605,7 @@ export class OverlayStack extends ContextMenu { let item = Layout.horizontal({ layout: [ '
' + (layer.name) + '
', - Layout.horizontal({layout: btns}) + Layout.horizontal(btns) ], /*cssStyle: { display: 'flex', diff --git a/src/js/gui/FoV.js b/src/js/gui/FoV.js index 8dc806f2a..13d93a2d2 100644 --- a/src/js/gui/FoV.js +++ b/src/js/gui/FoV.js @@ -42,29 +42,25 @@ export class FoV extends DOMElement { // constructor constructor(aladin, options) { let layout = []; - + if (options.showZoomControl) { - layout.push(new ActionButton({ + let zoomIn = new ActionButton({ + classList: 'aladin-zoom-in', size: 'small', + tooltip: {content: 'zoom in', position: {direction: 'top'}}, icon: { monochrome: true, size: 'small', url: plusIconUrl, }, - cssStyle: { - marginRight: 0, - borderRight: 'none', - borderRadius: '5px 0px 0px 5px' - }, action(o) { aladin.increaseZoom(); } - })) - layout.push(new ActionButton({ + }) + let zoomOut = new ActionButton({ size: 'small', - cssStyle: { - borderRadius: '0px 5px 5px 0px' - }, + classList: 'aladin-zoom-out', + tooltip: {content: 'zoom out', position: {direction: 'top'}}, icon: { monochrome: true, size: 'small', @@ -73,7 +69,12 @@ export class FoV extends DOMElement { action(o) { aladin.decreaseZoom(); } - })) + }); + zoomIn.el.classList.add('aladin-zoom-in'); + zoomOut.el.classList.add('aladin-zoom-out'); + + layout.push(zoomIn) + layout.push(zoomOut) } if (options.showFov) { @@ -82,10 +83,10 @@ export class FoV extends DOMElement { '
']) } - let el = Layout.horizontal({layout, tooltip: { content: 'FoV', position: {direction: "top"}}}); - if (el.tooltip) { - el.tooltip.addClass('aladin-fov'); - el.tooltip.addClass('aladin-dark-theme') + let el = Layout.horizontal({layout}); + if (el) { + el.addClass('aladin-fov'); + el.addClass('aladin-dark-theme') } super(el) diff --git a/src/js/gui/Input/HiPSSearch.js b/src/js/gui/Input/HiPSSearch.js new file mode 100644 index 000000000..6817ebfd8 --- /dev/null +++ b/src/js/gui/Input/HiPSSearch.js @@ -0,0 +1,149 @@ +// 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. +// + +import { Box } from "../Widgets/Box.js"; +import { Layout } from "../Layout.js"; +import { ActionButton } from "../Widgets/ActionButton.js"; +import { ALEvent } from "../../events/ALEvent.js"; +/****************************************************************************** + * Aladin Lite project + * + * File gui/HiPSSelector.js + * + * + * Author: Thomas Boch, Matthieu Baumann[CDS] + * + *****************************************************************************/ + +// 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 Location.js + * + * Author: Thomas Boch[CDS] + * + *****************************************************************************/ + + import { Input } from "./../Widgets/Input.js"; + + export class HiPSSearch extends Input { + static HiPSList = {}; + + // constructor + constructor(aladin, options) { + let self; + let layer = options && options.layer; + + aladin.view.catalogCanvas.addEventListener('click', (e) => { + self.el.blur(); + }); + + let prevKey = layer.name; + super({ + name: 'HiPS search', + type: 'text', + classList: ['search'], + name: 'survey', + placeholder: "Survey keywords or url", + autocomplete: {options: Object.keys(HiPSSearch.HiPSList)}, + title: layer.name, + actions: { + change(e) { + const key = e.target.value; + if (!key) { + self.update({value: prevKey, title: prevKey}); + return; + } + + let image; + // A user can put an url + try { + image = new URL(key).href; + } catch(e) { + // Or he can select a HiPS from the list given + let hips = HiPSSearch.HiPSList[key] + //console.log("HIPS", key, hips) + if (hips) { + image = hips.id || hips.url || undefined; + } else { + // Finally if not found, interpret the input text value as the HiPS (e.g. ID) + image = key; + } + } + + self.el.blur(); + + if (image) { + prevKey = key; + aladin.setOverlayImageLayer(image, layer.layer); + } + }, + /*input(e) { + let value = e.target.value; + + self.update({value, title: value}) + }*/ + }, + value: layer.name, + ...options + }) + this.addClass('aladin-HiPS-search') + + self = this; + this.layer = layer; + + this._addEventListeners(aladin); + } + + setAutocompletionList(options) { + this.update({autocomplete: {options}}) + } + + _addEventListeners(aladin) { + let self = this; + ALEvent.HIPS_LAYER_ADDED.listenedBy(aladin.aladinDiv, (e) => { + const layer = e.detail.layer; + if (layer.layer === self.layer.layer) { + let value = layer.name + self.update({value, title: value}) + } + }); + } +}; \ No newline at end of file diff --git a/src/js/gui/Location.js b/src/js/gui/Location.js index 8d7043039..ad0aa496a 100644 --- a/src/js/gui/Location.js +++ b/src/js/gui/Location.js @@ -28,7 +28,7 @@ * *****************************************************************************/ - +import { CooConversion } from "../CooConversion.js"; import { Coo } from "../libs/astro/coo.js"; import { CooFrameEnum } from "../CooFrameEnum.js"; @@ -146,7 +146,15 @@ export class Location extends DOMElement { let param = e.detail; if (param.type === 'mouseout') { - let [lon, lat] = aladin.getRaDec(); + let radec = aladin.getRaDec(); + // convert to the view frame + let lonlat = radec; + if (aladin.getFrame() === "Galactic") { + lonlat = CooConversion.J2000ToGalactic(radec) + } + + let [lon, lat] = lonlat; + self.update({ lon, lat, frame: aladin.view.cooFrame, diff --git a/src/js/gui/Widgets/Box.js b/src/js/gui/Widgets/Box.js index 23732ba79..a5b548e57 100644 --- a/src/js/gui/Widgets/Box.js +++ b/src/js/gui/Widgets/Box.js @@ -67,6 +67,25 @@ export class Box extends DOMElement { let self = this; + let close = this.options.close === false ? false : true; + if (close) { + new ActionButton({ + size: 'small', + content: '❌', + //tooltip: {content: 'Close the window', position: {direction: 'bottom'}}, + action(e) { + self._hide(); + }, + cssStyle: { + position: 'absolute', + }, + position: { + top: 0, + right: 0, + } + }, this.el); + } + // Check for the title if (this.options.header) { let header = this.options.header; @@ -98,23 +117,11 @@ export class Box extends DOMElement { titleEl.style.cursor = 'move' } - let closedEl = new ActionButton({ - size: 'small', - content: '❌', - tooltip: {content: 'Close the window', position: {direction: 'bottom'}}, - cssStyle: { - cursor: 'pointer', - }, - action(e) { - self._hide(); - } - }); - Layout.horizontal({ cssStyle: { justifyContent: 'space-between', }, - layout: [draggableEl, titleEl, closedEl] + layout: [draggableEl, titleEl] }, this.el); let separatorEl = document.createElement('div') diff --git a/src/js/gui/Widgets/Input.js b/src/js/gui/Widgets/Input.js index a6c70d1ef..5307b77ae 100644 --- a/src/js/gui/Widgets/Input.js +++ b/src/js/gui/Widgets/Input.js @@ -135,7 +135,7 @@ export class Input extends DOMElement { } this.el.appendChild(datalist); - this.el.autocomplete = 'on'; + this.el.autocomplete = 'off'; } else { this.el.autocomplete = autocomplete; } @@ -199,6 +199,10 @@ export class Input extends DOMElement { this.el.name = this.options.name; } + if (this.options.title) { + this.el.title = this.options.title; + } + this.el.classList.add('aladin-input'); this.el.classList.add('aladin-dark-theme'); diff --git a/src/js/gui/Widgets/Widget.js b/src/js/gui/Widgets/Widget.js index bf5224035..565a06dfd 100644 --- a/src/js/gui/Widgets/Widget.js +++ b/src/js/gui/Widgets/Widget.js @@ -146,9 +146,9 @@ export class DOMElement { } const aladinDiv = options && options.aladin && options.aladin.aladinDiv; - if (!aladinDiv) { - return; - } + let innerWidth = aladinDiv && aladinDiv.offsetWidth; + let innerHeight = aladinDiv && aladinDiv.offsetHeight; + let left, top, bottom, right; let x, y; @@ -156,11 +156,8 @@ export class DOMElement { // handle the anchor/dir case with higher priority const {offsetWidth, offsetHeight} = el; - const innerWidth = aladinDiv.offsetWidth; - const innerHeight = aladinDiv.offsetHeight; - // take on less priority the left and top - if (options && (options.left || options.top || options.right || options.bottom)) { + if (options && (options.left !== undefined || options.top !== undefined || options.right !== undefined || options.bottom !== undefined)) { el.style.position = 'absolute'; if (options.top !== undefined) { @@ -177,7 +174,7 @@ export class DOMElement { } if (typeof top === 'number') { - if (top + offsetHeight >= innerHeight) { + if (innerHeight && top + offsetHeight >= innerHeight) { y = '-' + (top + offsetHeight - innerHeight) + 'px'; } else if (top < 0) { y = Math.abs(top) + 'px'; @@ -189,7 +186,7 @@ export class DOMElement { bottom = bottom + 'px'; } if (typeof left === 'number') { - if (left + offsetWidth > innerWidth) { + if (innerWidth && left + offsetWidth > innerWidth) { x = '-' + (left + offsetWidth - innerWidth) + 'px'; } else if (left < 0) { x = Math.abs(left) + 'px';