From 2a60319ceb4f38def481e559fbf0e4f0bcf6c07e Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Tue, 22 Aug 2023 14:38:24 +0200 Subject: [PATCH 01/32] simple SODA resource parsing and window form --- examples/al-easy-access-vizier.html | 31 +++- examples/al-gw.html | 8 +- examples/al-soda-eso.html | 20 +++ examples/al-soda-ska.html | 20 +++ examples/index.html | 5 +- src/core/src/app.rs | 3 +- src/css/aladin.css | 45 ++++++ src/js/A.js | 21 +++ src/js/Aladin.js | 8 +- src/js/Catalog.js | 7 +- src/js/URLBuilder.js | 24 +++ src/js/View.js | 2 +- src/js/gui/WindowForm.js | 176 ++++++++++++++++++++ src/js/vo/Datalink.js | 191 ++++++++++++++-------- src/js/vo/ObsCore.js | 7 +- src/js/vo/VOTable.js | 239 +++++++++++++++++++++++----- 16 files changed, 679 insertions(+), 128 deletions(-) create mode 100644 examples/al-soda-eso.html create mode 100644 examples/al-soda-ska.html create mode 100644 src/js/gui/WindowForm.js diff --git a/examples/al-easy-access-vizier.html b/examples/al-easy-access-vizier.html index c568cfc38..3ec0f56fd 100644 --- a/examples/al-easy-access-vizier.html +++ b/examples/al-easy-access-vizier.html @@ -9,12 +9,31 @@ diff --git a/examples/al-gw.html b/examples/al-gw.html index bee55f15e..bcbcf1760 100644 --- a/examples/al-gw.html +++ b/examples/al-gw.html @@ -10,14 +10,8 @@ A.init.then(() => { aladin = A.aladin('#aladin-lite-div', {projection: "TAN", target: '15 16 57.636 -60 55 7.49', samp: true, showCooGrid: true, fov: 90, fullScreen: true}); - var moc_0_99 = A.MOCFromURL("./gw/gw_0.9.fits",{ name: "GW 90%", color: "#ff0000", opacity: 0.7, lineWidth: 5, perimeter: true}); - var moc_0_95 = A.MOCFromURL("./gw/gw_0.6.fits",{ name: "GW 60%", color: "#00ff00", opacity: 0.8, lineWidth: 5, perimeter: true}); - var moc_0_5 = A.MOCFromURL("./gw/gw_0.3.fits",{ name: "GW 30%", color: "#00ffff", opacity: 1.0, lineWidth: 5, perimeter: true}); - var moc_0_2 = A.MOCFromURL("./gw/gw_0.1.fits",{ name: "GW 10%", color: "#ff00ff", opacity: 1.0, lineWidth: 5, perimeter: true}); + var moc_0_2 = A.MOCFromURL("./gw/gw_0.1.fits",{ name: "GW 10%", color: "#ff00ff", opacity: 0.3, lineWidth: 5, perimeter: true, fill: true, fillColor:"#00ff00"}); - aladin.addMOC(moc_0_99); - aladin.addMOC(moc_0_95); - aladin.addMOC(moc_0_5); aladin.addMOC(moc_0_2); }); diff --git a/examples/al-soda-eso.html b/examples/al-soda-eso.html new file mode 100644 index 000000000..77de6b9f3 --- /dev/null +++ b/examples/al-soda-eso.html @@ -0,0 +1,20 @@ + + + + + + +
+ + + + diff --git a/examples/al-soda-ska.html b/examples/al-soda-ska.html new file mode 100644 index 000000000..0ed728a89 --- /dev/null +++ b/examples/al-soda-ska.html @@ -0,0 +1,20 @@ + + + + + + +
+ + + + diff --git a/examples/index.html b/examples/index.html index 24d3a21df..9d64a20ea 100644 --- a/examples/index.html +++ b/examples/index.html @@ -4,14 +4,13 @@ -
+
diff --git a/examples/al-soda-eso.html b/examples/al-soda-eso.html index 77de6b9f3..33a131082 100644 --- a/examples/al-soda-eso.html +++ b/examples/al-soda-eso.html @@ -10,9 +10,9 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "12 26 14.618 -08 12 31.70", fov: 10, projection: 'SIN', showContextMenu: true}); + aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "orion", fov: 10, projection: 'SIN', showContextMenu: true}); - const c1 = A.catalogFromURL('./eso.xml', {onClick: 'showTable', limit: 100}); + const c1 = A.catalogFromURL('./eso.xml', {onClick: 'showTable'}); aladin.addCatalog(c1); }); diff --git a/examples/al-soda-ska.html b/examples/al-soda-ska.html index 0ed728a89..4a8ae970b 100644 --- a/examples/al-soda-ska.html +++ b/examples/al-soda-ska.html @@ -10,9 +10,11 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "galactic center", fov: 10, projection: 'SIN', showContextMenu: true}); + aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "m51", fov: 180, projection: 'SIN', showContextMenu: true}); + + //const c1 = A.catalogFromSKAORucio("200 50", 90, {onClick: 'showTable'}); + const c1 = A.catalogFromURL('./ObsCoreRucioScs.xml', {onClick: 'showTable', limit: 1000}); - const c1 = A.catalogFromSKAORucio("galactic center", 90, {onClick: 'showTable'}); aladin.addCatalog(c1); }); diff --git a/package.json b/package.json index 7a15a3373..5c5773070 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,8 @@ "test:unit": "vitest run" }, "devDependencies": { - "happy-dom": "^8.9.0", + "happy-dom": "^10.11.0", "npm": "^9.8.1", - "typescript": "^5.0.4", "vite": "^4.3.8", "vite-plugin-css-injected-by-js": "^3.1.1", "vite-plugin-glsl": "^1.1.2", diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index bd9a91bdc..ba85f8b75 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -76,6 +76,8 @@ use math::projection::*; use votable::votable::VOTableWrapper; use wasm_bindgen::prelude::*; +use crate::math::angle::ToAngle; + mod app; pub mod async_task; mod camera; @@ -609,6 +611,15 @@ impl WebClient { .map(|v| Box::new([v.x, v.y]) as Box<[f64]>) } + #[wasm_bindgen(js_name = angularDist)] + pub fn ang_dist(&self, lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 { + crate::math::lonlat::ang_between_lonlat( + LonLatT::new(lon1.to_radians().to_angle(), lat1.to_radians().to_angle()), + LonLatT::new(lon2.to_radians().to_angle(), lat2.to_radians().to_angle()), + ) + .to_degrees() + } + #[wasm_bindgen(js_name = screenToClip)] pub fn screen_to_clip(&self, x: f64, y: f64) -> Box<[f64]> { let v = self.app.screen_to_clip(&Vector2::new(x, y)); diff --git a/src/css/aladin.css b/src/css/aladin.css index d7d9e5867..c579e7909 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -256,7 +256,7 @@ word-wrap:break-word; .aladin-layerBox { top: 30px; - padding: 4px 4px; + left: 40px; max-height: 90%; overflow-y: auto; } @@ -265,11 +265,29 @@ word-wrap:break-word; padding-bottom: 4px; } +.aladin-anchor-left { + top: 50%; + left: 0%; + transform: translate(0%,-50%); +} + +.aladin-anchor-right { + top: 50%; + right: 0%; + transform: translate(0%,50%); +} + +.aladin-anchor-center { + top: 50%; + left: 50%; + transform: translate(-50%,-50%); +} + .aladin-box { display: none; z-index: 30; + padding: 4px 4px; position: absolute; - left: 40px; background: whitesmoke; border-radius: 2px; @@ -386,6 +404,7 @@ canvas { .aladin-gotoBox { top: 68px; + left: 40px; padding: 4px; } @@ -440,6 +459,7 @@ canvas { .aladin-shareBox { bottom: 30px; padding: 4px; + left: 40px; } .aladin-shareInput { @@ -592,7 +612,7 @@ canvas { .aladin-btn { display: inline-block; - padding: 6px 8px; + padding: 8px; margin-bottom: 0; font-size: 12px; font-weight: normal; @@ -625,6 +645,18 @@ canvas { border-color: #38a730; } +.aladin-selectBtn { + margin-right: 4px; + margin-left: 4px; + + background-color: #bababa; + border-color: #484848; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAD/h4/MvwAAAAd0SU1FB+cIFxEZCysdHKoAAAEcSURBVCjPhdG/K8QBGMfxlzul7iaLXNQNus6mszDI9zrKYjDouxjFYHSTut0/wGYTgxF/gMNwFzmrLs5AyYUSCl0MJz8H7+np83x6evp8+Eko/Cm0fk5tBmQESKgoe27K0Y91j7yEEi4cCIypufsy9Ji3pShjRJe6TbemnbhrGtrkbXkyp+rBpboph6pCexowrCBpWRqdOpG2JKlguHlhQklG1S4iXj27EZdyrF85KjSOnEdnIhaN2tcukNNiyFvEP7TakHDk2pMrLODevXPXzrzYiKAisCMrjZgY0rJ2ZFWaT14Zc+vQrLhAny6hVR16rWlE0VAzrWpbSk63mnUdJi2r0/IZ9YwHRYMoyYpZccqX4XtZxe9l/eZP3e+aWVQQn/7CMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wOC0yM1QxNzoyNDo0NiswMDowMJfQN1cAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDgtMjNUMTc6MjQ6NDYrMDA6MDDmjY/rAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTA4LTIzVDE3OjI1OjExKzAwOjAw0x314AAAAABJRU5ErkJggg=='); + background-size: contain; + background-repeat: no-repeat; + background-position:center center; +} + .aladin-box-content > div { margin: 10px 0px 0px 0px; } @@ -660,16 +692,28 @@ canvas { margin-bottom: 5px; } +.aladin-form-param { + display: flex; + align-items: center; + margin-bottom: 7px; +} +.aladin-form-param .aladin-input { + width: 100%; +} + + .aladin-form-param-group { border: 2px solid silver; padding: 5px; } -.aladin-form-param-group > h1 { - position: relative; +.aladin-form-param-group .aladin-form-group-header { + display: flex; + align-items: center; + top: 0px; margin: 0; - font-size: 11px; + font-size: 13px; font-family: Verdana, Geneva, Tahoma, sans-serif; cursor: pointer; @@ -680,7 +724,7 @@ canvas { position: absolute; left: 0; bottom: 300px; - width: 200px; + width: 250px; line-height: 1.3; } diff --git a/src/js/Aladin.js b/src/js/Aladin.js index 031c4252d..8f576e379 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -47,7 +47,7 @@ import { ProjectionEnum } from "./ProjectionEnum.js"; import { Stack } from "./gui/Stack.js"; import { CooGrid } from "./gui/CooGrid.js"; import { ContextMenu } from "./gui/ContextMenu.js"; -import { WindowForm } from "./gui/WindowForm.js"; +import { SODAQueryWindow } from "./gui/SODAQueryWindow"; import { ALEvent } from "./events/ALEvent.js"; import { Color } from './Color.js'; import { ImageFITS } from "./ImageFITS.js"; @@ -164,7 +164,7 @@ export let Aladin = (function () { }); // Aladin SODA form - this.form = new WindowForm(this); + this.sodaQueryWindow = new SODAQueryWindow(this); // Aladin logo new AladinLogo(aladinDiv); @@ -1354,18 +1354,24 @@ export let Aladin = (function () { new ALEvent(alEventName).listenedBy(this.aladinDiv, customFn); }; - Aladin.prototype.select = function () { - this.fire('selectstart'); + // Possible values are 'rect' and 'circle' + // TODO: add a 'polygon' selection mode + Aladin.prototype.select = function (mode = 'rect', callbackFn) { + this.fire('selectstart', {mode: mode, callbackFn: callbackFn}); }; Aladin.prototype.fire = function (what, params) { if (what === 'selectstart') { - this.view.setMode(View.SELECT); + this.view.startSelection(params["mode"], params["callbackFn"]); } else if (what === 'selectend') { - this.view.setMode(View.PAN); + this.view.finishSelection(); + var callbackFn = this.callbacksByEventName['select']; - (typeof callbackFn === 'function') && callbackFn(params); + if (typeof callbackFn === "function") { + this.view.showSelectedObjects(); + callbackFn(this.view.selectedObjects); + } } }; @@ -1618,6 +1624,35 @@ export let Aladin = (function () { } }; + /** + * Transform world coordinates to pixel coordinates in the view + * + * @API + * + * @param ra + * @param dec + * + * @return a [x, y] array with pixel coordinates in the view. Returns null if the projection failed somehow + * + */ + Aladin.prototype.angularDist = function (x1, y1, x2, y2) { + // this might happen at early stage of initialization + if (!this.view) { + return; + } + + try { + const [ra1, dec1] = this.pix2world(x1, y1); + const [ra2, dec2] = this.pix2world(x2, y2); + + console.log(ra1, dec1, ra2, dec2) + + return this.wasm.angularDist(ra1, dec1, ra2, dec2); + } catch (e) { + return undefined; + } + }; + /** * * @API diff --git a/src/js/Catalog.js b/src/js/Catalog.js index c4b12cf57..774834c76 100644 --- a/src/js/Catalog.js +++ b/src/js/Catalog.js @@ -305,7 +305,7 @@ export let Catalog = (function() { let footprints = []; var coo = new Coo(); - console.log(fields, rows); + rows.every(row => { let ra, dec, region; var mesures = {}; diff --git a/src/js/DefaultActionsForContextMenu.js b/src/js/DefaultActionsForContextMenu.js index 17c89c206..6a0c3b9d1 100644 --- a/src/js/DefaultActionsForContextMenu.js +++ b/src/js/DefaultActionsForContextMenu.js @@ -160,7 +160,9 @@ export let DefaultActionsForContextMenu = (function () { label: "Select sources", action(o) { const a = aladinInstance; - a.select(); + a.select('rect', (_) => { + a.view.showSelectedObjects(); + }) } }, ] diff --git a/src/js/HiPSProperties.js b/src/js/HiPSProperties.js index b09516d2f..b5fb90bea 100644 --- a/src/js/HiPSProperties.js +++ b/src/js/HiPSProperties.js @@ -41,7 +41,7 @@ HiPSProperties.fetchFromID = async function(ID) { ID: "*" + ID + "*", }; - let metadata = await Utils.loadFromMirrors(MocServer.MIRRORS_HTTPS, { + let metadata = await Utils.loadFromUrls(MocServer.MIRRORS_HTTPS, { data: params, }).then(response => response.json()); diff --git a/src/js/MocServer.js b/src/js/MocServer.js index 0f8603e7d..e29c44073 100644 --- a/src/js/MocServer.js +++ b/src/js/MocServer.js @@ -54,7 +54,7 @@ export class MocServer { fields: "ID,hips_initial_fov,hips_initial_ra,hips_initial_dec,hips_pixel_bitpix,hips_creator,hips_copyright,hips_frame,hips_order,hips_order_min,hips_tile_width,hips_tile_format,hips_pixel_cut,obs_title,obs_description,obs_copyright,obs_regime,hips_data_range,hips_service_url", }; - this._allHiPSes = await Utils.loadFromMirrors(MocServer.MIRRORS_HTTPS, { + this._allHiPSes = await Utils.loadFromUrls(MocServer.MIRRORS_HTTPS, { data: params, }).then(response => response.json()); })(); @@ -72,7 +72,7 @@ export class MocServer { fmt: "json", fields: "ID,hips_copyright,hips_order,hips_order_min,obs_title,obs_description,obs_copyright,obs_regime,cs_service_url,hips_service_url", }; - this._allCatalogHiPSes = await Utils.loadFromMirrors(MocServer.MIRRORS_HTTPS, {data: params}) + this._allCatalogHiPSes = await Utils.loadFromUrls(MocServer.MIRRORS_HTTPS, {data: params}) .then(response => response.json()); })(); } diff --git a/src/js/PlanetaryFeaturesPointer.js b/src/js/PlanetaryFeaturesPointer.js index 88c4206a8..e9a61e389 100644 --- a/src/js/PlanetaryFeaturesPointer.js +++ b/src/js/PlanetaryFeaturesPointer.js @@ -89,7 +89,7 @@ export let PlanetaryFeaturesPointer = (function() { return ret; }; - Utils.loadFromMirrors(PlanetaryFeaturesPointer.MIRRORS, {contentType: "text/plain", data: params}) + Utils.loadFromUrls(PlanetaryFeaturesPointer.MIRRORS, {contentType: "text/plain", data: params}) .then((response) => response.text()) .then((result) => { aladinInstance.view.setCursor('pointer'); diff --git a/src/js/Selector.js b/src/js/Selector.js new file mode 100644 index 000000000..9a56ad286 --- /dev/null +++ b/src/js/Selector.js @@ -0,0 +1,121 @@ +// Copyright 2015 - 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 + * + * Class Selector + * + * A selector + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export let Selector = (function() { + // constructor + let Selector = function() {}; + + Selector.prototype.start = function(startCoo, mode, callbackFn) { + this.startCoo = { x: startCoo.x, y: startCoo.y}; + this.mode = mode; + this.callbackFn = callbackFn; + }; + + Selector.prototype.draw = function(ctx, dragCoo) { + ctx.fillStyle = "rgba(100, 240, 110, 0.20)"; + ctx.strokeStyle = "rgb(100, 240, 110)"; + ctx.lineWidth = 2; + switch (this.mode) { + case 'rect': + var w = dragCoo.x - this.startCoo.x; + var h = dragCoo.y - this.startCoo.y; + + ctx.fillRect(this.startCoo.x, this.startCoo.y, w, h); + ctx.strokeRect(this.startCoo.x, this.startCoo.y, w, h); + break; + case 'circle': + var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); + var r = Math.sqrt(r2); + + ctx.beginPath(); + ctx.arc(this.startCoo.x, this.startCoo.y, r, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + break; + default: + break; + } + }; + + Selector.prototype.finish = function(dragCoo) { + switch (this.mode) { + case 'rect': + var w = dragCoo.x - this.startCoo.x; + var h = dragCoo.y - this.startCoo.y; + + (typeof this.callbackFn === 'function') && this.callbackFn({ + x: this.startCoo.x, + y: this.startCoo.y, + w: w, + h: h + }); + break; + case 'circle': + var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); + var r = Math.sqrt(r2); + + (typeof this.callbackFn === 'function') && this.callbackFn({ + x: this.startCoo.x, + y: this.startCoo.y, + r: r, + }); + break; + default: + break; + } + } + + Selector.prototype.getBBox = function(dragCoo) { + let x, y, w, h; + switch (this.mode) { + case 'rect': + w = dragCoo.x - this.startCoo.x; + h = dragCoo.y - this.startCoo.y; + x = this.startCoo.x; + y = this.startCoo.y; + break; + case 'circle': + var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); + var r = Math.sqrt(r2); + + x = this.startCoo.x - r; + y = this.startCoo.y - r; + w = 2 * r; + h = 2 * r; + break; + default: + break; + } + + return {x: x, y: y, w: w, h: h}; + }; + + return Selector; +})(); \ No newline at end of file diff --git a/src/js/SimbadPointer.js b/src/js/SimbadPointer.js index e03d918b8..e09074c7f 100644 --- a/src/js/SimbadPointer.js +++ b/src/js/SimbadPointer.js @@ -44,7 +44,7 @@ export let SimbadPointer = (function() { var coo = new Coo(ra, dec, 7); var params = {"Ident": coo.format('s/'), "SR": radiusDegrees}; - Utils.loadFromMirrors(SimbadPointer.MIRRORS, {contentType: "text/plain", data: params}) + Utils.loadFromUrls(SimbadPointer.MIRRORS, {contentType: "text/plain", data: params}) .then((response) => response.text()) .then((result) => { aladinInstance.view.setCursor('pointer'); diff --git a/src/js/URLBuilder.js b/src/js/URLBuilder.js index a883aab06..abbd4172e 100644 --- a/src/js/URLBuilder.js +++ b/src/js/URLBuilder.js @@ -70,7 +70,7 @@ export let URLBuilder = (function() { } if (posParam) { - return 'https://ivoa.dachs.srcdev.skao.int/rucio/rucio/cone/form?__nevow_form__=genForm&hscs_pos=' + posParam + '&hscs_sr=' + encodeURIComponent(radiusDegrees * 60) + '&_DBOPTIONS_ORDER=_r&_DBOPTIONS_DIR=ASC&MAXREC=100&_FORMAT=VOTable&submit=Go'; + return 'https://ivoa.dachs.srcdev.skao.int/rucio/rucio/cone/form?__nevow_form__=genForm&hscs_pos=' + posParam + '&hscs_sr=' + encodeURIComponent(radiusDegrees * 60) + '&_FORMAT=VOTable&submit=Go'; } }, diff --git a/src/js/Utils.ts b/src/js/Utils.ts index e9fe92059..4b7de23df 100644 --- a/src/js/Utils.ts +++ b/src/js/Utils.ts @@ -223,52 +223,61 @@ Utils.LRUCache.prototype = { A promise is returned. When all the urls fail, a rejected Promise is returned so that it can be catched afterwards */ -Utils.loadFromMirrors = function (urls, options) { - const contentType = options && options.contentType || 'application/json' +Utils.loadFromUrls = function (urls, options) { const data = options && options.data || undefined const timeout = options && options.timeout || 5000 - - // Base case, when all urls have been fetched and failed - if (urls.length === 0) { - return Promise.reject('None of the urls given can be fetched!') - } - + const mode = options && options.mode || 'cors'; + const contentType = options && options.contentType || undefined; + // A controller that can abort the query when a timeout is reached const controller = new AbortController() // Launch a timemout that will interrupt the fetch if it has not yet succeded: const timeoutId = setTimeout(() => controller.abort(), timeout) - const init = { + let init = { // *GET, POST, PUT, DELETE, etc. method: 'GET', - headers: { + /*headers: { 'Content-Type': contentType - }, + },*/ // no-cors, *cors, same-origin - mode: 'cors', + mode: mode, // *default, no-cache, reload, force-cache, only-if-cached cache: 'default', - // manual, *follow, error - redirect: 'follow', // Abort the request when a timeout exceeded signal: controller.signal, } - const url = urls[0] + '?' + new URLSearchParams(data) + if (contentType) { + init.headers = { + 'Content-Type': contentType + }; + } + + let url = urls[0]; + if (data) { + url += '?' + new URLSearchParams(data) + } + return fetch(url, init) .then((response) => { // completed request before timeout fired clearTimeout(timeoutId) - if (!response.ok) { - return Promise.reject('Url: ', urls[0], ' cannot be reached in some way.') + return Promise.reject(response) } else { return response } }) .catch((e) => { // The request aborted because it was to slow, fetch the next url given recursively - return Utils.loadFromMirrors(urls.slice(1), options) + // Base case, when all urls have been fetched and failed + if (urls && urls.length === 1) { + let errorMsg = "Status: "+ e.status +"\nMessage: " + e.statusText; + return Promise.reject(errorMsg); + } else { + return Utils.loadFromUrls(urls.slice(1), options); + } }) } diff --git a/src/js/View.js b/src/js/View.js index bbca16415..b79573122 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -46,9 +46,9 @@ import { WebGLCtx } from "./WebGL.js"; import { Logger } from "./Logger.js"; import { ALEvent } from "./events/ALEvent.js"; import { ColorCfg } from "./ColorCfg.js"; - -import $ from 'jquery'; import { Footprint } from "./Footprint.js"; +import { Selector } from "./Selector.js"; +import $ from 'jquery'; export let View = (function () { @@ -149,6 +149,10 @@ export let View = (function () { // Frame setting this.changeFrame(this.cooFrame); + this.selector = new Selector(); + this.selectMode = 'rect'; + this.selectCallbackFn = undefined; + // Zoom starting setting const si = 500000.0; const alpha = 40.0; @@ -206,8 +210,7 @@ export let View = (function () { // some variables for mouse handling this.dragging = false; - this.dragx = null; - this.dragy = null; + this.dragCoo = null; this.rightclickx = null; this.rightclicky = null; this.selectedLayer = 'base'; @@ -381,6 +384,16 @@ export let View = (function () { ctx.oImageSmoothingEnabled = enableSmoothing; } + View.prototype.startSelection = function(mode, callbackFn) { + this.setMode(View.SELECT); + + this.selectMode = mode; + this.selectCallbackFn = callbackFn; + } + + View.prototype.finishSelection = function() { + this.setMode(View.PAN); + } View.prototype.setMode = function (mode) { this.mode = mode; @@ -463,7 +476,7 @@ export let View = (function () { const xymouse = Utils.relMouseCoords(view.imageCanvas, e); // deselect all the selected sources with Select panel - view.deselectObjects() + view.hideSelectedObjects() try { const lonlat = view.wasm.screenToWorld(xymouse.x, xymouse.y); @@ -535,18 +548,17 @@ export let View = (function () { return; } - view.dragx = xymouse.x; - view.dragy = xymouse.y; + view.dragCoo = xymouse; view.dragging = true; if (view.mode == View.PAN) { view.setCursor('move'); } else if (view.mode == View.SELECT) { - view.selectStartCoo = { x: view.dragx, y: view.dragy }; + view.selector.start(view.dragCoo, view.selectMode, view.selectCallbackFn); } - view.wasm.pressLeftMouseButton(view.dragx, view.dragy); + view.wasm.pressLeftMouseButton(view.dragCoo.x, view.dragCoo.y); // false disables default browser behaviour like possibility to touch hold for context menu. // To disable text selection use css user-select: none instead of putting this value to false @@ -598,58 +610,10 @@ export let View = (function () { } // end of "if (view.dragging) ... " if (selectionHasEnded) { - view.deselectObjects() - - const selectedObjects = view.getObjectsInBBox( - view.selectStartCoo.x, - view.selectStartCoo.y, - view.dragx - view.selectStartCoo.x, - view.dragy - view.selectStartCoo.y - ); - - selectedObjects.forEach((objListPerCatalog) => { - objListPerCatalog.forEach((obj) => obj.select()) - }); - - if (selectedObjects.length > 0) { - let options = {}; - - let tables = selectedObjects.map((objList) => { - // Get the catalog containing that list of objects - let catalog = objList[0].getCatalog(); - - let rows = objList.map((o) => { - if (o instanceof Footprint) { - return o.source; - } else { - return o; - } - }); - let table = { - 'name': catalog.name, - 'color': catalog.color, - 'rows': rows, - 'fields': catalog.fields, - 'fieldsClickedActions': catalog.fieldsClickedActions, - }; - - if (catalog.isObsCore && catalog.isObsCore()) { - // If the source is obscore, save the table state inside the measurement table - // This is used to go back from a possible datalink table to the obscore one - options["save"] = true; - } - - return table; - }) - - view.aladin.measurementTable.showMeasurement(tables, options); - } - - view.selectedObjects = selectedObjects; + view.selector.finish(view.dragCoo); view.aladin.fire( - 'selectend', - selectedObjects + 'selectend' ); view.requestRedraw(); @@ -658,7 +622,7 @@ export let View = (function () { } view.mustClearCatalog = true; - view.dragx = view.dragy = null; + view.dragCoo = null; const xymouse = Utils.relMouseCoords(view.imageCanvas, e); if (e.type === "mouseout" || e.type === "touchend" || e.type === "touchcancel") { @@ -683,7 +647,7 @@ export let View = (function () { // popup to show ? var objs = view.closestObjects(xymouse.x, xymouse.y, 5); if (!wasDragging && objs) { - view.deselectObjects(); + view.hideSelectedObjects(); var o = objs[0]; @@ -720,11 +684,11 @@ export let View = (function () { } else { if (!wasDragging) { // Deselect objects if any - view.deselectObjects(); + view.hideSelectedObjects(); // If there is a past clicked object if (view.lastClickedObject) { - view.aladin.measurementTable.hide(); + //view.aladin.measurementTable.hide(); //view.aladin.sodaForm.hide(); view.popup.hide(); @@ -903,13 +867,9 @@ export let View = (function () { return; } - //var xoffset, yoffset; - var s1, s2; - s1 = { x: view.dragx, y: view.dragy }; - s2 = { x: xymouse.x, y: xymouse.y }; - - view.dragx = xymouse.x; - view.dragy = xymouse.y; + var s1 = view.dragCoo, s2 = xymouse; + // update drag coo with the new position + view.dragCoo = xymouse; if (view.mode == View.SELECT) { view.requestRedraw(); @@ -1181,11 +1141,7 @@ export let View = (function () { catalogCanvasCleared = true; } - reticleCtx.fillStyle = "rgba(100, 240, 110, 0.25)"; - var w = this.dragx - this.selectStartCoo.x; - var h = this.dragy - this.selectStartCoo.y; - - reticleCtx.fillRect(this.selectStartCoo.x, this.selectStartCoo.y, w, h); + this.selector.draw(reticleCtx, this.dragCoo) } } else { // Normal modes @@ -1264,7 +1220,7 @@ export let View = (function () { return pixList; }; - View.prototype.deselectObjects = function() { + View.prototype.hideSelectedObjects = function() { if (this.selectedObjects) { this.selectedObjects.forEach((objList) => { objList.forEach((o) => o.deselect()) @@ -1272,6 +1228,52 @@ export let View = (function () { this.aladin.measurementTable.hide(); this.selectedObjects = null; + + this.requestRedraw(); + } + } + + View.prototype.showSelectedObjects = function() { + this.hideSelectedObjects(); + + this.selectedObjects = this.getSelectedObjects(); + + if (this.selectedObjects.length > 0) { + this.selectedObjects.forEach((objListPerCatalog) => { + objListPerCatalog.forEach((obj) => obj.select()) + }); + + let saveTable = false; + + let tables = this.selectedObjects.map((objList) => { + // Get the catalog containing that list of objects + let catalog = objList[0].getCatalog(); + + let rows = objList.map((o) => { + if (o instanceof Footprint) { + return o.source; + } else { + return o; + } + }); + let table = { + 'name': catalog.name, + 'color': catalog.color, + 'rows': rows, + 'fields': catalog.fields, + 'fieldsClickedActions': catalog.fieldsClickedActions, + }; + + if (catalog.isObsCore && catalog.isObsCore()) { + // If the source is obscore, save the table state inside the measurement table + // This is used to go back from a possible datalink table to the obscore one + saveTable = true; + } + + return table; + }) + + this.aladin.measurementTable.showMeasurement(tables, {save: saveTable}); } } @@ -1903,7 +1905,9 @@ export let View = (function () { moc.setView(this); }; - View.prototype.getObjectsInBBox = function (x, y, w, h) { + View.prototype.getSelectedObjects = function () { + let {x, y, w, h} = this.selector.getBBox(this.dragCoo); + if (w < 0) { x = x + w; w = -w; diff --git a/src/js/gui/WindowForm.js b/src/js/gui/SODAQueryWindow.js similarity index 54% rename from src/js/gui/WindowForm.js rename to src/js/gui/SODAQueryWindow.js index 758c0eca5..1c7726082 100644 --- a/src/js/gui/WindowForm.js +++ b/src/js/gui/SODAQueryWindow.js @@ -34,17 +34,17 @@ import { Coo } from '../libs/astro/coo.js'; import { CooFrameEnum } from '../CooFrameEnum.js'; import { Utils } from '../Utils'; -export class WindowForm { - constructor(aladin, title) { +export class SODAQueryWindow { + constructor(aladin) { this.aladin = aladin; this.isShowing = false; - this.title = title; } _attachParam(target, input) { if (input.type === "text" || input.type === "number") { let inputEl = document.createElement('input'); inputEl.type = input.type; + inputEl.classList.add('aladin-input'); if (input.type === "number") { inputEl.step = "any"; } @@ -69,7 +69,27 @@ export class WindowForm { } else if (input.type === "group") { let groupEl = document.createElement('div'); groupEl.classList.add(input.name, "aladin-form-param-group"); - groupEl.innerHTML = "

" + input.name + "

"; + groupEl.innerHTML = '
' + input.name + '
'; + + if (input.name === 'CIRCLE') { + let circleSelectBtnEl = document.createElement('div'); + circleSelectBtnEl.classList.add('aladin-btn', 'aladin-selectBtn'); + circleSelectBtnEl.addEventListener('click', (e) => { + this.aladin.select('circle', (s) => { + const {x, y, r} = s; + + const [ra, dec] = this.aladin.pix2world(x, y); + const dist = this.aladin.angularDist(x, y, x + r, y); + // find the children + let [raInputEl, decInputEl, radiusInputEl] = groupEl.querySelectorAll(".aladin-form-param input"); + + raInputEl.value = ra; + decInputEl.value = dec; + radiusInputEl.value = dist; + }); + }); + groupEl.querySelector(".aladin-form-group-header").appendChild(circleSelectBtnEl); + } input.value.forEach((subInput) => this._attachParam(groupEl, subInput)); @@ -85,7 +105,7 @@ export class WindowForm { this.isShowing = false; } - show(callbackValid) { + show(callbackValid) { this.isShowing = true; this.formEl = document.createElement('form'); @@ -100,66 +120,66 @@ export class WindowForm { this.formEl.appendChild(submitFormDiv); this.mainEl = document.createElement('div'); - this.mainEl.className = 'aladin-window-container'; - this.mainEl.innerHTML = '
×
' + this.title + '
'; + this.mainEl.classList.add('aladin-box', 'aladin-anchor-left'); + this.mainEl.style.display = 'initial'; + + this.mainEl.innerHTML = '
×
Cutout Query Window
'; - let windowEl = this.mainEl.querySelector(".aladin-window"); + console.log(this.mainEl); + let windowEl = this.mainEl.querySelector("div"); windowEl.appendChild(this.formEl); this.aladin.aladinDiv.appendChild(this.mainEl); - this.mainEl.querySelector(".aladin-window .aladin-closeBtn") + this.mainEl.querySelector(".aladin-closeBtn") .addEventListener( "click", () => { this.hide(); } ); - this.mainEl.querySelector(".aladin-window .submit .aladin-cancelBtn") + this.mainEl.querySelector(".submit .aladin-cancelBtn") .addEventListener( "click", () => { this.hide(); } ); - this.formEl.addEventListener("submit", (e) => { - e.preventDefault(); - let params = []; - - for (let child of this.formEl.children) { - let param; - if (child.classList.contains("aladin-form-param")) { - // get the input - let input = child.querySelector("input"); - param = { - name: input.name, - value: input.value - }; - } else if (child.classList.contains("aladin-form-param-group")) { - let values = []; - for (let formParam of child.children) { - if (formParam.classList.contains("aladin-form-param")) { - // get the input - let input = formParam.querySelector("input"); - values.push(input.value); + this.mainEl.querySelector(".submit .aladin-validBtn") + .addEventListener("click", (e) => { + e.preventDefault(); + let params = []; + + for (let child of this.formEl.children) { + let param; + if (child.classList.contains("aladin-form-param")) { + // get the input + let input = child.querySelector("input"); + param = { + name: input.name, + value: input.value + }; + } else if (child.classList.contains("aladin-form-param-group")) { + let values = []; + for (let formParam of child.children) { + if (formParam.classList.contains("aladin-form-param")) { + // get the input + let input = formParam.querySelector("input"); + values.push(input.value); + } } + + param = { + name: child.classList[0], + value: values + }; } - param = { - name: child.classList[0], - value: values - }; + if (param) { + params.push(param); + } } - if (param) { - params.push(param); + if (callbackValid) { + callbackValid(this.formParams["baseUrl"], params); } - } - - if (callbackValid) { - callbackValid(this.formParams["baseUrl"], params); - } - }); - } - - setTitle(title) { - this.title = title; + }); } setParams(params) { diff --git a/src/js/vo/Datalink.js b/src/js/vo/Datalink.js index 30c2cb1f2..ff903c4f8 100644 --- a/src/js/vo/Datalink.js +++ b/src/js/vo/Datalink.js @@ -36,10 +36,12 @@ export let Datalink = (function() { let Datalink = function () { this.SODAServerParams = undefined; - this.form = undefined; + this.sodaQueryWindow = undefined; }; - Datalink.prototype.handleActions = function(url, aladinInstance) { + Datalink.prototype.handleActions = function(obscoreRow, aladinInstance) { + + const url = obscoreRow["access_url"]; VOTable.parse( url, (rsc) => { @@ -67,12 +69,12 @@ export let Datalink = (function() { 'fields': fields, 'fieldsClickedActions': { 'service_def': (row) => { - let service = row['service_def']; + const service = row['service_def']; + if (service) { - aladinInstance.form.hide(); - aladinInstance.form.setTitle("SODA Query form"); - aladinInstance.form.setParams(this.SODAServerParams); - aladinInstance.form.show((baseUrl, SODAParams) => { + aladinInstance.sodaQueryWindow.hide(); + aladinInstance.sodaQueryWindow.setParams(this.SODAServerParams); + aladinInstance.sodaQueryWindow.show((baseUrl, SODAParams) => { let url = new URL(baseUrl) SODAParams.forEach((param) => { let value; @@ -81,32 +83,38 @@ export let Datalink = (function() { } else { value = param.value; } - url.searchParams.append(param.name, value) + url.searchParams.append(param.name, value); }); let spinnerEl = document.createElement('div'); spinnerEl.classList.add("aladin-spinner"); spinnerEl.innerText = "fetching..."; - aladinInstance.form.mainEl.querySelector(".aladin-window .submit") + aladinInstance.sodaQueryWindow.mainEl.querySelector(".submit") .appendChild(spinnerEl); - // tackle cors problems - console.log(url) - url = Utils.handleCORSNotSameOrigin(url); - fetch(url) - .then((response) => { - if (response.status == 200) { - return response; - } else { - return Promise.reject("Error status code: " + response.status + ".\nStatus message: " + response.statusText); + let removeSpinner = () => { + aladinInstance.sodaQueryWindow.mainEl.querySelector(".aladin-spinner").remove(); + }; + + let name = url.searchParams.toString(); + // Tackle cors problems + Utils.loadFromUrls([url, Utils.handleCORSNotSameOrigin(url)], {timeout: 30000}) + .then((response) => response.blob()) + .then((blob) => { + const url = URL.createObjectURL(blob); + try { + let image = aladinInstance.createImageFITS(url, name); + aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) + } catch(e) { + throw('Fail to interpret ' + url + ' as a fits file') } }) .catch((e) => { window.alert(e) }) .finally(() => { - aladinInstance.form.mainEl.querySelector(".aladin-spinner").remove(); + removeSpinner(); }) }); } @@ -160,14 +168,57 @@ export let Datalink = (function() { aladinInstance.measurementTable.showMeasurement([datalinkTable], { save: true }); } else { - // resource meta + // Try to parse a SODA service descriptor resource let SODAServerParams = VOTable.parseSODAServiceRsc(rsc); if (SODAServerParams) { this.SODAServerParams = SODAServerParams; + + // Try to populate the SODA form fields with obscore values + let populateSODAFields = (SODAParams) => { + for (const inputParam of SODAParams.inputParams) { + if (inputParam.type === "group") { + for (const param of inputParam.value) { + if (param.value) { + continue; + } + + if (param.name === "ra") { + param.value = obscoreRow['s_ra']; + } else if (param.name === "dec") { + param.value = obscoreRow['s_dec']; + } else if (param.name === "fmin") { + param.value = obscoreRow['em_min']; + } else if (param.name === "fmax") { + param.value = obscoreRow['em_max']; + } else if (param.name === "rad") { + param.value = obscoreRow['s_fov'] / 2; + } + } + } + } + }; + + // Request the base url of the SODA server to check if there + // is a self description VOTable + VOTable.parse(this.SODAServerParams.baseUrl, (rsc) => { + const SODAServerDesc = VOTable.parseSODAServiceRsc(rsc); + + for (const inputParam of SODAServerDesc.inputParams) { + const inputParamAlreadyDefined = this.SODAServerParams.inputParams.some((inputParamFromDatalink) => inputParamFromDatalink.name === inputParam.name); + if (!inputParamAlreadyDefined) { + this.SODAServerParams.inputParams.push(inputParam); + } + } + + populateSODAFields(this.SODAServerParams); + }, undefined, true); + + populateSODAFields(this.SODAServerParams); } - console.log(this.SODAServerParams) } - } + }, + undefined, + true ) }; diff --git a/src/js/vo/ObsCore.js b/src/js/vo/ObsCore.js index 2f86eeffe..8438fb11e 100644 --- a/src/js/vo/ObsCore.js +++ b/src/js/vo/ObsCore.js @@ -218,8 +218,7 @@ // A datalink response containing links to datasets or services attached to the current dataset case 'application/x-votable+xml;content=datalink': //Datalink.handleActions(url) - let dl = new Datalink(); - dl.handleActions(url, aladinInstance); + new Datalink().handleActions(row, aladinInstance); break; // Any multidimensional regularly sampled FITS image or cube case 'image/fits': diff --git a/src/js/vo/VOTable.js b/src/js/vo/VOTable.js index 6fef0fb6d..00cb3fb61 100644 --- a/src/js/vo/VOTable.js +++ b/src/js/vo/VOTable.js @@ -106,8 +106,6 @@ export let VOTable = (function() { throw 'VOTable has data type not handled:' + data.get("data_type"); } - console.log(rows, fields) - return {fields: fields, rows: rows}; } } @@ -117,8 +115,9 @@ export let VOTable = (function() { let isSODAService = rsc.get("utype") === "adhoc:service"; if (isSODAService) { let elems = rsc.get("elems"); + let id = rsc.get("ID"); - if (rsc.get("ID").includes("async")) { + if (id && id.includes("async")) { // First way to check if the resource refers to a async SODA service return; } @@ -128,25 +127,22 @@ export let VOTable = (function() { let inputParams = []; elems.forEach((elem) => { - let elemObj; if (elem instanceof Map) { - elemObj = Object.fromEntries(elem.entries()); - } else { - elemObj = elem; + elem = Object.fromEntries(elem.entries()); } // SODA access url - if (elemObj["elem_type"] === "Param" && (elemObj["ucd"] === "meta.ref.url" || elemObj["name"] === "accessURL")) { - accessUrl = elemObj["value"]; - } else if (elemObj["elem_type"] === "Param" && elemObj["name"] === "standardID") { + if (elem["elem_type"] === "Param" && (elem["ucd"] === "meta.ref.url" || elem["name"] === "accessURL")) { + accessUrl = elem["value"]; + } else if (elem["elem_type"] === "Param" && elem["name"] === "standardID") { // Check if it is the sync service // discard the async - if (elemObj["value"].includes("async")) { + if (elem["value"].includes("async")) { return; } // Input params group - } else if (elemObj["name"] === "inputParams") { - elemObj["elems"].forEach((inputParam) => { + } else if (elem["name"] === "inputParams") { + elem["elems"].forEach((inputParam) => { let name = inputParam.get("name"); let utype = inputParam.get("utype"); let values; @@ -164,7 +160,10 @@ export let VOTable = (function() { }) break; case 'CIRCLE': - values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); + if (inputParam.get("values")) { + values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); + } + inputParams.push({ name: 'CIRCLE', type: 'group', @@ -172,52 +171,55 @@ export let VOTable = (function() { value: [{ name: 'ra', type: 'number', - maxVal: values[0], - value: values[0], + maxVal: values && values[0], + value: values && values[0], utype: utype }, { name: 'dec', type: 'number', - maxVal: values[1], - value: values[1], + maxVal: values && values[1], + value: values && values[1], utype: utype }, { name: 'rad', type: 'number', - maxVal: values[2], - value: values[2], + maxVal: values && values[2], + value: values && values[2], utype: utype }] }); break; case 'BAND': - values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); + if (inputParam.get("values")) { + values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); + } inputParams.push({ name: 'BAND', type: 'group', description: inputParam.get("description"), value: [{ - name: 'fmax', + name: 'fmin', type: 'number', - maxVal: values[0], - value: values[0], + maxVal: values && values[0], + value: values && values[0], utype: utype }, { - name: 'fmin', + name: 'fmax', type: 'number', - maxVal: values[1], - value: values[1], + maxVal: values && values[1], + value: values && values[1], utype: utype }] }); break; case 'RANGE': - values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {console.log(v); return +v;}); - + if (inputParam.get("values")) { + values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); + } inputParams.push({ name: 'RANGE', type: 'group', @@ -225,29 +227,29 @@ export let VOTable = (function() { value: [{ name: 'raMin', type: 'number', - maxVal: values[0], - value: values[0], + maxVal: values && values[0], + value: values && values[0], utype: utype }, { name: 'raMax', type: 'number', - maxVal: values[1], - value: values[1], + maxVal: values && values[1], + value: values && values[1], utype: utype }, { name: 'decMin', type: 'number', - maxVal: values[2], - value: values[2], + maxVal: values && values[2], + value: values && values[2], utype: utype }, { name: 'decMax', type: 'number', - maxVal: values[3], - value: values[3], + maxVal: values && values[3], + value: values && values[3], utype: utype }] }); diff --git a/tests/unit/Utils.spec.js b/tests/unit/Utils.spec.js index 52625c97a..99f04980f 100644 --- a/tests/unit/Utils.spec.js +++ b/tests/unit/Utils.spec.js @@ -2,7 +2,7 @@ import {Utils} from '@/js/Utils.ts'; describe('Utils.ts', () => { beforeEach(() => { - delete window.location; + //delete window.location; window.location = {href: {}, search: ''}; }); diff --git a/tsconfig.json b/tsconfig.json index 96692920a..c8e90c198 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,7 +39,7 @@ "include": [ "src/**/*.ts", "src/**/*.d.ts", - "src/**/*.tsx", - "tests/unit/**/*.ts" + //"src/**/*.tsx", + //"tests/unit/**/*.ts" ] } From 373f8b9975b5e327cce1a73de6f690960afab0fb Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Thu, 24 Aug 2023 23:25:14 +0200 Subject: [PATCH 03/32] fix hover line thickness --- src/css/aladin.css | 1 + src/js/Circle.js | 4 ++++ src/js/Ellipse.js | 4 ++++ src/js/Footprint.js | 8 +++++++- src/js/Polyline.js | 9 ++++++++- src/js/View.js | 28 ++++++++++++++++++---------- 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/css/aladin.css b/src/css/aladin.css index c579e7909..a23837c7f 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -160,6 +160,7 @@ text-overflow: ellipsis; text-align: left; color: #000; + max-width: 150px; } .aladin-marker-measurement { diff --git a/src/js/Circle.js b/src/js/Circle.js index 57445276c..5e61d0249 100644 --- a/src/js/Circle.js +++ b/src/js/Circle.js @@ -84,6 +84,10 @@ export let Circle = (function() { } }; + Circle.prototype.getLineWidth = function() { + return this.lineWidth; + }; + Circle.prototype.setOverlay = function(overlay) { this.overlay = overlay; }; diff --git a/src/js/Ellipse.js b/src/js/Ellipse.js index 599cfb7ac..4e8e0c637 100644 --- a/src/js/Ellipse.js +++ b/src/js/Ellipse.js @@ -86,6 +86,10 @@ export let Ellipse = (function() { } }; + Ellipse.prototype.getLineWidth = function() { + return this.lineWidth; + }; + Ellipse.prototype.setOverlay = function(overlay) { this.overlay = overlay; }; diff --git a/src/js/Footprint.js b/src/js/Footprint.js index b9b61918b..24317970f 100644 --- a/src/js/Footprint.js +++ b/src/js/Footprint.js @@ -83,7 +83,13 @@ export let Footprint= (function() { }; Footprint.prototype.setLineWidth = function(lineWidth) { - this.shapes.forEach((shape) => shape.setLineWidth()) + this.shapes.forEach((shape) => shape.setLineWidth(lineWidth)) + }; + + Footprint.prototype.getLineWidth = function() { + if (this.shapes && this.shapes.length > 0) { + return this.shapes[0].getLineWidth(); + } }; Footprint.prototype.setColor = function(color) { diff --git a/src/js/Polyline.js b/src/js/Polyline.js index 9299e4820..5670e088e 100644 --- a/src/js/Polyline.js +++ b/src/js/Polyline.js @@ -37,7 +37,7 @@ import { AladinUtils } from './AladinUtils.js'; import { Line } from './Line.js'; import { Utils } from './Utils'; import { Overlay } from "./Overlay.js"; -import { ProjectionEnum, projectionNames } from "./ProjectionEnum.js"; +import { ProjectionEnum } from "./ProjectionEnum.js"; export let Polyline= (function() { @@ -146,10 +146,15 @@ export let Polyline= (function() { } }; + Polyline.prototype.getLineWidth = function() { + return this.lineWidth; + }; + Polyline.prototype.setLineWidth = function(lineWidth) { if (this.lineWidth == lineWidth) { return; } + this.lineWidth = lineWidth; if (this.overlay) { this.overlay.reportChange(); @@ -388,6 +393,8 @@ export let Polyline= (function() { }; Polyline.prototype.isInStroke = function(ctx, view, x, y) { + ctx.lineWidth = this.lineWidth; + let pointXY = []; for (var j = 0; j < this.radecArray.length; j++) { var xy = AladinUtils.radecToViewXy(this.radecArray[j][0], this.radecArray[j][1], view); diff --git a/src/js/View.js b/src/js/View.js index b79573122..3fddfb5e0 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -1221,16 +1221,18 @@ export let View = (function () { }; View.prototype.hideSelectedObjects = function() { + this.aladin.measurementTable.hide(); + if (this.selectedObjects) { this.selectedObjects.forEach((objList) => { objList.forEach((o) => o.deselect()) }); - this.aladin.measurementTable.hide(); this.selectedObjects = null; - - this.requestRedraw(); } + + this.requestRedraw(); + //} } View.prototype.showSelectedObjects = function() { @@ -1999,9 +2001,16 @@ export let View = (function () { footprints.forEach((footprint) => { // Hidden footprints are not considered + let lineWidth = footprint.getLineWidth(); + + footprint.setLineWidth(6.0); if (footprint.isShowing && footprint.isInStroke(ctx, this, x, y)) { closest = footprint; - return; + } + footprint.setLineWidth(lineWidth); + + if (closest) { + return closest; } }) @@ -2015,8 +2024,7 @@ export let View = (function () { var canvas = this.catalogCanvas; var ctx = canvas.getContext("2d"); // this makes footprint selection easier as the catch-zone is larger - let pastLineWidth = ctx.lineWidth; - ctx.lineWidth = 6.0; + //let pastLineWidth = ctx.lineWidth; if (this.overlays) { for (var k = 0; k < this.overlays.length; k++) { @@ -2024,7 +2032,7 @@ export let View = (function () { let closest = this.closestFootprints(overlay.overlayItems, ctx, x, y); if (closest) { - ctx.lineWidth = pastLineWidth; + //ctx.lineWidth = pastLineWidth; return [closest]; } } @@ -2037,18 +2045,18 @@ export let View = (function () { let closest = this.closestFootprints(catalog.footprints, ctx, x, y); if (closest) { - ctx.lineWidth = pastLineWidth; + //ctx.lineWidth = pastLineWidth; return [closest]; } } } if (!this.objLookup) { - ctx.lineWidth = pastLineWidth; + //ctx.lineWidth = pastLineWidth; return null; } - ctx.lineWidth = pastLineWidth; + //ctx.lineWidth = pastLineWidth; var closest, dist; for (var r = 0; r <= maxRadius; r++) { From 070b4882c4b8b2f422be1a4c17d4235a5eae0e82 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Mon, 28 Aug 2023 15:41:18 +0200 Subject: [PATCH 04/32] add some ui widgets --- assets/icons/target.svg | 1 + assets/target.svg | 100 ----------- examples/al-coronelli.html | 4 +- src/css/aladin.css | 145 ++++++++------- src/js/Aladin.js | 4 +- src/js/GenericPointer.js | 2 +- src/js/HiPSProperties.js | 2 - src/js/Utils.ts | 2 +- src/js/View.js | 10 +- src/js/gui/CatalogSelector.js | 2 +- src/js/gui/CooGrid.js | 2 +- src/js/gui/HiPSLayer.js | 229 ++++++++++++------------ src/js/gui/HiPSSelector.js | 2 +- src/js/gui/SODAQueryWindow.js | 74 +++++--- src/js/gui/Stack.js | 14 +- src/js/gui/widgets/ActionButton.js | 83 +++++++++ src/js/gui/{ => widgets}/ContextMenu.js | 28 +-- src/js/gui/widgets/Form.js | 101 +++++++++++ src/js/gui/widgets/Tooltip.js | 67 +++++++ src/js/gui/widgets/Utils.js | 7 + src/js/gui/widgets/Widget.js | 57 ++++++ 21 files changed, 576 insertions(+), 360 deletions(-) create mode 100644 assets/icons/target.svg delete mode 100644 assets/target.svg create mode 100644 src/js/gui/widgets/ActionButton.js rename src/js/gui/{ => widgets}/ContextMenu.js (89%) create mode 100644 src/js/gui/widgets/Form.js create mode 100644 src/js/gui/widgets/Tooltip.js create mode 100644 src/js/gui/widgets/Utils.js create mode 100644 src/js/gui/widgets/Widget.js diff --git a/assets/icons/target.svg b/assets/icons/target.svg new file mode 100644 index 000000000..090311884 --- /dev/null +++ b/assets/icons/target.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/target.svg b/assets/target.svg deleted file mode 100644 index ed04eec3d..000000000 --- a/assets/target.svg +++ /dev/null @@ -1,100 +0,0 @@ - - - - diff --git a/examples/al-coronelli.html b/examples/al-coronelli.html index e5d9a70db..97decd871 100644 --- a/examples/al-coronelli.html +++ b/examples/al-coronelli.html @@ -497,7 +497,7 @@ deleteOverlayTimeout = undefined; } isDrawing = true; - points.push([Utils.relMouseCoords(drawOverlayCanvas.imageCanvas, e)]); + points.push([Utils.relMouseCoords(e)]); }); @@ -506,7 +506,7 @@ e.preventDefault(); drawOverlayCtx.clearRect(0, 0, drawOverlayCtx.canvas.width, drawOverlayCtx.canvas.height); - points[points.length-1].push(Utils.relMouseCoords(drawOverlayCanvas.imageCanvas, e)); + points[points.length-1].push(Utils.relMouseCoords(e)); drawOverlayCtx.beginPath(); diff --git a/src/css/aladin.css b/src/css/aladin.css index a23837c7f..efcb67f6f 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -306,6 +306,8 @@ word-wrap:break-word; -ms-overflow-style: none; /* for Internet Explorer, Edge */ scrollbar-width: none; /* for Firefox */ overflow-y: scroll; + + overflow-y: inherit; } .aladin-box::-webkit-scrollbar { @@ -343,10 +345,6 @@ input[type=number] { padding: 0.8em; } -.aladin-layer .aladin-input, .aladin-layer .aladin-selector { - width: 100%; -} - .aladin-selector, .aladin-input { padding: 4px; box-sizing: border-box; @@ -474,28 +472,10 @@ canvas { margin-left: 5px; } -.aladin-cb-list ul { +.aladin-list { margin: 0; padding-left: 4px; list-style: none; - /*max-width: 200px; */ - /* max-width: 150px; */ - display: flex; - flex-direction: column; - align-items: stretch; -} - -.aladin-cb-list ul li { - flex: 1 1 100%; - text-align: left; -} - -.aladin-cb-list label { - display: inline-block; -} - -.aladin-cb-list input[type="checkbox"] { - margin: 3px; } .aladin-indicatorBtn { @@ -613,27 +593,32 @@ canvas { .aladin-btn { display: inline-block; - padding: 8px; margin-bottom: 0; + margin: 0; + + padding: 8px; font-size: 12px; font-weight: normal; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; - border: 1px solid transparent; + border: 1px solid #357ebd; border-radius: 3px; color: #ffffff; background-color: #428bca; - border-color: #357ebd; - margin-right: 4px; } -.aladin-layer { - padding: 0px; - margin-bottom: 4px; - background-color: #f2f2f2; - border-radius: 2px; +.aladin-24px-icon { + padding: 0; + border: 1px solid #AEAEAE; + + width: 24px; + height: 24px; +} + +.aladin-btn:hover { + filter: brightness(105%); } .aladin-cancelBtn { @@ -646,27 +631,14 @@ canvas { border-color: #38a730; } -.aladin-selectBtn { - margin-right: 4px; - margin-left: 4px; - - background-color: #bababa; - border-color: #484848; - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAD/h4/MvwAAAAd0SU1FB+cIFxEZCysdHKoAAAEcSURBVCjPhdG/K8QBGMfxlzul7iaLXNQNus6mszDI9zrKYjDouxjFYHSTut0/wGYTgxF/gMNwFzmrLs5AyYUSCl0MJz8H7+np83x6evp8+Eko/Cm0fk5tBmQESKgoe27K0Y91j7yEEi4cCIypufsy9Ji3pShjRJe6TbemnbhrGtrkbXkyp+rBpboph6pCexowrCBpWRqdOpG2JKlguHlhQklG1S4iXj27EZdyrF85KjSOnEdnIhaN2tcukNNiyFvEP7TakHDk2pMrLODevXPXzrzYiKAisCMrjZgY0rJ2ZFWaT14Zc+vQrLhAny6hVR16rWlE0VAzrWpbSk63mnUdJi2r0/IZ9YwHRYMoyYpZccqX4XtZxe9l/eZP3e+aWVQQn/7CMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wOC0yM1QxNzoyNDo0NiswMDowMJfQN1cAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDgtMjNUMTc6MjQ6NDYrMDA6MDDmjY/rAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTA4LTIzVDE3OjI1OjExKzAwOjAw0x314AAAAABJRU5ErkJggg=='); - background-size: contain; - background-repeat: no-repeat; - background-position:center center; -} - .aladin-box-content > div { margin: 10px 0px 0px 0px; } -.aladin-btn-small { +/*.aladin-btn-small { display: inline-block; border-radius: 3px; margin-bottom: 0; - padding: 0; vertical-align: middle; cursor: pointer; border: 1px solid transparent; @@ -675,9 +647,11 @@ canvas { background-color: #428bca; border-color: #357ebd; margin-left: 2px; - min-width: 1.5em; - min-height: 1.5em; -} + + background-size: 'contain'; + background-repeat: 'no-repeat'; + background-position: 'center center'; +}*/ .aladin-button:hover { color: #ffffff; @@ -693,22 +667,41 @@ canvas { margin-bottom: 5px; } -.aladin-form-param { +.aladin-horizontal-list { display: flex; align-items: center; + list-style: none; + + padding: 0; + margin: 0; +} + +.aladin-horizontal-list > * { + vertical-align: middle; + margin-right: 2px; +} + +.aladin-form-input { + display: flex; + justify-content: flex-end; + align-items: center; margin-bottom: 7px; } -.aladin-form-param .aladin-input { - width: 100%; + +.aladin-form-input > label{ + flex: 1; } +.aladin-form-input .aladin-input { + flex: 2; +} -.aladin-form-param-group { +.aladin-form-input-group { border: 2px solid silver; padding: 5px; } -.aladin-form-param-group .aladin-form-group-header { +.aladin-form-input-group .aladin-form-group-header { display: flex; align-items: center; @@ -716,8 +709,6 @@ canvas { margin: 0; font-size: 13px; font-family: Verdana, Geneva, Tahoma, sans-serif; - - cursor: pointer; } .aladin-window-container { @@ -785,7 +776,6 @@ canvas { */ } - .aladin-popupTitle { font-weight: bold; } @@ -819,17 +809,13 @@ canvas { .aladin-layer-label { padding: 0 4px 0 4px; color: #ddd; - border-bottom-left-radius: 8px; - border-top-left-radius: 8px; - border-bottom-right-radius: 8px; - border-top-right-radius: 8px; + border-radius: 8px; cursor: pointer; user-select: none; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; - vertical-align: text-top; max-width: 150px; } @@ -860,11 +846,8 @@ canvas { background-repeat: no-repeat; background-position:center center; display: inline-block; - vertical-align: text-top; } - - .aladin-chevron { display: inline-block; width: 16px; @@ -886,14 +869,6 @@ canvas { transform: rotate(270deg); } -.aladin-layer .right-triangle:before { - content: '\25b6'; -} - -.aladin-layer .down-triangle:before { - content: '\25bc'; -} - /* *********************************************** */ /* *********************************************** */ @@ -920,7 +895,6 @@ canvas { display: flex; align-items: center; padding: 4px 8px; - cursor: pointer; position: relative; border-bottom: 1px solid #f2f2f2; } @@ -970,6 +944,27 @@ canvas { transform: translate(-100%, -100%); } +/* *********************************************** */ + +/* Tooltip */ + +.aladin-tooltip-container .aladin-tooltip { + visibility: hidden; + background-color: black; + color: #fff; + text-align: center; + padding: 4px; + border-radius: 2px; + + /* Position the tooltip text - see examples below! */ + position: absolute; + z-index: 100; +} + +/* Show the tooltip text when you mouse over the tooltip container */ +.aladin-tooltip-container:hover .aladin-tooltip { + visibility: visible; +} /* *********************************************** */ diff --git a/src/js/Aladin.js b/src/js/Aladin.js index 8f576e379..de18ecee8 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -46,7 +46,7 @@ import { ProjectionSelector } from "./gui/ProjectionSelector.js"; import { ProjectionEnum } from "./ProjectionEnum.js"; import { Stack } from "./gui/Stack.js"; import { CooGrid } from "./gui/CooGrid.js"; -import { ContextMenu } from "./gui/ContextMenu.js"; +import { ContextMenu } from "./gui/widgets/ContextMenu.js"; import { SODAQueryWindow } from "./gui/SODAQueryWindow"; import { ALEvent } from "./events/ALEvent.js"; import { Color } from './Color.js'; @@ -474,7 +474,7 @@ export let Aladin = (function () { // set right click context menu if (options.showContextMenu) { this.contextMenu = new ContextMenu(this); - this.contextMenu.attachTo(this.view.catalogCanvas, DefaultActionsForContextMenu.getDefaultActions(this)); + this.contextMenu.attachTo(DefaultActionsForContextMenu.getDefaultActions(this)); } if (options.samp) { diff --git a/src/js/GenericPointer.js b/src/js/GenericPointer.js index 12b97a264..700cc9f35 100644 --- a/src/js/GenericPointer.js +++ b/src/js/GenericPointer.js @@ -11,7 +11,7 @@ import { Utils } from './Utils'; // allow to call either Simbad or Planetary features Pointers export let GenericPointer = (function (view, e) { - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + const xymouse = Utils.relMouseCoords(e); let radec = view.wasm.screenToWorld(xymouse.x, xymouse.y); if (radec) { // sky case diff --git a/src/js/HiPSProperties.js b/src/js/HiPSProperties.js index b5fb90bea..af21c508b 100644 --- a/src/js/HiPSProperties.js +++ b/src/js/HiPSProperties.js @@ -78,8 +78,6 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) { urlOrId = Utils.getAbsoluteURL(urlOrId) urlOrId = new URL(urlOrId); - - } catch(e) { throw e; } diff --git a/src/js/Utils.ts b/src/js/Utils.ts index 4b7de23df..f17c9c1b5 100644 --- a/src/js/Utils.ts +++ b/src/js/Utils.ts @@ -38,7 +38,7 @@ Utils.HTTPS_WHITELIST = ['alasky.u-strasbg.fr', 'alaskybis.u-strasbg.fr', 'alask Utils.cssScale = undefined -Utils.relMouseCoords = function (canvas: HTMLCanvasElement, event: MouseEvent) { +Utils.relMouseCoords = function (event: MouseEvent) { if (event.offsetX) { return {x: event.offsetX, y: event.offsetY} } else { diff --git a/src/js/View.js b/src/js/View.js index 3fddfb5e0..9dc6d7c51 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -473,7 +473,7 @@ export let View = (function () { // various listeners let onDblClick = function (e) { - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + const xymouse = Utils.relMouseCoords(e); // deselect all the selected sources with Select panel view.hideSelectedObjects() @@ -506,7 +506,7 @@ export let View = (function () { e.preventDefault(); e.stopPropagation(); - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + const xymouse = Utils.relMouseCoords(e); if (e.which === 3 || e.button === 2) { view.rightClick = true; @@ -623,7 +623,7 @@ export let View = (function () { view.mustClearCatalog = true; view.dragCoo = null; - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + const xymouse = Utils.relMouseCoords(e); if (e.type === "mouseout" || e.type === "touchend" || e.type === "touchcancel") { view.updateLocation(xymouse.x, xymouse.y, true); @@ -729,7 +729,7 @@ export let View = (function () { var lastMouseMovePos = null; $(view.catalogCanvas).bind("mousemove touchmove", function (e) { e.preventDefault(); - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + const xymouse = Utils.relMouseCoords(e); if (view.rightClick) { var onRightClickMoveFunction = view.aladin.callbacksByEventName['rightClickMove']; @@ -2003,7 +2003,7 @@ export let View = (function () { // Hidden footprints are not considered let lineWidth = footprint.getLineWidth(); - footprint.setLineWidth(6.0); + footprint.setLineWidth(10.0); if (footprint.isShowing && footprint.isInStroke(ctx, this, x, y)) { closest = footprint; } diff --git a/src/js/gui/CatalogSelector.js b/src/js/gui/CatalogSelector.js index 7e6fed6f7..a42075fd0 100644 --- a/src/js/gui/CatalogSelector.js +++ b/src/js/gui/CatalogSelector.js @@ -48,7 +48,7 @@ import $ from 'jquery'; const self = this; this.mainDiv = document.createElement('div'); - this.mainDiv.classList.add('aladin-dialog', 'aladin-cb-list'); + this.mainDiv.classList.add('aladin-dialog'); this.mainDiv.style.display = 'block'; const autocompleteId = 'autocomplete-' + Utils.uuidv4(); diff --git a/src/js/gui/CooGrid.js b/src/js/gui/CooGrid.js index 247a4948c..c740bed06 100644 --- a/src/js/gui/CooGrid.js +++ b/src/js/gui/CooGrid.js @@ -43,7 +43,7 @@ this.mainDiv = document.createElement('div'); this.mainDiv.style.display = 'none'; - this.mainDiv.classList.add('aladin-box', 'aladin-layerBox', 'aladin-cb-list'); + this.mainDiv.classList.add('aladin-box', 'aladin-layerBox'); this.aladinDiv = parentDiv; parentDiv.appendChild(this.mainDiv); diff --git a/src/js/gui/HiPSLayer.js b/src/js/gui/HiPSLayer.js index 4fa139b2e..405612c08 100644 --- a/src/js/gui/HiPSLayer.js +++ b/src/js/gui/HiPSLayer.js @@ -33,6 +33,7 @@ import { ALEvent } from "../events/ALEvent.js"; import { HiPSSelector } from "./HiPSSelector.js"; import $ from 'jquery'; +import { ActionButton } from "./widgets/ActionButton.js"; export class HiPSLayer { @@ -45,33 +46,109 @@ export class HiPSLayer { // HiPS header div this.headerDiv = $( - '
' + - '
' + - '' + - '' + - '' + - '' + - '' + - '
' + + '
' + + '
' + '
' ); + let layerHeaderEl = this.headerDiv[0].querySelector(".aladin-layer-header"); + + let self = this; + + let clickOpenerBtn = new ActionButton(layerHeaderEl, { + content: "▶", + backgroundColor: '#eaeaea', + color: 'black', + info: 'Open the survey edition panel', + action(e) { + if (clickOpenerBtn.opt.content === '▶') { + clickOpenerBtn.attach({ + info: 'Close the survey edition panel', + content: '▼', + }); + self.mainDiv.slideDown(300); + } + else { + clickOpenerBtn.attach({ + info: 'Open the survey edition panel', + content: '▶', + }); + self.mainDiv.slideUp(300); + } + } + }); + + layerHeaderEl.appendChild((() => { + let selector = $(''); + return selector[0]; + })()); + + let hideLayerBtn = new ActionButton(layerHeaderEl, { + content: "👁️", + backgroundColor: '#eaeaea', + info: 'Hide the layer', + action(e) { + self.hidden = !self.hidden; + let opacitySlider = self.mainDiv.find('.opacity').eq(0); + + let newOpacity = 0.0; + if (self.hidden) { + self.lastOpacity = self.layer.getOpacity(); + hideLayerBtn.attach({content: ' '}); + } else { + newOpacity = self.lastOpacity; + hideLayerBtn.attach({content: '👁️'}); + } + // Update the opacity slider + opacitySlider.val(newOpacity); + opacitySlider.get(0).disabled = self.hidden; + + self.layer.setOpacity(newOpacity); + } + }); + + new ActionButton(layerHeaderEl, { + content: "🔍", + backgroundColor: '#eaeaea', + info: 'Search for a survey (HiPS)', + action(e) { + if (!self.hipsSelector) { + self.hipsSelector = new HiPSSelector(self.aladin.aladinDiv, (IDOrURL) => { + const layerName = self.layer.layer; + self.aladin.setOverlayImageLayer(IDOrURL, layerName); + }, self.aladin); + } + + self.hipsSelector.show(); + } + }); + + let deleteLayerBtn = new ActionButton(layerHeaderEl, { + content: "❌", + backgroundColor: '#eaeaea', + info: 'Delete this layer', + action(e) { + self.aladin.aladinDiv.dispatchEvent(new CustomEvent('remove-layer', { + detail: self.layer.layer + })); + } + }); + // Add a centered on button for images if (this.layer.subtype === "fits") { let layerSelector = this.headerDiv[0].querySelector(".aladin-layerSelection"); - layerSelector.after($('')[0]); + new ActionButton(layerSelector, { + content: "🎯", + backgroundColor: '#eaeaea', + info: 'Focus on the FITS', + action(e) { + self.layer.focusOn(); + } + }, 'afterend'); } if (this.layer.layer === "base") { - let deleteLayerBtn = this.headerDiv[0].querySelector(".aladin-delete-layer"); - deleteLayerBtn.disabled = true; - deleteLayerBtn.style.backgroundColor = 'lightgray'; - deleteLayerBtn.style.borderColor = 'gray'; - - // This is how to color emojis: - // see: https://stackoverflow.com/questions/32413731/color-for-unicode-emoji - deleteLayerBtn.style.color = 'transparent'; - deleteLayerBtn.style.textShadow = '0 0 0 gray'; + deleteLayerBtn.attach({backgroundColor: 'lightgray', disable: true}); } // HiPS main options div @@ -83,31 +160,28 @@ export class HiPSLayer { this.cmap = "native"; this.color = "#ff0000"; - this.mainDiv = $('' - ); + ); $(this.mainDiv).find('.add-layer-hips').on('click', function () { self.aladin.addNewImageLayer(); @@ -243,17 +243,17 @@ export class Stack { var svgBase64 = window.btoa(iconSvg.replace(/FILLCOLOR/g, layer.color)); str += '
  • '; str += ''; - str += ' '; + str += ' '; str += '
  • '; } str += ''; - str += ''; + str += ''; layerBox.append(str); - layerBox.find('.aladin-delete-graphic-layer').click(function() { - const layerToDelete = self.aladin.findLayerByUUID($(this).data('uuid')); - self.aladin.removeLayer(layerToDelete); + layerBox.find('.aladin-delete-graphic-layer').on('click', () => { + const layerToDelete = this.aladin.findLayerByUUID(layer.uuid); + this.aladin.removeLayer(layerToDelete); }); let addCatalogBtn = layerBox.find('.catalogue-selector'); diff --git a/src/js/gui/widgets/ActionButton.js b/src/js/gui/widgets/ActionButton.js index 0da846ab5..f77239b5d 100644 --- a/src/js/gui/widgets/ActionButton.js +++ b/src/js/gui/widgets/ActionButton.js @@ -33,12 +33,12 @@ import { Widget } from "./Widget"; *****************************************************************************/ export class ActionButton extends Widget { - constructor(target, opt, position = "beforeend") { + constructor(opt, target, position = "beforeend") { let el = document.createElement('button'); el.classList.add('aladin-btn', 'aladin-24px-icon'); // add it to the dom - super(el, target, opt, position); + super(el, opt, target, position); // add a tooltip on it this.tooltip = new Tooltip(this.el, this.opt.info); @@ -78,6 +78,22 @@ export class ActionButton extends Widget { super._show(); } -} + static create(opt, info, target) { + opt['info'] = info || undefined; + + return new ActionButton(opt, target); + } + static DEFAULT_BTN = { + 'loading': { + content: '⏳', + width: '28px', + height: '28px', + position: 'right', + backgroundColor: 'white', + borderColor: '#484848', + action(e) {} + } + } +} diff --git a/src/js/gui/widgets/Form.js b/src/js/gui/widgets/Form.js index b43b8cf1b..62917937d 100644 --- a/src/js/gui/widgets/Form.js +++ b/src/js/gui/widgets/Form.js @@ -36,22 +36,26 @@ Exemple of layout object { name: 'ID', type: 'group', - headerEl: 'htmlCode' or DOM Element object, - subInputs: [{ - label: "ID", - type: "text", - value: inputParam.get("value") - } + header: 'htmlCode' or DOM Element object, + cssStyle: {...} + subInputs: [ + { + label: "ID", + type: "text", + value: "the placeholder value..." + }, + .. + ] */ export class Form extends Widget { - constructor(target, layout, opt, position = "beforeend") { - let el = this._createInput(layout); + constructor(layout, opt, target, position = "beforeend") { + let el = Form._createInput(layout); // add it to the dom - super(el, target, opt, position); + super(el, opt, target, position); } - _createInput(layout) { + static _createInput(layout) { if (layout.type === "text" || layout.type === "number") { let inputEl = document.createElement('input'); inputEl.type = layout.type; @@ -62,11 +66,12 @@ export class Form extends Widget { } inputEl.value = layout.value; - inputEl.name = layout.label; + inputEl.name = layout.name; + inputEl.id = layout.label; let labelEl = document.createElement('label'); labelEl.textContent = layout.label; - labelEl.for = input.id; + labelEl.for = inputEl.id; let divEl = document.createElement("div"); divEl.classList.add(labelEl.textContent, "aladin-form-input"); @@ -78,16 +83,23 @@ export class Form extends Widget { } else if (layout.type === "group") { let groupEl = document.createElement('div'); groupEl.classList.add(layout.name, "aladin-form-input-group"); + for (const property in layout.cssStyle) { + groupEl.style[property] = layout.cssStyle[property]; + } if (layout.header instanceof Element) { groupEl.innerHTML = '
    '; groupEl.firstChild.appendChild(layout.header); + } else if (layout.header instanceof Widget) { + let el = layout.header.element(); + groupEl.innerHTML = '
    '; + groupEl.firstChild.appendChild(el); } else { - groupEl.innerHTML = '
    ' + input.header + '
    '; + groupEl.innerHTML = '
    ' + layout.header + '
    '; } layout.subInputs.forEach((subInput) => { - let inputEl = this._createInput(subInput) + let inputEl = Form._createInput(subInput) groupEl.appendChild(inputEl); }); @@ -95,6 +107,28 @@ export class Form extends Widget { } } + values() { + let inputs = this.el.querySelectorAll('.aladin-input'); + + let values = {}; + for (let input of inputs) { + values[input.name] = input.value; + } + + return values; + } + + set(name, value) { + let inputs = this.el.querySelectorAll('.aladin-input'); + for (let input of inputs) { + if (input.name === name) { + input.value = value; + + return; + } + } + } + _show() { super._show(); } diff --git a/src/js/gui/widgets/Tooltip.js b/src/js/gui/widgets/Tooltip.js index 3023169d8..1b7a82b3d 100644 --- a/src/js/gui/widgets/Tooltip.js +++ b/src/js/gui/widgets/Tooltip.js @@ -39,26 +39,36 @@ export class Tooltip { let el = document.createElement('span'); el.classList.add('aladin-tooltip'); + let targetParent = target.parentNode; + // Insert it into the DOM tree let wrapperEl = document.createElement('div'); wrapperEl.classList.add('aladin-tooltip-container'); - let targetParent = target.parentNode; - let targetIndex = Array.prototype.indexOf.call(targetParent.children, target); - - let element = targetParent.removeChild(target); + if (targetParent) { + let targetIndex = Array.prototype.indexOf.call(targetParent.children, target); + targetParent.removeChild(target); - wrapperEl.appendChild(element); - wrapperEl.appendChild(el); + wrapperEl.appendChild(target); + wrapperEl.appendChild(el); - targetParent.insertChildAtIndex(wrapperEl, targetIndex); + targetParent.insertChildAtIndex(wrapperEl, targetIndex); + } else { + wrapperEl.appendChild(target); + wrapperEl.appendChild(el); + } - this.el = el; + this.el = wrapperEl; + this.innerElement = target; this._show(innerHTML) } + innerElement() { + return this.innerElement; + } + _show(innerHTML) { - this.el.innerHTML = innerHTML; + this.el.querySelector('.aladin-tooltip').innerHTML = innerHTML; } attach(innerHTML) { diff --git a/src/js/gui/widgets/Widget.js b/src/js/gui/widgets/Widget.js index 53945c5f7..cc8dfbcf0 100644 --- a/src/js/gui/widgets/Widget.js +++ b/src/js/gui/widgets/Widget.js @@ -17,9 +17,6 @@ // along with Aladin Lite. // -import { Tooltip } from "./Tooltip"; - - /****************************************************************************** * Aladin Lite project * @@ -33,15 +30,32 @@ import { Tooltip } from "./Tooltip"; *****************************************************************************/ export class Widget { - constructor(el, target, opt, position = 'beforeend') { + constructor(el, opt, target, position = 'beforeend') { this.opt = opt; - this.el = el; - target.insertAdjacentElement(position, el); + if (target) { + if (target instanceof Widget) { + target = target.element(); + } + let insertedEl = target.insertAdjacentElement(position, el); + if (insertedEl) { + el = insertedEl; + } + } + + this.el = el; this._show(); } + element() { + if (this.tooltip) { + return this.tooltip.el; + } else { + return this.el; + } + } + _show() { // CSS style elements for (const property in this.opt) { @@ -54,4 +68,8 @@ export class Widget { this._show(); } + + remove() { + this.el.remove() + } } diff --git a/src/js/gui/widgets/layout/Horizontal.js b/src/js/gui/widgets/layout/Horizontal.js new file mode 100644 index 000000000..f04e3671d --- /dev/null +++ b/src/js/gui/widgets/layout/Horizontal.js @@ -0,0 +1,61 @@ +// Copyright 2023 - 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 { Widget } from "../Widget"; + +/****************************************************************************** + * Aladin Lite project + * + * File gui/widgets/layout/Horizontal.js + * + * A layout grouping widgets horizontaly + * + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export class HorizontalLayout extends Widget { + constructor(widgets, opt, target, position = "beforeend") { + let el = document.createElement('div'); + el.classList.add('aladin-horizontal-list'); + + let node; + for(let widget of widgets) { + if (widget instanceof Element) { + node = widget; + } else if (widget instanceof Widget) { + node = widget.element(); + } else { + const placeholder = document.createElement("div"); + placeholder.innerHTML = widget; + node = placeholder.firstElementChild; + } + + el.appendChild(node); + } + + // add it to the dom + super(el, opt, target, position); + } + + _show() { + super._show(); + } +} diff --git a/src/js/vo/Datalink.js b/src/js/vo/Datalink.js index ff903c4f8..fb3deb184 100644 --- a/src/js/vo/Datalink.js +++ b/src/js/vo/Datalink.js @@ -31,6 +31,7 @@ import { VOTable } from "./VOTable.js"; import { Utils } from './../Utils'; +import { ActionButton } from "../gui/widgets/ActionButton.js"; export let Datalink = (function() { @@ -40,7 +41,6 @@ export let Datalink = (function() { }; Datalink.prototype.handleActions = function(obscoreRow, aladinInstance) { - const url = obscoreRow["access_url"]; VOTable.parse( url, @@ -59,6 +59,16 @@ export let Datalink = (function() { data[key] = row[field.idx]; } + if (data['semantics'] === '#cutout') { + data['service_def'] = new ActionButton({ + content: '📡', + backgroundColor: 'white', + borderColor: '#484848', + info: 'Open the cutout service form', + action(e) {} + }, aladinInstance.measurementTable.element).element(); + } + measures.push({data: data}) }) @@ -74,53 +84,16 @@ export let Datalink = (function() { if (service) { aladinInstance.sodaQueryWindow.hide(); aladinInstance.sodaQueryWindow.setParams(this.SODAServerParams); - aladinInstance.sodaQueryWindow.show((baseUrl, SODAParams) => { - let url = new URL(baseUrl) - SODAParams.forEach((param) => { - let value; - if (Array.isArray(param.value)) { - value = param.value.join(' '); - } else { - value = param.value; - } - url.searchParams.append(param.name, value); - }); - - let spinnerEl = document.createElement('div'); - spinnerEl.classList.add("aladin-spinner"); - spinnerEl.innerText = "fetching..."; - - aladinInstance.sodaQueryWindow.mainEl.querySelector(".submit") - .appendChild(spinnerEl); - - let removeSpinner = () => { - aladinInstance.sodaQueryWindow.mainEl.querySelector(".aladin-spinner").remove(); - }; - - let name = url.searchParams.toString(); - // Tackle cors problems - Utils.loadFromUrls([url, Utils.handleCORSNotSameOrigin(url)], {timeout: 30000}) - .then((response) => response.blob()) - .then((blob) => { - const url = URL.createObjectURL(blob); - try { - let image = aladinInstance.createImageFITS(url, name); - aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) - } catch(e) { - throw('Fail to interpret ' + url + ' as a fits file') - } - }) - .catch((e) => { - window.alert(e) - }) - .finally(() => { - removeSpinner(); - }) - }); + aladinInstance.sodaQueryWindow.show(aladinInstance); } }, 'access_url': (row) => { let url = row['access_url']; + console.log(url) + if (url === '--') { + return; + } + let contentType = row['content_type']; let contentQualifier = row['content_qualifier']; @@ -166,7 +139,7 @@ export let Datalink = (function() { } } - aladinInstance.measurementTable.showMeasurement([datalinkTable], { save: true }); + aladinInstance.measurementTable.showMeasurement([datalinkTable]); } else { // Try to parse a SODA service descriptor resource let SODAServerParams = VOTable.parseSODAServiceRsc(rsc); @@ -175,9 +148,11 @@ export let Datalink = (function() { // Try to populate the SODA form fields with obscore values let populateSODAFields = (SODAParams) => { - for (const inputParam of SODAParams.inputParams) { + for (const name in SODAParams.inputParams) { + let inputParam = SODAParams.inputParams[name]; + if (inputParam.type === "group") { - for (const param of inputParam.value) { + for (const param of inputParam.subInputs) { if (param.value) { continue; } @@ -203,10 +178,14 @@ export let Datalink = (function() { VOTable.parse(this.SODAServerParams.baseUrl, (rsc) => { const SODAServerDesc = VOTable.parseSODAServiceRsc(rsc); - for (const inputParam of SODAServerDesc.inputParams) { - const inputParamAlreadyDefined = this.SODAServerParams.inputParams.some((inputParamFromDatalink) => inputParamFromDatalink.name === inputParam.name); - if (!inputParamAlreadyDefined) { - this.SODAServerParams.inputParams.push(inputParam); + if (SODAServerDesc) { + console.log(rsc, "soda server", SODAServerDesc) + + for (const name in SODAServerDesc.inputParams) { + let inputParam = SODAServerDesc.inputParams[name]; + if (!this.SODAServerParams.inputParams[name]) { + this.SODAServerParams.inputParams[name] = inputParam; + } } } diff --git a/src/js/vo/VOTable.js b/src/js/vo/VOTable.js index 00cb3fb61..e60523420 100644 --- a/src/js/vo/VOTable.js +++ b/src/js/vo/VOTable.js @@ -124,7 +124,7 @@ export let VOTable = (function() { if (elems) { let accessUrl; - let inputParams = []; + let inputParams = {}; elems.forEach((elem) => { if (elem instanceof Map) { @@ -148,111 +148,103 @@ export let VOTable = (function() { let values; switch (name) { case 'ID': - inputParams.push({ + inputParams['ID'] = { name: 'ID', type: 'group', description: inputParam.get("description"), - value: [{ - name: "ID", + subInputs: [{ + label: "ID", + name: 'ID', type: "text", value: inputParam.get("value") }], - }) + } break; case 'CIRCLE': if (inputParam.get("values")) { values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); } - inputParams.push({ + inputParams['Circle'] = { name: 'CIRCLE', type: 'group', description: inputParam.get("description"), - value: [{ + subInputs: [{ name: 'ra', + label: 'ra[' + utype + ']', type: 'number', - maxVal: values && values[0], value: values && values[0], - utype: utype }, { name: 'dec', + label: 'dec[' + utype + ']', type: 'number', - maxVal: values && values[1], value: values && values[1], - utype: utype }, { name: 'rad', + label: 'rad[' + utype + ']', type: 'number', - maxVal: values && values[2], value: values && values[2], - utype: utype }] - }); + }; break; case 'BAND': if (inputParam.get("values")) { values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); } - - inputParams.push({ + + inputParams['Band'] = { name: 'BAND', type: 'group', description: inputParam.get("description"), - value: [{ + subInputs: [{ name: 'fmin', + label: 'fmin[' + utype + ']', type: 'number', - maxVal: values && values[0], value: values && values[0], - utype: utype }, { name: 'fmax', + label: 'fmax[' + utype + ']', type: 'number', - maxVal: values && values[1], value: values && values[1], - utype: utype }] - }); + }; break; case 'RANGE': if (inputParam.get("values")) { values = inputParam.get("values")["max"]["value"].split(" ").map((v) => {return +v;}); } - inputParams.push({ + inputParams['Range'] = { name: 'RANGE', type: 'group', description: inputParam.get("description"), - value: [{ - name: 'raMin', + subInputs: [{ + name: 'ramin', + label: 'ramin[' + utype + ']', type: 'number', - maxVal: values && values[0], value: values && values[0], - utype: utype }, { - name: 'raMax', + name: 'ramax', + label: 'ramax[' + utype + ']', type: 'number', - maxVal: values && values[1], value: values && values[1], - utype: utype }, { - name: 'decMin', + name: 'decmin', + label: 'decmin[' + utype + ']', type: 'number', - maxVal: values && values[2], value: values && values[2], - utype: utype }, { - name: 'decMax', + name: 'decmax', + label: 'decmax[' + utype + ']', type: 'number', - maxVal: values && values[3], value: values && values[3], - utype: utype }] - }); + }; default: break; } From b6d26c94fa689ee96d0ddfe14d1672be2c60a8b4 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Thu, 14 Sep 2023 04:07:54 +0200 Subject: [PATCH 06/32] allow to overwrite the display of specific fields in MeasurementTable.js --- src/css/aladin.css | 13 ++- src/js/A.js | 5 +- src/js/Catalog.js | 34 +++++-- src/js/MeasurementTable.js | 78 ++++++--------- src/js/Source.js | 22 ++--- src/js/View.js | 23 ++--- src/js/gui/widgets/ActionButton.js | 4 +- src/js/vo/Datalink.js | 153 ++++++++++++++++------------- src/js/vo/ObsCore.js | 122 ++++++++++++++--------- src/js/vo/VOTable.js | 12 +-- 10 files changed, 251 insertions(+), 215 deletions(-) diff --git a/src/css/aladin.css b/src/css/aladin.css index e52d63a9e..d561ee38a 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -99,7 +99,7 @@ .aladin-measurement-div { z-index: 77; - width: 100%; + max-width: 100%; position: absolute; bottom: 20px; font-family: monospace; @@ -114,6 +114,7 @@ -ms-overflow-style: none; scrollbar-width: none; + position: relative; overflow-y: scroll; overscroll-behavior-x: none; background-color: rgba(255, 255, 255, 0.9); @@ -148,6 +149,7 @@ .aladin-measurement-div table thead { position: sticky; + width: 100%; top: 0; background-color: #fff; color: #000; @@ -177,7 +179,7 @@ .aladin-measurement-div table td .aladin-href-td-container { border: 1px solid #d2d2d2; border-radius: 3px; - padding: 0.6em; + padding: 0.4em; white-space: nowrap; overflow: hidden; @@ -198,8 +200,9 @@ } 80%, 100% { - transform: translateX(-100%); - left: 100%; + /* the max width is 150px and a padding of 0.8em is added for href link */ + transform: translateX(calc(-100% + 150px - 0.8em)); + left: calc(100% - 150px + 0.8em); } } @@ -993,7 +996,7 @@ canvas { /* Position the tooltip text - see examples below! */ position: absolute; - z-index: 100; + z-index: 10000; } /* Show the tooltip text when you mouse over the tooltip container */ diff --git a/src/js/A.js b/src/js/A.js index 7d5c232fa..7dcaafdf9 100644 --- a/src/js/A.js +++ b/src/js/A.js @@ -180,7 +180,8 @@ A.MOCFromJSON = function (jsonMOC, options) { A.catalogFromURL = function (url, options, successCallback, errorCallback, useProxy) { var catalog = A.catalog(options); - const processVOTable = function (sources, footprints, fields) { + const processVOTable = function (table) { + let {sources, footprints, fields, type} = table; catalog.setFields(fields); if (catalog.isObsCore()) { @@ -198,7 +199,7 @@ A.catalogFromURL = function (url, options, successCallback, errorCallback, usePr // Even if the votable is not a proper ObsCore one, try to see if specific columns are given // e.g. access_format and access_url - ObsCore.handleActions(catalog); + //ObsCore.handleActions(catalog); }; if (useProxy !== undefined) { diff --git a/src/js/Catalog.js b/src/js/Catalog.js index 774834c76..290faa345 100644 --- a/src/js/Catalog.js +++ b/src/js/Catalog.js @@ -36,6 +36,7 @@ import { Coo } from "./libs/astro/coo.js"; import { VOTable } from "./vo/VOTable.js"; import { ALEvent } from "./events/ALEvent.js"; import { Footprint } from "./Footprint.js"; +import { ObsCore } from "./vo/ObsCore.js"; import A from "./A.js"; import $ from 'jquery'; @@ -65,7 +66,7 @@ export let Catalog = (function() { // allows for filtering of sources this.filterFn = options.filter || undefined; // TODO: do the same for catalog - this.fieldsClickedActions = {}; // callbacks when the user clicks on a cell in the measurement table associated + this.showFieldCallback = {}; // callbacks when the user clicks on a cell in the measurement table associated this.fields = undefined; this.indexationNorder = 5; // à quel niveau indexe-t-on les sources @@ -274,6 +275,8 @@ export let Catalog = (function() { fields.forEach((field) => { let key = field.name ? field.name : field.id; + key = key.split(' ').join('_') + let nameField; if (fieldIdx == raFieldIdx) { nameField = 'ra'; @@ -283,6 +286,7 @@ export let Catalog = (function() { nameField = key; } + // remove the space character parsedFields[nameField] = { name: key, idx: fieldIdx, @@ -300,7 +304,18 @@ export let Catalog = (function() { VOTable.parse( url, (rsc) => { - let { fields, rows } = VOTable.parseTableRsc(rsc, raField, decField) + let { fields, rows } = VOTable.parseTableRsc(rsc) + let type; + try { + fields = ObsCore.parseFields(fields); + //fields.subtype = "ObsCore"; + type = 'ObsCore'; + } catch(e) { + // It is not an ObsCore table + fields = Catalog.parseFields(fields, raField, decField); + type = 'sources'; + } + let sources = []; let footprints = []; @@ -357,7 +372,12 @@ export let Catalog = (function() { }) if (successCallback) { - successCallback(sources, footprints, fields); + successCallback({ + sources: sources, + footprints: footprints, + fields: fields, + type: type + }); } }, errorCallback, @@ -446,12 +466,8 @@ export let Catalog = (function() { }; /// This add a callback when the user clicks on the field column in the measurementTable - Catalog.prototype.addFieldClickCallback = function(field, callback) { - this.fieldsClickedActions[field] = callback; - }; - - Catalog.prototype.isObsCore = function() { - return this.fields && this.fields.subtype === "ObsCore"; + Catalog.prototype.addShowFieldCallback = function(field, callback) { + this.showFieldCallback[field] = callback; }; // API diff --git a/src/js/MeasurementTable.js b/src/js/MeasurementTable.js index 6bb1562a4..f47112dc7 100644 --- a/src/js/MeasurementTable.js +++ b/src/js/MeasurementTable.js @@ -31,6 +31,7 @@ *****************************************************************************/ import { Color } from "./Color.js" +import { ActionButton } from "./gui/widgets/ActionButton.js"; export let MeasurementTable = (function() { @@ -45,60 +46,44 @@ export let MeasurementTable = (function() { aladinLiteDiv.appendChild(this.element); } - MeasurementTable.prototype.updateRows = function() { + MeasurementTable.prototype.updateTableBody = function() { let tbody = this.element.querySelector('tbody'); - - tbody.innerHTML = ""; + tbody.innerHTML = ''; let table = this.tables[this.curTableIdx]; - let result = ''; table["rows"].forEach((row) => { - result += '' + let trEl = document.createElement('tr'); + for (let key in row.data) { // check the type here - let val = row.data[key] || '--'; - - if (val instanceof Element) { - val = val.outerHTML; - } - - result += '' - try { - let url = new URL(val); - let link = ''; - result += link; - } catch(e) { - result += val + let tdEl = document.createElement('td'); + tdEl.classList.add(key); + + if (table.showCallback && table.showCallback[key]) { + let showFieldCallback = table.showCallback[key]; + + let el = showFieldCallback(row.data); + if (el instanceof Element) { + tdEl.appendChild(el); + } else { + tdEl.innerHTML = el; + } + } else { + let val = row.data[key] || '--'; + tdEl.innerText = val; } - result += '' + trEl.appendChild(tdEl); } - result += ''; - }); - tbody.innerHTML = result; - - if (table["fieldsClickedActions"]) { - for (let key in table["fieldsClickedActions"]) { - tbody.querySelectorAll("." + key).forEach(function(e, index) { - e.addEventListener('click', (e) => { - let callback = table["fieldsClickedActions"][key]; - let fieldClickedVal = table["rows"][index].data[key]; - if (fieldClickedVal && fieldClickedVal !== '--') { - callback(table["rows"][index].data) - } - - e.preventDefault(); - }, false) - }) - } - } + tbody.appendChild(trEl); + }); } // show measurement associated with a given source - MeasurementTable.prototype.showMeasurement = function(tables, options) { + MeasurementTable.prototype.showMeasurement = function(tables) { if (tables.length === 0) { return; } @@ -126,12 +111,11 @@ export let MeasurementTable = (function() { const thead = MeasurementTable.createTableHeader(table); // table body creation const tbody = document.createElement('tbody'); - tableElement.appendChild(thead); tableElement.appendChild(tbody); - this.element.appendChild(tableElement); - this.updateRows(); + this.element.appendChild(tableElement); + this.updateTableBody(); this.show(); } @@ -154,8 +138,6 @@ export let MeasurementTable = (function() { tabButtonElement.style.whiteSpace = 'nowrap'; tabButtonElement.style.maxWidth = '20%'; - tabButtonElement - tabButtonElement.addEventListener( 'click', () => { @@ -168,7 +150,7 @@ export let MeasurementTable = (function() { // replace the old header with the one of the current table thead.parentNode.replaceChild(MeasurementTable.createTableHeader(table), thead); - self.updateRows() + self.updateTableBody() } ,false ); @@ -194,9 +176,11 @@ export let MeasurementTable = (function() { var content = ''; for (let [_, field] of Object.entries(table["fields"])) { - content += '' + field.name + ''; + if (field.name) { + content += '' + field.name + ''; + } } - content += ''; + content += ''; theadElement.innerHTML = content; diff --git a/src/js/Source.js b/src/js/Source.js index 6132ad268..22d1ce006 100644 --- a/src/js/Source.js +++ b/src/js/Source.js @@ -28,6 +28,11 @@ * *****************************************************************************/ +import { ActionButton } from "./gui/widgets/ActionButton"; +import { Datalink } from "./vo/Datalink"; +import { Utils } from "./Utils"; +import { ObsCore } from "./vo/ObsCore"; + export let Source = (function() { // constructor let Source = function(ra, dec, data, options) { @@ -101,27 +106,22 @@ export let Source = (function() { if (this.catalog && this.catalog.onClick) { var view = this.catalog.view; - if (this.catalog.onClick=='showTable') { + if (this.catalog.onClick == 'showTable') { this.select(); + view.aladin.measurementTable.hide(); + let singleSourceTable = { 'rows': [this], 'fields': this.catalog.fields, - 'fieldsClickedActions': this.catalog.fieldsClickedActions, + 'showCallback': ObsCore.SHOW_CALLBACKS(view.aladin), 'name': this.catalog.name, 'color': this.catalog.color }; - let options = {}; - //if (this.catalog.isObsCore && this.catalog.isObsCore()) { - // If the source is obscore, save the table state inside the measurement table - // This is used to go back from a possible datalink table to the obscore one - options["save"] = true; - //} - view.aladin.measurementTable.hide(); - view.aladin.measurementTable.showMeasurement([singleSourceTable], options); + view.aladin.measurementTable.showMeasurement([singleSourceTable]); } - else if (this.catalog.onClick=='showPopup') { + else if (this.catalog.onClick == 'showPopup') { view.popup.setTitle('

    '); var m = '
    '; diff --git a/src/js/View.js b/src/js/View.js index c52070364..dc511fefb 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -49,6 +49,8 @@ import { ColorCfg } from "./ColorCfg.js"; import { Footprint } from "./Footprint.js"; import { Selector } from "./Selector.js"; import $ from 'jquery'; +import { ActionButton } from "./gui/widgets/ActionButton.js"; +import { ObsCore } from "./vo/ObsCore.js"; export let View = (function () { @@ -1245,33 +1247,28 @@ export let View = (function () { objListPerCatalog.forEach((obj) => obj.select()) }); - let saveTable = false; - let tables = this.selectedObjects.map((objList) => { // Get the catalog containing that list of objects let catalog = objList[0].getCatalog(); - let rows = objList.map((o) => { + let source; + let sources = objList.map((o) => { if (o instanceof Footprint) { - return o.source; + source = o.source; } else { - return o; + source = o; } + + return source; }); let table = { 'name': catalog.name, 'color': catalog.color, - 'rows': rows, + 'rows': sources, 'fields': catalog.fields, - 'fieldsClickedActions': catalog.fieldsClickedActions, + 'showCallback': ObsCore.SHOW_CALLBACKS(this.aladin) }; - if (catalog.isObsCore && catalog.isObsCore()) { - // If the source is obscore, save the table state inside the measurement table - // This is used to go back from a possible datalink table to the obscore one - saveTable = true; - } - return table; }) diff --git a/src/js/gui/widgets/ActionButton.js b/src/js/gui/widgets/ActionButton.js index f77239b5d..6360bc4e0 100644 --- a/src/js/gui/widgets/ActionButton.js +++ b/src/js/gui/widgets/ActionButton.js @@ -41,7 +41,9 @@ export class ActionButton extends Widget { super(el, opt, target, position); // add a tooltip on it - this.tooltip = new Tooltip(this.el, this.opt.info); + if (this.opt.info) { + this.tooltip = new Tooltip(this.el, this.opt.info); + } } _show() { diff --git a/src/js/vo/Datalink.js b/src/js/vo/Datalink.js index fb3deb184..583eb7469 100644 --- a/src/js/vo/Datalink.js +++ b/src/js/vo/Datalink.js @@ -32,6 +32,7 @@ import { VOTable } from "./VOTable.js"; import { Utils } from './../Utils'; import { ActionButton } from "../gui/widgets/ActionButton.js"; +import { Catalog } from "../Catalog.js"; export let Datalink = (function() { @@ -48,6 +49,8 @@ export let Datalink = (function() { let table = VOTable.parseTableRsc(rsc); if (table && table.fields && table.rows) { + table.fields = Catalog.parseFields(table.fields); + // Get the fields and the rows let measures = []; const { fields, rows } = table; @@ -59,82 +62,99 @@ export let Datalink = (function() { data[key] = row[field.idx]; } - if (data['semantics'] === '#cutout') { - data['service_def'] = new ActionButton({ - content: '📡', - backgroundColor: 'white', - borderColor: '#484848', - info: 'Open the cutout service form', - action(e) {} - }, aladinInstance.measurementTable.element).element(); - } - measures.push({data: data}) }) - + let self = this; let datalinkTable = { 'name': 'Datalink:' + url, 'color': 'purple', 'rows': measures, 'fields': fields, - 'fieldsClickedActions': { - 'service_def': (row) => { - const service = row['service_def']; - - if (service) { - aladinInstance.sodaQueryWindow.hide(); - aladinInstance.sodaQueryWindow.setParams(this.SODAServerParams); - aladinInstance.sodaQueryWindow.show(aladinInstance); + 'showCallback': { + 'service_def': (data) => { + const service = data['service_def']; + + if (data['semantics'] === "#cutout") { + return new ActionButton({ + content: '📡', + backgroundColor: 'white', + borderColor: '#484848', + info: 'Open the cutout service form', + action(e) { + aladinInstance.sodaQueryWindow.hide(); + aladinInstance.sodaQueryWindow.setParams(self.SODAServerParams); + aladinInstance.sodaQueryWindow.show(aladinInstance); + } + }).element(); + } else { + return service || '--'; } }, - 'access_url': (row) => { - let url = row['access_url']; - console.log(url) - if (url === '--') { - return; + 'access_url': (data) => { + let url = data['access_url']; + + let accessUrlEl = document.createElement('div'); + + if (url) { + let contentType = data['content_type']; + let contentQualifier = data['content_qualifier']; + + try { + // Just create a URL object to verify it is a good url + // If not, it will throw an exception + let _ = new URL(url); + accessUrlEl.classList.add('aladin-href-td-container'); + accessUrlEl.innerHTML = '' + url + ''; + + accessUrlEl.addEventListener('click', (e) => { + + let processImageFitsClick = () => { + var successCallback = ((ra, dec, fov, _) => { + aladinInstance.gotoRaDec(ra, dec); + aladinInstance.setFoV(fov); + }); + + let image = aladinInstance.createImageFITS(url, url, {}, successCallback); + aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) + }; + + switch (contentType) { + case 'application/hips': + // Clic on a HiPS + let survey = aladinInstance.newImageSurvey(url); + aladinInstance.setOverlayImageLayer(survey, Utils.uuidv4()) + break; + // Any generic FITS file + case 'application/fits': + if (contentQualifier === "cube") { + // fits cube + console.warn("Cube not handled, only first slice downloaded") + } + + processImageFitsClick(); + break; + case 'image/fits': + if (contentQualifier === "cube") { + // fits cube + console.warn("Cube not handled, only first slice downloaded") + } + + processImageFitsClick(); + break; + default: + // When all has been done, download what's under the link + //Utils.download(url); + break; + } + }); + } catch(e) { + accessUrlEl.innerText = '--'; + } + } else { + accessUrlEl.innerText = '--'; } - let contentType = row['content_type']; - let contentQualifier = row['content_qualifier']; - - let processImageFitsClick = () => { - var successCallback = ((ra, dec, fov, _) => { - aladinInstance.gotoRaDec(ra, dec); - aladinInstance.setFoV(fov); - }); - - let image = aladinInstance.createImageFITS(url, url, {}, successCallback); - aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) - }; - - switch (contentType) { - case 'application/hips': - // Clic on a HiPS - let survey = aladinInstance.newImageSurvey(url); - aladinInstance.setOverlayImageLayer(survey, Utils.uuidv4()) - break; - // Any generic FITS file - case 'application/fits': - if (contentQualifier === "cube") { - // fits cube - console.warn("Cube not handled, only first slice downloaded") - } - - processImageFitsClick(); - break; - case 'image/fits': - if (contentQualifier === "cube") { - // fits cube - console.warn("Cube not handled, only first slice downloaded") - } - - processImageFitsClick(); - break; - default: - // When all has been done, download what's under the link - Utils.download(url); - break; - } + return accessUrlEl; } } } @@ -143,6 +163,7 @@ export let Datalink = (function() { } else { // Try to parse a SODA service descriptor resource let SODAServerParams = VOTable.parseSODAServiceRsc(rsc); + if (SODAServerParams) { this.SODAServerParams = SODAServerParams; @@ -179,8 +200,6 @@ export let Datalink = (function() { const SODAServerDesc = VOTable.parseSODAServiceRsc(rsc); if (SODAServerDesc) { - console.log(rsc, "soda server", SODAServerDesc) - for (const name in SODAServerDesc.inputParams) { let inputParam = SODAServerDesc.inputParams[name]; if (!this.SODAServerParams.inputParams[name]) { diff --git a/src/js/vo/ObsCore.js b/src/js/vo/ObsCore.js index 8438fb11e..da06038b3 100644 --- a/src/js/vo/ObsCore.js +++ b/src/js/vo/ObsCore.js @@ -32,6 +32,8 @@ import { Datalink } from "./Datalink.js"; import { Utils } from '../Utils'; + import { ActionButton } from "../gui/widgets/ActionButton.js"; + export let ObsCore = (function() { // dict of mandatory ObsCore fields @@ -188,57 +190,79 @@ throw 'Mandatory field ' + nameField + ' not found'; }; - ObsCore.handleActions = function(cat) { - // Get the ObsCore fields - let fields = cat.fields; - - cat.addFieldClickCallback("access_url", (row) => { - // And the aladin lite instance - let aladinInstance = cat.view.aladin; - - let accessUrlFieldName = fields["access_url"].name; - let accessFormatFieldName = fields["access_format"].name; - - let url = row[accessUrlFieldName]; - let accessFormat = row[accessFormatFieldName]; - - let processImageFitsClick = () => { - var obsId = fields['obs_id'] && row[fields['obs_id'].name]; - var name = obsId || url; - var successCallback = ((ra, dec, fov, _) => { - aladinInstance.gotoRaDec(ra, dec); - aladinInstance.setFoV(fov); - }); - - let image = aladinInstance.createImageFITS(url, name, {}, successCallback); - aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) - }; + ObsCore.SHOW_CALLBACKS = function(aladinInstance) { + return { + "access_url": (data) => { + let url = data['access_url']; + let format = data['access_format']; + + let accessUrlEl = document.createElement('div'); + try { + let _ = new URL(url); + accessUrlEl.classList.add('aladin-href-td-container'); + + accessUrlEl.innerHTML = '' + url + ''; + + accessUrlEl.addEventListener('click', (e) => { + e.preventDefault(); + let processImageFitsClick = () => { + var name = data['obs_id'] || url; + var successCallback = ((ra, dec, fov, _) => { + aladinInstance.gotoRaDec(ra, dec); + aladinInstance.setFoV(fov); + }); + + let image = aladinInstance.createImageFITS(url, name, {}, successCallback); + + aladinInstance.setOverlayImageLayer(image, Utils.uuidv4()) + }; + + switch (format) { + // A datalink response containing links to datasets or services attached to the current dataset + case 'application/x-votable+xml;content=datalink': + new Datalink().handleActions(data, aladinInstance); + break; + // Any multidimensional regularly sampled FITS image or cube + case 'image/fits': + processImageFitsClick(); + break; + // Any generic FITS file + case 'application/fits': + processImageFitsClick(); + break; + // A FITS multi-extension file (multiple extensions) + case 'application/x-fits-mef': + processImageFitsClick(); + break; + default: + console.warn("Access format ", format, " not yet implemented or not recognized. Download the file triggered") + Utils.download(url) + break; + } + }); + } catch(e) { + accessUrlEl.innerText = '--'; + } - switch (accessFormat) { - // A datalink response containing links to datasets or services attached to the current dataset - case 'application/x-votable+xml;content=datalink': - //Datalink.handleActions(url) - new Datalink().handleActions(row, aladinInstance); - break; - // Any multidimensional regularly sampled FITS image or cube - case 'image/fits': - processImageFitsClick(); - break; - // Any generic FITS file - case 'application/fits': - processImageFitsClick(); - break; - // A FITS multi-extension file (multiple extensions) - case 'application/x-fits-mef': - processImageFitsClick(); - break; - default: - console.warn("Access format ", accessFormat, " not yet implemented or not recognized. Download the file triggered") - Utils.download(url) - break; + return accessUrlEl; + }, + 'access_format': (data) => { + let accessFormat = data['access_format']; + + if (accessFormat && accessFormat.includes('datalink')) { + return new ActionButton({ + content: '🔗', + backgroundColor: 'white', + borderColor: '#484848', + info: accessFormat, + action(e) {} + }).element(); + } else { + return accessFormat; + } } - }); - } + } + }; return ObsCore; })(); diff --git a/src/js/vo/VOTable.js b/src/js/vo/VOTable.js index e60523420..1e49ca8eb 100644 --- a/src/js/vo/VOTable.js +++ b/src/js/vo/VOTable.js @@ -26,8 +26,6 @@ * *****************************************************************************/ import { ALEvent } from "../events/ALEvent.js"; -import { Catalog } from "../Catalog.js"; -import { ObsCore } from "./ObsCore.js"; import { Utils } from "./../Utils"; export let VOTable = (function() { @@ -70,7 +68,7 @@ export let VOTable = (function() { }) }; - VOTable.parseTableRsc = function (rsc, raField, decField) { + VOTable.parseTableRsc = function (rsc) { let tables = rsc.get("tables") if (tables) { // take only the first table @@ -86,14 +84,6 @@ export let VOTable = (function() { return Object.fromEntries(field); }); - try { - fields = ObsCore.parseFields(fields); - fields.subtype = "ObsCore"; - } catch(e) { - // It is not an ObsCore table - fields = Catalog.parseFields(fields, raField, decField); - } - let data = table.get("data"); if (data) { let rows; From 05c322865d8193265c605fef254f8546daafef75 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Wed, 20 Sep 2023 19:33:09 +0200 Subject: [PATCH 07/32] swap 2 dom elements code --- src/js/gui/widgets/Utils.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/js/gui/widgets/Utils.js b/src/js/gui/widgets/Utils.js index a130feee3..069eac11a 100644 --- a/src/js/gui/widgets/Utils.js +++ b/src/js/gui/widgets/Utils.js @@ -4,4 +4,15 @@ Element.prototype.insertChildAtIndex = function(child, index) { } else { this.insertBefore(child, this.children[index]) } -}; \ No newline at end of file +}; + +Element.prototype.swap = function (node) { + const parent = this.parentNode; + const sibling = this.nextSibling === node ? this : this.nextSibling; + + // Move `this` to before the `node` + node.parentNode.insertBefore(this, node); + + // Move `node` to before the sibling of `this` + parent.insertBefore(node, sibling); +}; From a5760204c796df4f7e3917712616273c668e0a14 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Wed, 27 Sep 2023 17:49:00 +0200 Subject: [PATCH 08/32] UI: fix delete overlay --- src/js/gui/CatalogSelector.js | 8 +++----- src/js/gui/HiPSSelector.js | 2 +- src/js/gui/Stack.js | 8 +++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/gui/CatalogSelector.js b/src/js/gui/CatalogSelector.js index a42075fd0..533ff7680 100644 --- a/src/js/gui/CatalogSelector.js +++ b/src/js/gui/CatalogSelector.js @@ -62,11 +62,9 @@ import $ from 'jquery'; '
    around view center
    ' + '
    Limit to sources
    ' + '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + + '
    ' + + '' + + '' + '
    ' + '
    ' + '
    By VOTable URL
    ' + diff --git a/src/js/gui/HiPSSelector.js b/src/js/gui/HiPSSelector.js index 7cbc1b6ec..85f813bdb 100644 --- a/src/js/gui/HiPSSelector.js +++ b/src/js/gui/HiPSSelector.js @@ -62,7 +62,7 @@ import $ from 'jquery'; '
    By ID, title, keyword or URL
    ' + '
    ' + - '
    ' + + '
    ' + '' + '' + '
    ' + diff --git a/src/js/gui/Stack.js b/src/js/gui/Stack.js index 2782baeb5..e23697f05 100644 --- a/src/js/gui/Stack.js +++ b/src/js/gui/Stack.js @@ -36,6 +36,7 @@ import { HiPSLayer } from "./HiPSLayer.js"; import A from "../A.js"; import $ from 'jquery'; +import { ActionButton } from "./widgets/ActionButton.js"; export class Stack { @@ -242,7 +243,7 @@ export class Stack { // retrieve SVG icon, and apply the layer color var svgBase64 = window.btoa(iconSvg.replace(/FILLCOLOR/g, layer.color)); str += '
  • '; - str += ''; + str += ''; str += ' '; str += '
  • '; } @@ -251,8 +252,9 @@ export class Stack { str += ''; layerBox.append(str); - layerBox.find('.aladin-delete-graphic-layer').on('click', () => { - const layerToDelete = this.aladin.findLayerByUUID(layer.uuid); + layerBox.find('.aladin-delete-graphic-layer').on('click', (e) => { + let layerUuid = e.target.previousElementSibling.getAttribute("for"); + const layerToDelete = this.aladin.findLayerByUUID(layerUuid); this.aladin.removeLayer(layerToDelete); }); From 17f54ad3543bd036fc23401f1e3370fb813a145b Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Wed, 27 Sep 2023 23:25:01 +0200 Subject: [PATCH 09/32] add Horizontal layout --- src/js/gui/SODAQueryWindow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/gui/SODAQueryWindow.js b/src/js/gui/SODAQueryWindow.js index 5600aed1f..1948000f6 100644 --- a/src/js/gui/SODAQueryWindow.js +++ b/src/js/gui/SODAQueryWindow.js @@ -36,7 +36,7 @@ import { Utils } from '../Utils'; import { ActionButton } from './widgets/ActionButton.js'; import targetIconImg from '../../../assets/icons/target.svg'; import { Form } from './widgets/Form.js'; -import { HorizontalLayout } from './widgets/layout/horizontal.js'; +import { HorizontalLayout } from './widgets/layout/Horizontal.js'; export class SODAQueryWindow { constructor(aladin) { From 3214ca3d67c66efe3f0e5197bb2a5b21e47f6aad Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Tue, 10 Oct 2023 11:51:31 +0200 Subject: [PATCH 10/32] fix: set cuts before FITS HiPS has not been loaded --- examples/al-cuts-fits-hips.html | 25 +++++++++++++++++++++++++ src/js/ColorCfg.js | 4 ++-- src/js/ImageSurvey.js | 11 ++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 examples/al-cuts-fits-hips.html diff --git a/examples/al-cuts-fits-hips.html b/examples/al-cuts-fits-hips.html new file mode 100644 index 000000000..3faf8f24e --- /dev/null +++ b/examples/al-cuts-fits-hips.html @@ -0,0 +1,25 @@ + + + + + + +
    + + + + + + + diff --git a/src/js/ColorCfg.js b/src/js/ColorCfg.js index c4ead39d6..7175cdd48 100644 --- a/src/js/ColorCfg.js +++ b/src/js/ColorCfg.js @@ -46,8 +46,8 @@ this.reversed = true; } - this.minCut = (options && options.minCut) || 0.0; - this.maxCut = (options && options.maxCut) || 1.0; + this.minCut = (options && options.minCut) || undefined; + this.maxCut = (options && options.maxCut) || undefined; this.additiveBlending = options && options.additive; if (this.additiveBlending === undefined) { diff --git a/src/js/ImageSurvey.js b/src/js/ImageSurvey.js index 8f0cd59e7..a9962c0c3 100644 --- a/src/js/ImageSurvey.js +++ b/src/js/ImageSurvey.js @@ -343,9 +343,14 @@ export let ImageSurvey = (function () { // Initialize the color meta data here if (imgFormat === "fits") { // Take into account the default cuts given by the property file (this is true especially for FITS HiPSes) - const minCut = (options && options.minCut) || self.properties.minCutout || 0.0; - const maxCut = (options && options.maxCut) || self.properties.maxCutout || 1.0; - + const minCut = self.colorCfg.minCut || (options && options.minCut) || self.properties.minCutout || 0.0; + const maxCut = self.colorCfg.maxCut || (options && options.maxCut) || self.properties.maxCutout || 1.0; + + this.colorCfg.setCuts(minCut, maxCut); + } else { + const minCut = self.colorCfg.minCut || (options && options.minCut) || 0.0; + const maxCut = self.colorCfg.maxCut || (options && options.maxCut) || 1.0; + this.colorCfg.setCuts(minCut, maxCut); } From d5101acd917450b48cc22c13e4fac147cb544b03 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Tue, 21 Nov 2023 10:03:03 +0100 Subject: [PATCH 11/32] UI: stack layer WIP --- assets/icons/brightness.svg | 55 ++ assets/icons/color.svg | 2 + assets/icons/edit.svg | 24 + assets/icons/font-size.svg | 2 + assets/icons/function.svg | 5 + assets/icons/grid.svg | 39 + assets/icons/hide.svg | 13 + assets/icons/maximize.svg | 6 + assets/icons/move.svg | 50 ++ assets/icons/opacity.svg | 19 + assets/icons/pixel_histogram.svg | 4 + assets/icons/projection.svg | 5 + assets/icons/remove.svg | 19 + assets/icons/restore.svg | 6 + assets/icons/reticle.svg | 7 + assets/icons/search.svg | 4 + assets/icons/select.svg | 1 + assets/icons/settings.svg | 4 + assets/icons/show.svg | 10 + assets/icons/stack.svg | 6 + assets/icons/thickness.svg | 16 + assets/icons/upload.svg | 5 + examples/al-gw.html | 12 +- examples/al-hips-order-list.html | 2 +- examples/al-mars-features.html | 9 +- examples/al-multiple-surveys.html | 2 +- examples/al-soda-ska.html | 4 +- package.json | 1 - src/core/al-api/src/angle_fmt.rs | 29 +- src/core/al-api/src/color.rs | 2 + src/core/src/app.rs | 4 +- src/core/src/grid/label.rs | 9 +- src/core/src/grid/mod.rs | 2 +- src/core/src/renderable/hips/raytracing.rs | 2 +- src/core/src/renderable/text.rs | 5 +- src/css/aladin.css | 468 +++++++--- src/js/Aladin.js | 321 +++---- src/js/Color.js | 52 +- src/js/ColorCfg.js | 8 + src/js/ImageFITS.js | 4 + src/js/ImageSurvey.js | 26 +- src/js/MeasurementTable.js | 152 +--- src/js/ProjectionEnum.js | 18 +- src/js/Reticle.js | 137 +++ src/js/Source.js | 2 +- src/js/View.js | 344 ++++---- src/js/events/ALEvent.js | 8 +- src/js/gui/CatalogSelector.js | 2 +- src/js/gui/FoV.js | 99 +++ src/js/gui/HiPSLayer.js | 817 +++++++++--------- src/js/gui/HiPSSelector.js | 213 ----- src/js/gui/HiPSSelectorBox.js | 336 +++++++ src/js/gui/Layout.js | 189 ++++ src/js/gui/Location.js | 166 ++++ src/js/gui/SODAQueryWindow.js | 35 +- src/js/gui/Stack.js | 103 +-- .../Toolbar/Controls/FullScreen.js} | 52 +- src/js/gui/Toolbar/Controls/GotoBox.js | 91 ++ src/js/gui/Toolbar/Controls/GridBox.js | 196 +++++ src/js/gui/Toolbar/Controls/Settings.js | 244 ++++++ src/js/gui/Toolbar/Controls/SimbadPointer.js | 62 ++ .../Controls/StackLayer/ColormapSelector.js | 78 ++ .../Toolbar/Controls/StackLayer/EditBox.js | 416 +++++++++ .../gui/Toolbar/Controls/StackLayer/Menu.js | 101 +++ .../gui/Toolbar/Controls/StackLayer/Stack.js | 300 +++++++ src/js/gui/Toolbar/MainHiPSSelector.js | 218 +++++ src/js/gui/Toolbar/Menu.js | 314 +++++++ src/js/gui/Toolbar/Toolbar.js | 65 ++ src/js/gui/Toolbar/ViewPortInfo.js | 121 +++ src/js/gui/Utils.js | 66 ++ src/js/gui/widgets/ActionButton.js | 92 +- src/js/gui/widgets/Box.js | 232 +++++ src/js/gui/widgets/ContextMenu.js | 176 ++-- src/js/gui/widgets/Form.js | 139 ++- src/js/gui/widgets/Input.js | 238 +++++ src/js/gui/widgets/Selector.js | 159 ++++ src/js/gui/widgets/Tab.js | 137 +++ src/js/gui/widgets/Table.js | 148 ++++ src/js/gui/widgets/Tooltip.js | 99 ++- src/js/gui/widgets/Utils.js | 18 - src/js/gui/widgets/Widget.js | 188 +++- src/js/gui/widgets/layout/Horizontal.js | 61 -- src/js/libs/astro/coo.js | 41 +- src/js/vo/Datalink.js | 8 +- src/js/vo/ObsCore.js | 8 +- 85 files changed, 6292 insertions(+), 1661 deletions(-) create mode 100644 assets/icons/brightness.svg create mode 100644 assets/icons/color.svg create mode 100644 assets/icons/edit.svg create mode 100644 assets/icons/font-size.svg create mode 100644 assets/icons/function.svg create mode 100644 assets/icons/grid.svg create mode 100644 assets/icons/hide.svg create mode 100644 assets/icons/maximize.svg create mode 100644 assets/icons/move.svg create mode 100644 assets/icons/opacity.svg create mode 100644 assets/icons/pixel_histogram.svg create mode 100644 assets/icons/projection.svg create mode 100644 assets/icons/remove.svg create mode 100644 assets/icons/restore.svg create mode 100644 assets/icons/reticle.svg create mode 100644 assets/icons/search.svg create mode 100644 assets/icons/select.svg create mode 100644 assets/icons/settings.svg create mode 100644 assets/icons/show.svg create mode 100644 assets/icons/stack.svg create mode 100644 assets/icons/thickness.svg create mode 100644 assets/icons/upload.svg create mode 100644 src/js/Reticle.js create mode 100644 src/js/gui/FoV.js delete mode 100644 src/js/gui/HiPSSelector.js create mode 100644 src/js/gui/HiPSSelectorBox.js create mode 100644 src/js/gui/Layout.js create mode 100644 src/js/gui/Location.js rename src/js/{Location.js => gui/Toolbar/Controls/FullScreen.js} (52%) create mode 100644 src/js/gui/Toolbar/Controls/GotoBox.js create mode 100644 src/js/gui/Toolbar/Controls/GridBox.js create mode 100644 src/js/gui/Toolbar/Controls/Settings.js create mode 100644 src/js/gui/Toolbar/Controls/SimbadPointer.js create mode 100644 src/js/gui/Toolbar/Controls/StackLayer/ColormapSelector.js create mode 100644 src/js/gui/Toolbar/Controls/StackLayer/EditBox.js create mode 100644 src/js/gui/Toolbar/Controls/StackLayer/Menu.js create mode 100644 src/js/gui/Toolbar/Controls/StackLayer/Stack.js create mode 100644 src/js/gui/Toolbar/MainHiPSSelector.js create mode 100644 src/js/gui/Toolbar/Menu.js create mode 100644 src/js/gui/Toolbar/Toolbar.js create mode 100644 src/js/gui/Toolbar/ViewPortInfo.js create mode 100644 src/js/gui/Utils.js create mode 100644 src/js/gui/widgets/Box.js create mode 100644 src/js/gui/widgets/Input.js create mode 100644 src/js/gui/widgets/Selector.js create mode 100644 src/js/gui/widgets/Tab.js create mode 100644 src/js/gui/widgets/Table.js delete mode 100644 src/js/gui/widgets/Utils.js delete mode 100644 src/js/gui/widgets/layout/Horizontal.js diff --git a/assets/icons/brightness.svg b/assets/icons/brightness.svg new file mode 100644 index 000000000..e76c0ea9d --- /dev/null +++ b/assets/icons/brightness.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/color.svg b/assets/icons/color.svg new file mode 100644 index 000000000..5502e1e89 --- /dev/null +++ b/assets/icons/color.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg new file mode 100644 index 000000000..f0e21be55 --- /dev/null +++ b/assets/icons/edit.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/font-size.svg b/assets/icons/font-size.svg new file mode 100644 index 000000000..31764824e --- /dev/null +++ b/assets/icons/font-size.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/function.svg b/assets/icons/function.svg new file mode 100644 index 000000000..0585b6464 --- /dev/null +++ b/assets/icons/function.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/icons/grid.svg b/assets/icons/grid.svg new file mode 100644 index 000000000..79b38637e --- /dev/null +++ b/assets/icons/grid.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/hide.svg b/assets/icons/hide.svg new file mode 100644 index 000000000..e283a029e --- /dev/null +++ b/assets/icons/hide.svg @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg new file mode 100644 index 000000000..b93f83b7e --- /dev/null +++ b/assets/icons/maximize.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/icons/move.svg b/assets/icons/move.svg new file mode 100644 index 000000000..be7aa16ba --- /dev/null +++ b/assets/icons/move.svg @@ -0,0 +1,50 @@ + + + + + \ No newline at end of file diff --git a/assets/icons/opacity.svg b/assets/icons/opacity.svg new file mode 100644 index 000000000..4a64379ca --- /dev/null +++ b/assets/icons/opacity.svg @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/pixel_histogram.svg b/assets/icons/pixel_histogram.svg new file mode 100644 index 000000000..2363bb7b2 --- /dev/null +++ b/assets/icons/pixel_histogram.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/projection.svg b/assets/icons/projection.svg new file mode 100644 index 000000000..ebe335b98 --- /dev/null +++ b/assets/icons/projection.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/icons/remove.svg b/assets/icons/remove.svg new file mode 100644 index 000000000..8b63983ea --- /dev/null +++ b/assets/icons/remove.svg @@ -0,0 +1,19 @@ + + + + + delete [#1487] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/restore.svg b/assets/icons/restore.svg new file mode 100644 index 000000000..498e7ce46 --- /dev/null +++ b/assets/icons/restore.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/icons/reticle.svg b/assets/icons/reticle.svg new file mode 100644 index 000000000..82427adf9 --- /dev/null +++ b/assets/icons/reticle.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 000000000..648171eb1 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/select.svg b/assets/icons/select.svg new file mode 100644 index 000000000..bbd5f4a6d --- /dev/null +++ b/assets/icons/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg new file mode 100644 index 000000000..ba0aa4398 --- /dev/null +++ b/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/show.svg b/assets/icons/show.svg new file mode 100644 index 000000000..2a00ea56c --- /dev/null +++ b/assets/icons/show.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/stack.svg b/assets/icons/stack.svg new file mode 100644 index 000000000..ffaa37154 --- /dev/null +++ b/assets/icons/stack.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/icons/thickness.svg b/assets/icons/thickness.svg new file mode 100644 index 000000000..6c5e85125 --- /dev/null +++ b/assets/icons/thickness.svg @@ -0,0 +1,16 @@ + + + + line-thickness + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/upload.svg b/assets/icons/upload.svg new file mode 100644 index 000000000..f387c575f --- /dev/null +++ b/assets/icons/upload.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/al-gw.html b/examples/al-gw.html index bcbcf1760..71a0485b8 100644 --- a/examples/al-gw.html +++ b/examples/al-gw.html @@ -8,10 +8,16 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {projection: "TAN", target: '15 16 57.636 -60 55 7.49', samp: true, showCooGrid: true, fov: 90, fullScreen: true}); - - var moc_0_2 = A.MOCFromURL("./gw/gw_0.1.fits",{ name: "GW 10%", color: "#ff00ff", opacity: 0.3, lineWidth: 5, perimeter: true, fill: true, fillColor:"#00ff00"}); + aladin = A.aladin('#aladin-lite-div', {showReticle: true, projection: "TAN", target: '15 16 57.636 -60 55 7.49', showProjectionControl: true, showSimbadPointerControl: true, showContextMenu: true, showCooGridControl: true, showCooGrid: true, fov: 90, fullScreen: true}); + + var moc_0_99 = A.MOCFromURL("./gw/gw_0.9.fits",{ name: "GW 90%", color: "#ff0000", opacity: 0.5, lineWidth: 3, fill: true, perimeter: true}); + var moc_0_95 = A.MOCFromURL("./gw/gw_0.6.fits",{ name: "GW 60%", color: "#00ff00", opacity: 0.5, lineWidth: 3, fill: true, perimeter: true}); + var moc_0_5 = A.MOCFromURL("./gw/gw_0.3.fits",{ name: "GW 30%", color: "#00ffff", opacity: 0.5, lineWidth: 3, fill: true, perimeter: true}); + var moc_0_2 = A.MOCFromURL("./gw/gw_0.1.fits",{ name: "GW 10%", color: "#ff00ff", opacity: 0.5, lineWidth: 3, fill: true, perimeter: true}); + aladin.addMOC(moc_0_99); + aladin.addMOC(moc_0_95); + aladin.addMOC(moc_0_5); aladin.addMOC(moc_0_2); }); diff --git a/examples/al-hips-order-list.html b/examples/al-hips-order-list.html index 4e36e4abb..d4b47131f 100644 --- a/examples/al-hips-order-list.html +++ b/examples/al-hips-order-list.html @@ -10,7 +10,7 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {survey: 'http://alasky.cds.unistra.fr/ancillary/GaiaDR2/hips-density-map/', target: 'galactic center'}); + aladin = A.aladin('#aladin-lite-div', {survey: 'http://alasky.cds.unistra.fr/ancillary/GaiaDR2/hips-density-map/', showProjectionControl: true, showContextMenu: true, fullScreen: true, target: 'galactic center'}); const fluxMap = aladin.createImageSurvey('gdr3-color-flux-map', 'Gaia DR3 flux map', 'https://alasky.u-strasbg.fr/ancillary/GaiaEDR3/color-Rp-G-Bp-flux-map', 'equatorial', 7); const densityMap = aladin.createImageSurvey('gdr3-density-map', 'Gaia DR3 density map', 'sdfsg', 'equatorial', 7, {imgFormat: 'fits'}); diff --git a/examples/al-mars-features.html b/examples/al-mars-features.html index 0b10f8765..c935890ac 100644 --- a/examples/al-mars-features.html +++ b/examples/al-mars-features.html @@ -10,7 +10,7 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {fov: 180, fullScreen: true, cooFrame: 'equatorial', showCooGridControl: true, showSimbadPointerControl: true, showCooGrid: false, survey: 'CDS/P/Mars/THEMIS-Day-100m-v12'}); + aladin = A.aladin('#aladin-lite-div', {fov: 180, fullScreen: true, cooFrame: 'equatorial', showCooGridControl: true, showSimbadPointerControl: true, showContextMenu: true, showCooGrid: false, survey: 'CDS/P/Mars/THEMIS-Day-100m-v12'}); aladin.setProjection('SIN'); @@ -37,7 +37,8 @@ canvasCtx.fillStyle = '#eee'; canvasCtx.strokeStyle = '#222'; canvasCtx.lineWidth = 1; - canvasCtx.fillText(source.data['Feature Name'], source.x + xShift, source.y -4); + + canvasCtx.fillText(source.data['Feature_Name'], source.x + xShift, source.y -4); //canvasCtx.strokeText(source.data['Feature Name'], source.x + xShift, source.y -4); // object type is displayed only if fov<5° @@ -48,11 +49,11 @@ canvasCtx.fillStyle = '#abc'; canvasCtx.strokeStyle = '#222'; canvasCtx.lineWidth = 1; - canvasCtx.fillText(source.data['Feature Type'], source.x + 2 + xShift, source.y + 10); + canvasCtx.fillText(source.data['Feature_Type'], source.x + 2 + xShift, source.y + 10); //canvasCtx.strokeText(source.data['Feature Type'], source.x + 2 + xShift, source.y + 10); }; - aladin.addCatalog(A.catalogFromURL('https://aladin.cds.unistra.fr/AladinLite/doc/API/examples/data/mars-features.xml', {raField: 'Longitude', decField: 'Latitude', shape: drawFunction, onClick: 'showTable'})); + aladin.addCatalog(A.catalogFromURL('https://aladin.cds.unistra.fr/AladinLite/doc/API/examples/data/mars-features.xml', {name: "Mars features", raField: 'Longitude', decField: 'Latitude', shape: drawFunction, onClick: 'showTable'})); aladin.getBaseImageLayer().setColormap('yiorbr') aladin.gotoRaDec(226.1433542, 18.6308694); diff --git a/examples/al-multiple-surveys.html b/examples/al-multiple-surveys.html index 6770a63ec..c41f596e3 100644 --- a/examples/al-multiple-surveys.html +++ b/examples/al-multiple-surveys.html @@ -9,7 +9,7 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {fullScreen: true, survey: ['P/DM/vizMine', 'P/HST/GOODS/color', 'P/MATLAS/g'], target: '0 0', showCooGrid: true, fov: 180}); + aladin = A.aladin('#aladin-lite-div', {fullScreen: true, survey: ['P/DM/vizMine', 'P/HST/GOODS/color', 'P/MATLAS/g'], target: '0 0', showProjectionControl: true, showCooGrid: true, fov: 180}); }); diff --git a/examples/al-soda-ska.html b/examples/al-soda-ska.html index 4a8ae970b..6d5b69174 100644 --- a/examples/al-soda-ska.html +++ b/examples/al-soda-ska.html @@ -12,8 +12,8 @@ A.init.then(() => { aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "m51", fov: 180, projection: 'SIN', showContextMenu: true}); - //const c1 = A.catalogFromSKAORucio("200 50", 90, {onClick: 'showTable'}); - const c1 = A.catalogFromURL('./ObsCoreRucioScs.xml', {onClick: 'showTable', limit: 1000}); + const c1 = A.catalogFromSKAORucio("200 50", 90, {onClick: 'showTable'}); + //const c1 = A.catalogFromURL('./ObsCoreRucioScs.xml', {onClick: 'showTable', limit: 1000}); aladin.addCatalog(c1); }); diff --git a/package.json b/package.json index 5c5773070..63f9e69b0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "devDependencies": { "happy-dom": "^10.11.0", - "npm": "^9.8.1", "vite": "^4.3.8", "vite-plugin-css-injected-by-js": "^3.1.1", "vite-plugin-glsl": "^1.1.2", diff --git a/src/core/al-api/src/angle_fmt.rs b/src/core/al-api/src/angle_fmt.rs index feff9e449..49ff3e97e 100644 --- a/src/core/al-api/src/angle_fmt.rs +++ b/src/core/al-api/src/angle_fmt.rs @@ -3,13 +3,38 @@ use wasm_bindgen::prelude::*; use std::fmt; +#[wasm_bindgen(raw_module = "../../js/libs/astro/coo.js")] +extern "C" { + #[wasm_bindgen(js_name = Format)] + pub type Format; + + /** + * Convert a decimal coordinate into sexagesimal string, according to the given precision
    + * 8: 1/1000th sec, 7: 1/100th sec, 6: 1/10th sec, 5: sec, 4: 1/10th min, 3: min, 2: 1/10th deg, 1: deg + * @param num number (integer or decimal) + * @param prec precision (= number of decimal digit to keep or append) + * @param plus if true, the '+' sign is displayed + * @return a string with the formatted sexagesimal number + */ + #[wasm_bindgen(static_method_of = Format)] + pub fn toSexagesimal(num: f64, prec: u8, plus: bool) -> String; + /** + * Convert a decimal coordinate into a decimal string, according to the given precision + * @param num number (integer or decimal) + * @param prec precision (= number of decimal digit to keep or append) + * @return a string with the formatted sexagesimal number + */ + #[wasm_bindgen(static_method_of = Format)] + pub fn toDecimal(num: f64, prec: u8) -> String; +} + #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] #[wasm_bindgen] pub enum AngleSerializeFmt { DMM, DD, DMS, - HMS + HMS, } impl fmt::Display for AngleSerializeFmt { @@ -27,4 +52,4 @@ impl fmt::Display for AngleSerializeFmt { }; write!(f, "{}", str) } -} \ No newline at end of file +} diff --git a/src/core/al-api/src/color.rs b/src/core/al-api/src/color.rs index d12287cb3..b5e04de83 100644 --- a/src/core/al-api/src/color.rs +++ b/src/core/al-api/src/color.rs @@ -10,6 +10,8 @@ extern "C" { pub fn hexToRgba(hex: String) -> JsValue; #[wasm_bindgen(static_method_of = Color)] pub fn rgbToHex(r: u8, g: u8, b: u8) -> String; + #[wasm_bindgen(static_method_of = Color)] + pub fn rgbaToHex(r: u8, g: u8, b: u8, a: u8) -> String; } #[derive(Debug, Clone, Copy, Deserialize, Serialize)] diff --git a/src/core/src/app.rs b/src/core/src/app.rs index 7ce21929d..bd00de6c8 100644 --- a/src/core/src/app.rs +++ b/src/core/src/app.rs @@ -113,7 +113,7 @@ use cgmath::{Vector2, Vector3}; use futures::{io::BufReader, stream::StreamExt}; // for `next` use crate::math::projection::*; -pub const BLENDING_ANIM_DURATION: DeltaTime = DeltaTime::from_millis(200.0); // in ms +pub const BLENDING_ANIM_DURATION: DeltaTime = DeltaTime::from_millis(400.0); // in ms //use crate::buffer::Tile; use crate::time::Time; use cgmath::InnerSpace; @@ -1276,7 +1276,7 @@ impl App { // launch the new tile requests self.request_for_new_tiles = true; - self.manager.set_kernel_size(&self.camera); + //self.manager.set_kernel_size(&self.camera); self.request_redraw = true; } diff --git a/src/core/src/grid/label.rs b/src/core/src/grid/label.rs index ba34300ae..86d3c2912 100644 --- a/src/core/src/grid/label.rs +++ b/src/core/src/grid/label.rs @@ -15,7 +15,7 @@ use crate::math::angle::ToAngle; use cgmath::Vector2; use core::ops::Range; -const OFF_TANGENT: f64 = 35.0; +const OFF_TANGENT: f64 = 70.0; const OFF_BI_TANGENT: f64 = 5.0; pub enum LabelOptions { @@ -76,7 +76,9 @@ impl Label { lon += TWICE_PI; } - let content = fmt.to_string(lon.to_angle()); + //let content = fmt.to_string(lon.to_angle()); + let content = al_api::angle_fmt::Format::toSexagesimal(lon.to_degrees() / 15.0, 8, false); + let position = if !fov.is_allsky() { d1 + OFF_TANGENT * dt - OFF_BI_TANGENT * db } else { @@ -128,7 +130,8 @@ impl Label { let dt = (d2 - d1).normalize(); let db = Vector2::new(dt.y.abs(), dt.x.abs()); - let content = SerializeFmt::DMS.to_string(lonlat.lat()); + //let content = SerializeFmt::DMS.to_string(lonlat.lat()); + let content = al_api::angle_fmt::Format::toSexagesimal(lonlat.lat().to_degrees(), 7, false); let fov = camera.get_field_of_view(); let position = if !fov.is_allsky() && !fov.contains_pole() { diff --git a/src/core/src/grid/mod.rs b/src/core/src/grid/mod.rs index e4ea59ea8..a8777108c 100644 --- a/src/core/src/grid/mod.rs +++ b/src/core/src/grid/mod.rs @@ -95,7 +95,7 @@ impl ProjetedGrid { b: color.b, a: self.color.a, }; - self.text_renderer.set_color(&color); + self.text_renderer.set_color(&self.color); } if let Some(opacity) = opacity { diff --git a/src/core/src/renderable/hips/raytracing.rs b/src/core/src/renderable/hips/raytracing.rs index 396648923..bcf18a5d3 100644 --- a/src/core/src/renderable/hips/raytracing.rs +++ b/src/core/src/renderable/hips/raytracing.rs @@ -242,6 +242,6 @@ impl RayTracer { // Check whether the tile depth is 0 for square projection // definition domains i.e. Mercator let depth = camera.get_tile_depth(); - camera.is_allsky() || depth <= 1 + camera.is_allsky() || depth <= 2 } } diff --git a/src/core/src/renderable/text.rs b/src/core/src/renderable/text.rs index 6010a14b2..aabce8ff2 100644 --- a/src/core/src/renderable/text.rs +++ b/src/core/src/renderable/text.rs @@ -46,11 +46,12 @@ impl TextRenderManager { }) } - pub fn set_color(&mut self, color: &ColorRGB) { - let hex = al_api::color::Color::rgbToHex( + pub fn set_color(&mut self, color: &ColorRGBA) { + let hex = al_api::color::Color::rgbaToHex( (color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, + (color.a * 255.0) as u8, ); self.color = JsValue::from_str(&hex); } diff --git a/src/css/aladin.css b/src/css/aladin.css index d561ee38a..447e06302 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -65,15 +65,6 @@ content: ""; display: table; clear: both; - } - -.aladin-location { - z-index: 20; - position:absolute; - top: 0px; - padding: 2px 4px 2px 4px; - background-color: rgba(255, 255, 255, 0.5); - font-size: 11px; } .aladin-projection-select { @@ -86,12 +77,6 @@ font-size: 11px; } -.aladin-location-text { - font-family: monospace; - font-size: 13px; - margin-left: 4px; -} - .aladin-clipboard::before { content: ' 📋'; cursor:pointer; @@ -99,50 +84,58 @@ .aladin-measurement-div { z-index: 77; - max-width: 100%; - position: absolute; - bottom: 20px; + font-family: monospace; font-size: 12px; - max-height: 20%; display: flex; flex-direction: column; flex-wrap: nowrap; -} -.aladin-measurement-div table { - -ms-overflow-style: none; - scrollbar-width: none; - - position: relative; - overflow-y: scroll; - overscroll-behavior-x: none; - background-color: rgba(255, 255, 255, 0.9); - border-collapse: collapse; - table-layout: fixed; - white-space: nowrap; - height: 100%; - display: block; + +} - border: 2px solid black; +.aladin-tabs .aladin-tabs-content { + float: left; } -.aladin-measurement-div .tabs button { - background-color: rgba(255, 255, 255, 0.9); +.aladin-tabs .aladin-tabs-head .aladin-tabs-head-tab { + background-color: #fff; + color: black; border-radius: 4px 4px 0px 0px; float: left; border: none; - outline: none; cursor: pointer; - font-family: monospace; - font-size: 13px; - + font-size: 13px; + font-family: Verdana, Geneva, Tahoma, sans-serif; + text-overflow: ellipsis; text-align: left; } +.aladin-tabs-head-tab.aladin-tabs-head-tab-selected, .aladin-tabs-content-option.aladin-tabs-content-option-selected { + filter: brightness(120%); +} + +.aladin-measurement-div table { + background-color: rgba(255, 255, 255, 0.9); + border-collapse: collapse; + table-layout: fixed; + white-space: nowrap; + + display: block; + + max-height: 15em; + max-width: 100%; + -ms-overflow-style: none; + overscroll-behavior-x: none; + overflow-y: scroll; + scrollbar-width: none; + + border: 2px solid black; +} + .aladin-measurement-div table::-webkit-scrollbar { display: none; /* for Chrome, Safari, and Opera */ } @@ -155,32 +148,21 @@ color: #000; } -.aladin-measurement-div table td, .aladin-measurement-div table th { - padding: 0.3em 0.3em; - white-space: nowrap; - overflow: hidden; - - text-align: left; - color: #000; - max-width: 150px; - text-overflow: ellipsis; -} - -.aladin-measurement-div table td .aladin-href-td-container a:hover { +.aladin-measurement-div table td.aladin-href-td-container a:hover { overflow: visible; display: inline-block; animation: leftright 5s infinite normal linear; } -.aladin-measurement-div table td > a:hover { - background-color: #fff; +.aladin-measurement-div table th { + padding: 0.3em 0.5em; } -.aladin-measurement-div table td .aladin-href-td-container { +.aladin-measurement-div table td.aladin-href-td-container { border: 1px solid #d2d2d2; border-radius: 3px; - padding: 0.4em; + padding: 0.5em; white-space: nowrap; overflow: hidden; text-align: center; @@ -188,7 +170,17 @@ max-width: 150px; text-overflow: ellipsis; } -.aladin-measurement-div table td .aladin-href-td-container:hover { + +.aladin-measurement-div table td.aladin-text-td-container { + padding: 0.5em; + white-space: nowrap; + overflow: hidden; + color: #000; + max-width: 150px; + text-overflow: ellipsis; +} + +.aladin-measurement-div table td.aladin-href-td-container:hover { background-color: #fff; } @@ -228,20 +220,6 @@ word-wrap:break-word; font-weight: bold; } -.aladin-fov { - z-index: 20; - position: absolute; - padding: 0; - font-size: 12px; - font-weight: bold; - font-family: monospace; - bottom: 0px; - padding: 2px; - color: #321bdf; - /*text-shadow: 0 0 1px white, 0 0 1px white, 0 0 1px white, 0 0 1px white, 0 0 1px white;*/ - background-color: rgba(255, 255, 255, 0.5); -} - /* maximize/restore size icons */ .aladin-maximize { position: absolute; @@ -319,6 +297,12 @@ word-wrap:break-word; transform: translate(0%,50%); } +.aladin-anchor-rb { + bottom: 0%; + right: 0%; + transform: translate(0%,0%); +} + .aladin-anchor-center { top: 50%; left: 50%; @@ -332,6 +316,7 @@ word-wrap:break-word; background: whitesmoke; border-radius: 2px; position: absolute; + max-width: 15em; font-size: 13px; font-family: Verdana, Geneva, Tahoma, sans-serif; @@ -339,13 +324,15 @@ word-wrap:break-word; line-height: 1.3; color: #222; - box-shadow: 0 0 6px rgba(0,0,0,0.2); + /*box-shadow: 0 0 6px rgba(0,0,0,0.2);*/ /* Allow scrolling but disable scroll bar */ -ms-overflow-style: none; /* for Internet Explorer, Edge */ scrollbar-width: none; /* for Firefox */ /*overflow-y: auto;*/ - overflow-y: inherit; + overflow-y: none; + max-height: 500px; + max-width: fit-content; } .aladin-box::-webkit-scrollbar { @@ -405,9 +392,9 @@ canvas { .aladin-box-title { font-size: 16px; font-family: Verdana, Geneva, Tahoma, sans-serif; - line-height: 2.0; + line-height: 1.5em; font-weight: bold; - cursor: pointer; + white-space: nowrap; } .aladin-box-content { @@ -449,7 +436,6 @@ canvas { padding: 5px; } - .aladin-simbadPointerControl-container { position: absolute; left: 4px; @@ -534,7 +520,8 @@ canvas { } .aladin-closeBtn { - float:right; + position:absolute; + right: 4px; margin-top: 0 0 2px 0; cursor:pointer; color: #605F61; @@ -543,7 +530,6 @@ canvas { background: #fff; font-size: 15px; font-weight: bold; - display: inline-block; line-height: 0px; padding: 8px 2px; } @@ -633,28 +619,80 @@ canvas { display: inline-block; margin-bottom: 0; margin: 0; - padding: 8px; + font-size: 12px; font-weight: normal; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; - border: 1px solid #357ebd; + border: 1px solid #484848; border-radius: 3px; - color: #ffffff; - background-color: #428bca; + color: white; + background-color: #bababa; +} + +.aladin-btn.toggled { + border: 2px solid greenyellow; +} + +.aladin-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + + position: absolute; + top: 0px; + left: 0px; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); + height: 2.5em; + color: white; + align-content: stretch; + z-index: 9000; + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 12px; +} +.aladin-label-text { + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 12px; +} + +.aladin-toolbar .aladin-base-survey { + position: absolute; + left: 50%; + transform: translate(-50%, 0%); +} + +.aladin-toolbar .aladin-fov, .aladin-toolbar .aladin-location { + font-family: monospace; + + border: 1px solid white; + border-radius: 3px; + height: 1.5em; } .aladin-24px-icon { padding: 0; - border: 1px solid #AEAEAE; - width: 24px; height: 24px; } +.aladin-horizontal-list.aladin-stack-item { + margin-bottom: 4px; + + padding: 4px; + background-color: rgb(220, 220, 220); + border: 1px solid rgb(72, 72, 72); + border-radius: 3px; + text-align: center; + font-size: 12px; + font-weight: normal; + text-overflow: ellipsis; + vertical-align: middle; +} + .aladin-btn:hover { filter: brightness(105%); } @@ -705,41 +743,61 @@ canvas { margin-bottom: 5px; } -.aladin-horizontal-list { +.aladin-vertical-list { display: flex; align-items: center; list-style: none; + flex-direction: column; +} - padding: 0; - margin: 0; +.aladin-horizontal-list { + display: flex; + align-items: center; + list-style: none; } .aladin-horizontal-list > * { vertical-align: middle; - margin-right: 2px; } -.aladin-form-input { +.aladin-horizontal-list > *:last-of-type { + margin: 0; +} + + +.aladin-form .aladin-form-input { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 7px; } -.aladin-form-input > label{ +.aladin-form .aladin-form-input:last-of-type { + margin-bottom: 0px; +} + +.aladin-form .aladin-form-input select { + width: 100%; +} + +.aladin-form .aladin-form-input > label{ flex: 1; } -.aladin-form-input .aladin-input { +.aladin-form .aladin-form-input .aladin-input { flex: 2; } -.aladin-form-input-group { +.aladin-form .aladin-form-input-group { border-bottom: 1px solid #d2d2d2; margin: 5px; } -.aladin-form-input-group .aladin-form-group-header { +.aladin-form .aladin-form-input-group:last-of-type { + border-bottom: none; +} + +.aladin-form .aladin-form-input-group .aladin-form-group-header { display: flex; align-items: center; @@ -912,33 +970,67 @@ canvas { /* *********************************************** */ /* Context menu */ +.aladin-context-menu .aladin-context-sub-menu { + position: absolute; + top: 0; + left: 100%; + display: none; + width: max-content; +} .aladin-context-sub-menu, .aladin-context-menu { - position: fixed; - background: #fff; - color: #000; - z-index: 9999999; - width: 150px; - margin: 0; - padding: 2px 0; - border-radius: 2px; - box-shadow: 0 0 6px rgba(0,0,0,0.2); - font-size: 12px; - font-family: Verdana, Geneva, Tahoma, sans-serif; + position: fixed; + background: #fff; + color: #000; + z-index: 100; + width: max-content; + margin: 0; + padding: 2px 0; + border-radius: 2px; + box-shadow: 0 0 8px rgba(0,0,0,0.2); + font-size: 12px; + font-family: Verdana, Geneva, Tahoma, sans-serif; } .aladin-context-menu .aladin-context-menu-item { - height: 22px; - display: flex; - align-items: center; - padding: 4px 8px; - position: relative; - border-bottom: 1px solid #d2d2d2; + cursor: pointer; + + height: 22px; + display: flex; + align-items: center; + padding: 4px 8px; + position: relative; + border-bottom: 1px solid #d2d2d2; } +.aladin-context-menu .aladin-context-menu-item.aladin-context-menu-item-disabled { + cursor: not-allowed; + background: #eee; + + z-index: 100; + + height: 22px; + display: flex; + align-items: center; + padding: 4px 8px; + position: relative; + border-bottom: 1px solid #d2d2d2; +} + +.aladin-context-menu .aladin-context-menu-item.aladin-context-menu-item-selected::after { + content: "✓"; + margin-left: 2px; +} + + .aladin-context-menu-item:hover { - background: rgb(212, 231, 250); + background-color: rgba(0, 0, 0, 0.1); +} + +.aladin-survey-item:hover { + filter: brightness(150%); + z-index: 20; /* This is for the tooltip to appear outside the item, i.e. over the sibling item */ } .aladin-context-menu .aladin-context-menu-item span { @@ -956,14 +1048,6 @@ canvas { background: #d2d2d2; } -.aladin-context-menu .aladin-context-sub-menu { - position: absolute; - top: 0; - left: 100%; - display: none; - width: 150px; -} - .aladin-context-menu .aladin-context-menu-item:hover > .aladin-context-sub-menu { display: block; } @@ -974,29 +1058,150 @@ canvas { } .aladin-context-menu.top .aladin-context-sub-menu { - top: 100%; - transform: translateY(-100%); + top: 100%; + transform: translateY(-100%); } .aladin-context-menu.left.top .aladin-context-sub-menu { - transform: translate(-100%, -100%); + transform: translate(-100%, -100%); +} + +.aladin-reticle { + position: absolute; + z-index: 20; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-size: contain; + + visibility: hidden; + + pointer-events: none; } +/* *********************************************** */ + +/* Frames */ +.aladin-input-color { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + padding: 0; + border: none; + border-radius: 5px; + width: 30px; + height: 20px; + cursor: pointer; +} + +.aladin-input-color::-webkit-color-swatch { + border: none; + border-radius: 5px; + padding: 0; +} + +.aladin-input-color::-webkit-color-swatch-wrapper { + border: none; + border-radius: 5px; + padding: 0; +} + +/********** Range Input Styles **********/ +/*Range Reset*/ +.aladin-input-range { + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; + width: 5em; + } + + /* Removes default focus */ + .aladin-input-range:focus { + outline: none; + } + + /***** Chrome, Safari, Opera and Edge Chromium styles *****/ + /* slider track */ + .aladin-input-range::-webkit-slider-runnable-track { + background-color: #bababa; + border-radius: 0.1rem; + height: 0.1rem; + } + + /* slider thumb */ + .aladin-input-range::-webkit-slider-thumb { + -webkit-appearance: none; /* Override default look */ + appearance: none; + margin-top: -7px; /* Centers thumb on the track */ + + /*custom styles*/ + background-color: #bababa; + height: 1rem; + width: 1rem; + + border-radius: 0.5rem; + } + .aladin-input-range:hover::-webkit-slider-thumb { + filter: brightness(105%); + } + + + /******** Firefox styles ********/ + /* slider track */ + .aladin-input-range::-moz-range-track { + background-color: #bababa; + border-radius: 0.1rem; + height: 0.1rem; + } + + /* slider thumb */ + .aladin-input-range::-moz-range-thumb { + border: none; /*Removes extra border that FF applies*/ + border-radius: 0; /*Removes default border-radius that FF applies*/ + + /*custom styles*/ + background-color: #bababa; + height: 1rem; + width: 1rem; + + border-radius: 0.5rem; + } + + .aladin-input-range:hover::-moz-range-thumb { + filter: brightness(105%); + } + + /* *********************************************** */ /* Tooltip */ +.aladin-tooltip-container { + position: relative; + /* take the size of its inner div child */ + + float: left; +} + .aladin-tooltip-container .aladin-tooltip { + pointer-events: none; visibility: hidden; background-color: black; + + width: max-content; color: #fff; text-align: center; padding: 4px; border-radius: 2px; + + top:0%; /* Position the tooltip text - see examples below! */ position: absolute; z-index: 10000; + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 12px; } /* Show the tooltip text when you mouse over the tooltip container */ @@ -1004,6 +1209,25 @@ canvas { visibility: visible; } +.aladin-tooltip-container.left .aladin-tooltip { + left: 0; + transform: translateX(-100%); +} + +.aladin-tooltip-container.top .aladin-tooltip { + top: 0; + transform: translateY(-100%); +} + +.aladin-tooltip-container.bottom .aladin-tooltip { + top: 100%; +} + +.aladin-tooltip-container.right .aladin-tooltip { + left: 100%; +} + + /* *********************************************** */ /* Cursors */ diff --git a/src/js/Aladin.js b/src/js/Aladin.js index de18ecee8..f0c7341b9 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -37,25 +37,24 @@ import { Sesame } from "./Sesame.js"; import { PlanetaryFeaturesNameResolver } from "./PlanetaryFeaturesNameResolver.js"; import { CooFrameEnum } from "./CooFrameEnum.js"; import { MeasurementTable } from "./MeasurementTable.js"; -import { Location } from "./Location.js"; import { ImageSurvey } from "./ImageSurvey.js"; import { Coo } from "./libs/astro/coo.js"; import { CooConversion } from "./CooConversion.js"; import { AladinLogo } from "./gui/AladinLogo.js"; -import { ProjectionSelector } from "./gui/ProjectionSelector.js"; import { ProjectionEnum } from "./ProjectionEnum.js"; -import { Stack } from "./gui/Stack.js"; -import { CooGrid } from "./gui/CooGrid.js"; -import { ContextMenu } from "./gui/widgets/ContextMenu.js"; +import { ContextMenu } from "./gui/Widgets/ContextMenu.js"; import { SODAQueryWindow } from "./gui/SODAQueryWindow"; 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"; + import A from "./A.js"; import $ from 'jquery'; +import { Toolbar } from "./gui/Toolbar/Toolbar.js"; export let Aladin = (function () { @@ -99,70 +98,17 @@ export let Aladin = (function () { this.options = options; - $("").appendTo(aladinDiv); - this.aladinDiv = aladinDiv; this.reduceDeformations = true; // parent div $(aladinDiv).addClass("aladin-container"); - let cooFrame = CooFrameEnum.fromString(options.cooFrame, CooFrameEnum.J2000); - - // locationDiv is the div where we write the position - const locationDiv = $('
    ' - + (options.showFrame ? '' : '') - + '' - + '' - + '
    ') - .appendTo(aladinDiv); - const copyCoo = locationDiv.find('.aladin-clipboard'); - copyCoo.hide(); - copyCoo.click(function() { - self.copyCoordinatesToClipboard(); - }); - locationDiv.mouseenter(function() { - copyCoo.show(); - }); - locationDiv.mouseleave(function() { - copyCoo.hide(); - }); - - // div where FoV value is written - var fovDiv = $('
    ').appendTo(aladinDiv); - // zoom control if (options.showZoomControl) { $('').appendTo(aladinDiv); } - // maximize control - if (options.showFullscreenControl) { - $('
    ') - .appendTo(aladinDiv); - } - this.fullScreenBtn = $(aladinDiv).find('.aladin-fullscreenControl') - this.fullScreenBtn.click(function () { - self.toggleFullscreen(self.options.realFullscreen); - }); - // react to fullscreenchange event to restore initial width/height (if user pressed ESC to go back from full screen) - $(document).on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function (e) { - var fullscreenElt = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; - if (fullscreenElt === null || fullscreenElt === undefined) { - self.fullScreenBtn.removeClass('aladin-restore'); - self.fullScreenBtn.addClass('aladin-maximize'); - self.fullScreenBtn.attr('title', 'Full screen'); - $(self.aladinDiv).removeClass('aladin-fullscreen'); - - var fullScreenToggledFn = self.callbacksByEventName['fullScreenToggled']; - var isInFullscreen = self.fullScreenBtn.hasClass('aladin-restore'); - (typeof fullScreenToggledFn === 'function') && fullScreenToggledFn(isInFullscreen); - } - }); - // Aladin SODA form this.sodaQueryWindow = new SODAQueryWindow(this); @@ -176,16 +122,15 @@ export let Aladin = (function () { this.measurementTable = new MeasurementTable(aladinDiv); - - var location = new Location(locationDiv.find('.aladin-location-text')); + //var location = new Location(locationDiv.find('.aladin-location-text')); // set different options - this.view = new View(this, location, fovDiv, cooFrame, options.fov); + this.view = new View(this); this.cacheSurveys = new Map(); // Stack GUI - this.stack = new Stack(this.aladinDiv, this, this.view); - this.coogrid = new CooGrid(this.aladinDiv, this, this.view); + //this.stack = new StackMenu(this); + //this.coogrid = new CooGrid(this.aladinDiv, this, this.view); // Background color if (options.backgroundColor) { @@ -193,8 +138,9 @@ export let Aladin = (function () { this.setBackgroundColor(this.backgroundColor) } - this.boxes.push(this.stack); - this.boxes.push(this.coogrid); + //this.boxes.push(this.stack); + //this.boxes.push(this.coogrid); + // Grid var color, opacity; @@ -202,11 +148,11 @@ export let Aladin = (function () { color = options.gridOptions.color && Color.hexToRgb(options.gridOptions.color); opacity = options.gridOptions.opacity; } else { - color = {r:0.0, g:1.0, b:0.0}; - opacity = 0.5; + color = Aladin.DEFAULT_OPTIONS.gridColor; + opacity = Aladin.DEFAULT_OPTIONS.gridOpacity; } - this.view.setGridConfig({ + this.setCooGrid({ color: color, opacity: opacity, }); @@ -215,11 +161,6 @@ export let Aladin = (function () { this.showCooGrid(); } - if (options && (options.showProjectionControl === undefined || options.showProjectionControl === true)) { - // Projection selector - new ProjectionSelector(aladinDiv, this); - } - // Set the projection let projection = (options && options.projection) || 'SIN'; this.setProjection(projection) @@ -228,86 +169,35 @@ export let Aladin = (function () { // layers control panel // TODO : valeur des checkbox en fonction des options - ALEvent.LOADING_STATE.listenedBy(aladinDiv, function (e) { + ALEvent.LOADING_START.listenedBy(aladinDiv, function (e) { let layerControl = aladinDiv.querySelector(".aladin-layersControl"); if (layerControl) { - if (e.detail.loading) { - layerControl.style.backgroundImage = 'url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K)'; - } else { - layerControl.style.backgroundImage = 'url("data:image/gif;base64,R0lGODlhGQAcAMIAAAAAADQ0NKahocvFxf///wAAAAAAAAAAACH5BAEKAAcALAAAAAAZABwAAANneLoH/hCwyaJ1dDrCuydY1gBfyYUaaZqosq0r+sKxNNP1pe98Hy2OgXBILLZGxWRSBlA6iZjgczrwWa9WIEDA7Xq/R8d3PGaSz97oFs0WYN9wiDZAr9vvYcB9v2fy/3ZqgIN0cYZYCQA7")'; - } + layerControl.style.backgroundImage = 'url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K)'; + } + }); + + ALEvent.LOADING_STOP.listenedBy(aladinDiv, function (e) { + let layerControl = aladinDiv.querySelector(".aladin-layersControl"); + + if (layerControl) { + layerControl.style.backgroundImage = 'url("data:image/gif;base64,R0lGODlhGQAcAMIAAAAAADQ0NKahocvFxf///wAAAAAAAAAAACH5BAEKAAcALAAAAAAZABwAAANneLoH/hCwyaJ1dDrCuydY1gBfyYUaaZqosq0r+sKxNNP1pe98Hy2OgXBILLZGxWRSBlA6iZjgczrwWa9WIEDA7Xq/R8d3PGaSz97oFs0WYN9wiDZAr9vvYcB9v2fy/3ZqgIN0cYZYCQA7")'; } }); + this.toolbar = new Toolbar(this); + if (options.showLayersControl) { // button to show Stack interface - var d = $('
    '); - d.appendTo(aladinDiv); if (options.expandLayersControl) { self.hideBoxes(); self.showLayerBox(); } - - // we return false so that the default event is not submitted, and to prevent event bubbling - d.click(function () { - self.hideBoxes(); - self.showLayerBox(); - return false; - }); - top_px += 38; - } - - // goto control panel - if (options.showGotoControl) { - var d = $('
    '); - d.appendTo(aladinDiv); - - var gotoBox = - $('
    ' + - '×' + - '
    Go to:
    '); - gotoBox.appendTo(aladinDiv); - this.boxes.push(gotoBox); - - var input = gotoBox.find('.aladin-target-form input'); - input.on("paste keydown", function () { - $(this).removeClass('aladin-unknownObject'); // remove red border - }); - - // Unfocus the keyboard on android devices (maybe it concerns all smartphones) when the user click on enter - input.on("change", function () { - input.blur(); - }); - - // TODO : classe GotoBox - d.click(function () { - self.hideBoxes(); - input.val(''); - input.removeClass('aladin-unknownObject'); - gotoBox.show(); - input.blur(); - - return false; - }); - gotoBox.find('.aladin-closeBtn').click(function () { self.hideBoxes(); input.blur(); return false; }); - top_px += 38; - } - - // simbad pointer tool - if (options.showSimbadPointerControl) { - var d = $('
    '); - d.appendTo(aladinDiv); - - d.click(function () { - self.view.setMode(View.TOOL_SIMBAD_POINTER); - }); - top_px += 38; } // Coo grid pointer tool - if (options.showCooGridControl) { + /*if (options.showCooGridControl) { var d = $('
    '); d.appendTo(aladinDiv); @@ -348,7 +238,7 @@ export let Aladin = (function () { } }) top_px += 38; - } + }*/ // share control panel if (options.showShareControl) { @@ -389,8 +279,6 @@ export let Aladin = (function () { Logger.log("startup", params); } - this.showReticle(options.showReticle); - if (options.catalogUrls) { for (var k = 0, len = options.catalogUrls.length; k < len; k++) { this.createCatalogFromVOTable(options.catalogUrls[k]); @@ -427,16 +315,6 @@ export let Aladin = (function () { this.view.showCatalog(options.showCatalog); var aladin = this; - $(aladinDiv).find('.aladin-frameChoice').change(function () { - aladin.setFrame($(this).val()); - }); - - $(aladinDiv).find('.aladin-target-form').submit(function () { - aladin.gotoObject($(this).find('input').val(), function () { - $(aladinDiv).find('.aladin-target-form input').addClass('aladin-unknownObject'); - }); - return false; - }); var zoomPlus = $(aladinDiv).find('.zoomPlus'); zoomPlus.click(function () { @@ -458,23 +336,32 @@ export let Aladin = (function () { this.callbacksByEventName = {}; // we store the callback functions (on 'zoomChanged', 'positionChanged', ...) here - // initialize the Vue components - //if (typeof Vue != "undefined") { - //this.discoverytree = new DiscoveryTree(this); - //} - - this.view.redraw(); - + // FullScreen toolbar icon + this.isInFullscreen = false; // go to full screen ? if (options.fullScreen) { // 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) + $(document).on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function (e) { + var fullscreenElt = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; + if (fullscreenElt === null || fullscreenElt === undefined) { + $(self.aladinDiv).removeClass('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.attachTo(DefaultActionsForContextMenu.getDefaultActions(this)); + this.contextMenu.attach(DefaultActionsForContextMenu.getDefaultActions(this)); } if (options.samp) { @@ -483,6 +370,8 @@ export let Aladin = (function () { console.log('is hub running samp', e.detail.isHubRunning) }); } + // Reticle + this.reticle = new Reticle(this.options, this); }; /**** CONSTANTS ****/ @@ -509,47 +398,33 @@ export let Aladin = (function () { showGotoControl: true, showSimbadPointerControl: false, showShareControl: false, + showCooGridControl: false, + showFrame: true, showContextMenu: false, showCatalog: true, // TODO: still used ?? - showFrame: true, fullScreen: false, reticleColor: "rgb(178, 50, 178)", reticleSize: 22, + gridColor: "rgb(0, 255, 0)", + gridOpacity: 0.5, log: true, samp: true, allowFullZoomout: false, realFullscreen: false, - showAllskyRing: false, - allskyRingColor: '#c8c8ff', - allskyRingWidth: 8, + //showAllskyRing: false, + //allskyRingColor: '#c8c8ff', + //allskyRingWidth: 8, pixelateCanvas: true }; - Aladin.prototype.copyCoordinatesToClipboard = function() { - let copyTextEl = this.view.location.$div[0]; - var r = document.createRange(); - r.selectNode(copyTextEl); - window.getSelection().removeAllRanges(); - window.getSelection().addRange(r); - try { - let successful = document.execCommand('copy'); - let msg = successful ? 'successful' : 'unsuccessful'; - console.log('Copying text command was ' + msg); - } catch (err) { - console.log('Oops, unable to copy'); - } - window.getSelection().removeAllRanges(); - } - // 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; + //this.fullScreenBtn.attr('title', isInFullscreen ? 'Restore original size' : 'Full screen'); - this.fullScreenBtn.toggleClass('aladin-maximize aladin-restore'); - var isInFullscreen = this.fullScreenBtn.hasClass('aladin-restore'); - this.fullScreenBtn.attr('title', isInFullscreen ? 'Restore original size' : 'Full screen'); //$(this.aladinDiv).toggleClass('aladin-fullscreen'); if (this.aladinDiv.classList.contains('aladin-fullscreen')) { this.aladinDiv.classList.remove('aladin-fullscreen'); @@ -701,7 +576,7 @@ export let Aladin = (function () { } // màj select box - $(this.aladinDiv).find('.aladin-frameChoice').val(newFrame.label); + //$(this.aladinDiv).find('.aladin-frameChoice').val(newFrame.label); }; Aladin.prototype.setProjection = function (projection) { @@ -717,14 +592,14 @@ export let Aladin = (function () { let projName = undefined; for (let key in ProjectionEnum) { - if (ProjectionEnum[key].id == self.view.projection.id) { + if (ProjectionEnum[key] == self.view.projection) { projName = key; break; } }; return projName; - }; + };`` /** return the current coordinate system: possible values are 'J2000', 'J2000d', and 'Galactic' * @api @@ -806,7 +681,7 @@ export let Aladin = (function () { (typeof successCallback === 'function') && successCallback(self.getRaDec()); }, - function (data) { // errror callback + function (data) { // error callback if (console) { console.log("Could not resolve object name " + targetName); console.log(data); @@ -1040,6 +915,10 @@ export let Aladin = (function () { this.view.showHealpixGrid(show); }; + Aladin.prototype.healpixGrid = function () { + return this.view.displayHpxGrid; + }; + Aladin.prototype.showSurvey = function (show) { this.view.showSurvey(show); }; @@ -1047,9 +926,15 @@ export let Aladin = (function () { this.view.showCatalog(show); }; Aladin.prototype.showReticle = function (show) { - this.view.showReticle(show); - $('#displayReticle').attr('checked', show); + console.log("show", show, this.reticle) + this.reticle.show(show) + //$('#displayReticle').attr('checked', show); + }; + + Aladin.prototype.getReticle = function () { + return this.reticle; }; + Aladin.prototype.removeLayers = function () { this.view.removeLayers(); }; @@ -1170,19 +1055,9 @@ export let Aladin = (function () { // @api // @old Aladin.prototype.setBackgroundColor = function(rgb) { - let color; - if (typeof rgb === "string") { - var rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); - - var r = parseInt(rgb[1]); - var g = parseInt(rgb[2]); - var b = parseInt(rgb[3]); + - color = { r: r, g: g, b: b }; - } else { - color = rgb; - } - this.backgroundColor = color; + this.backgroundColor = new Color(rgb); // Once the wasm is ready, send the color to change it ALEvent.AL_USE_WASM.dispatchedTo(document.body, {callback: (wasm) => { @@ -1274,11 +1149,6 @@ export let Aladin = (function () { return this.view.selectedLayer; } - // Get the list of image layer overlays - Aladin.prototype.getImageOverlays = function () { - return this.view.overlayLayers; - } - // Get the list of overlays Aladin.prototype.getOverlays = function () { return this.view.allOverlayLayers; @@ -1373,12 +1243,17 @@ export let Aladin = (function () { callbackFn(this.view.selectedObjects); } } + else if (what === 'simbad') { + this.view.setMode(View.TOOL_SIMBAD_POINTER); + } }; Aladin.prototype.hideBoxes = function () { if (this.boxes) { for (var k = 0; k < this.boxes.length; k++) { - this.boxes[k].hide(); + if (typeof this.boxes[k].hide === "function") { + this.boxes[k].hide(); + } } } }; @@ -1390,12 +1265,34 @@ export let Aladin = (function () { // TODO : LayerBox (or Stack?) must be extracted as a separate object Aladin.prototype.showLayerBox = function () { - this.stack.show(); + this.stack.showImageLayerBox(); }; - Aladin.prototype.showCooGridBox = function () { + /*Aladin.prototype.showCooGridBox = function () { this.coogrid.show(); - }; + };*/ + + /** + * Change the coo grid options + * @param {color: String | {r: Float, g: Float, b: Float}, labelSize: Float, thickness: Float, opacity: Float} options - Represents the structure of the Tabs + */ + 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}); @@ -1693,9 +1590,17 @@ export let Aladin = (function () { * @return the current FoV size in degrees as a 2-elements array */ 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]; }; diff --git a/src/js/Color.js b/src/js/Color.js index 9c51e2b2c..a310067a5 100644 --- a/src/js/Color.js +++ b/src/js/Color.js @@ -28,10 +28,39 @@ * *****************************************************************************/ export let Color = (function() { + let Color = function(label) { + if (label.r && label.g && label.b) { + return label; + } + + // hex string form + if (label.match(/^#?[0-9A-Fa-f]{6}$/) || label.match(/^#?[0-9A-Fa-f]{3}$/)) { + // if it is hexadecimal already, return it in its actual form + return Color.hexToRgb(label); + } + + // rgb string form + var rgb = label.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + + if (rgb) { + var r = parseInt(rgb[1]); + var g = parseInt(rgb[2]); + var b = parseInt(rgb[3]); + return {r: r, g: g, b: b} + } + + // fill style label + if (!Color.standardizedColors[label]) { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = label; + const colorHexa = ctx.fillStyle; + Color.standardizedColors[label] = colorHexa; + } + + return Color.standardizedColors[label]; + }; - let Color = {}; - Color.curIdx = 0; Color.colors = ['#ff0000', '#0000ff', '#99cc00', '#ffff00','#000066', '#00ffff', '#9900cc', '#0099cc', '#cc9900', '#cc0099', '#00cc99', '#663333', '#ffcc9a', '#ff9acc', '#ccff33', '#660000', '#ffcc33', '#ff00ff', '#00ff00', '#ffffff']; Color.standardizedColors = {}; @@ -60,7 +89,6 @@ export let Color = (function() { var g = parseInt(rgb[2]); var b = parseInt(rgb[3]); - var d = 0; // Counting the perceptive luminance - human eye favors green color... var a = 1 - ( 0.299 * r + 0.587 * g + 0.114 * b) / 255; @@ -108,6 +136,24 @@ export let Color = (function() { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } + Color.rgbaToHex = function (r, g, b, a) { + r = r.toString(16); + g = g.toString(16); + b = b.toString(16); + a = a.toString(16); + + if (r.length == 1) + r = "0" + r; + if (g.length == 1) + g = "0" + g; + if (b.length == 1) + b = "0" + b; + if (a.length == 1) + a = "0" + a; + + return "#" + r + g + b + a; + } + Color.standardizeColor = function(label) { if (label.match(/^#?[0-9A-Fa-f]{6}$/) || label.match(/^#?[0-9A-Fa-f]{3}$/)) { // if it is hexadecimal already, return it in its actual form diff --git a/src/js/ColorCfg.js b/src/js/ColorCfg.js index 7175cdd48..a7cc25ab2 100644 --- a/src/js/ColorCfg.js +++ b/src/js/ColorCfg.js @@ -208,12 +208,20 @@ return this.colormap; }; + ColorCfg.prototype.getReversed = function() { + return this.reversed; + }; + // @api ColorCfg.prototype.setCuts = function(lowCut, highCut) { this.minCut = lowCut; this.maxCut = highCut; }; + ColorCfg.prototype.getCuts = function() { + return [this.minCut, this.maxCut]; + }; + ColorCfg.COLORMAPS = []; return ColorCfg; diff --git a/src/js/ImageFITS.js b/src/js/ImageFITS.js index 61b0e71c3..b13e08211 100644 --- a/src/js/ImageFITS.js +++ b/src/js/ImageFITS.js @@ -110,6 +110,10 @@ export let ImageFITS = (function () { }); }; + ImageFITS.prototype.getAvailableFormats = function() { + return this.properties.formats; + } + // @api ImageFITS.prototype.setSaturation = function (saturation) { updateMetadata(this, () => { diff --git a/src/js/ImageSurvey.js b/src/js/ImageSurvey.js index a9962c0c3..11d6e760c 100644 --- a/src/js/ImageSurvey.js +++ b/src/js/ImageSurvey.js @@ -148,7 +148,7 @@ export let ImageSurvey = (function () { let self = this; self.query = (async () => { - let maxOrder, frame, tileSize, formats, minCutout, maxCutout, bitpix, skyFraction, minOrder, initialFov, initialRa, initialDec, hipsBody, isPlanetaryBody, dataproductSubtype; + let obsTitle, creatorDid, maxOrder, frame, tileSize, formats, minCutout, maxCutout, bitpix, skyFraction, minOrder, initialFov, initialRa, initialDec, hipsBody, isPlanetaryBody, dataproductSubtype; try { let properties; @@ -166,6 +166,8 @@ export let ImageSurvey = (function () { throw e; } + obsTitle = properties.obs_title; + creatorDid = properties.creator_did; // Give a better name if we have the HiPS metadata self.name = self.name || properties.obs_title; // Set it to a default value @@ -222,10 +224,8 @@ export let ImageSurvey = (function () { if (properties.hips_body !== undefined) { if (self.view.options.showFrame) { self.view.aladin.setFrame('J2000d'); - let frameChoiceElt = document.querySelectorAll('.aladin-location > .aladin-frameChoice')[0]; - frameChoiceElt.innerHTML = ''; } - } else { + } /*else { if (self.view.options.showFrame) { const cooFrame = CooFrameEnum.fromString(self.view.options.cooFrame, CooFrameEnum.J2000); let frameChoiceElt = document.querySelectorAll('.aladin-location > .aladin-frameChoice')[0]; @@ -234,7 +234,7 @@ export let ImageSurvey = (function () { + (cooFrame == CooFrameEnum.J2000d ? 'selected="selected"' : '') + '>J2000d'; } - } + }*/ } catch (e) { //console.error("Could not fetch properties for the survey ", self.id, " with the error:\n", e) /*if (!options.maxOrder) { @@ -266,6 +266,8 @@ export let ImageSurvey = (function () { } self.properties = { + creatorDid: creatorDid, + obsTitle: obsTitle, url: url, maxOrder: maxOrder, frame: frame, @@ -429,12 +431,26 @@ export let ImageSurvey = (function () { throw self.id + " does not provide jpeg tiles"; } + // Switch from png/webp/jpeg to fits + if ((self.imgFormat === 'png' || self.imgFormat === "webp" || self.imgFormat === "jpeg") && imgFormat === 'fits') { + if (self.properties.minCutout && self.properties.maxCutout) { + self.setCuts(self.properties.minCutout, self.properties.maxCutout) + } + // Switch from fits to png/webp/jpeg + } else if (self.imgFormat === "fits") { + self.setCuts(0.0, 1.0); + } + // Check if it is a fits self.imgFormat = imgFormat; }); }) }; + ImageSurvey.prototype.getAvailableFormats = function() { + return this.properties.formats; + } + // @api ImageSurvey.prototype.setOpacity = function (opacity) { let self = this; diff --git a/src/js/MeasurementTable.js b/src/js/MeasurementTable.js index f47112dc7..061c0970b 100644 --- a/src/js/MeasurementTable.js +++ b/src/js/MeasurementTable.js @@ -31,22 +31,19 @@ *****************************************************************************/ import { Color } from "./Color.js" -import { ActionButton } from "./gui/widgets/ActionButton.js"; +import { Layout } from "./gui/Layout.js"; +import { Tabs } from "./gui/Widgets/Tab.js"; +import { Table } from "./gui/Widgets/Table.js"; export let MeasurementTable = (function() { // constructor - function MeasurementTable(aladinLiteDiv) { - this.isShowing = false; - - let mainDiv = document.createElement('div'); - mainDiv.setAttribute("class", "aladin-measurement-div"); - this.element = mainDiv; - - aladinLiteDiv.appendChild(this.element); + function MeasurementTable(target) { + //this.isShowing = false; + this.target = target; } - MeasurementTable.prototype.updateTableBody = function() { + /*MeasurementTable.prototype.updateTableBody = function() { let tbody = this.element.querySelector('tbody'); tbody.innerHTML = ''; @@ -80,7 +77,7 @@ export let MeasurementTable = (function() { tbody.appendChild(trEl); }); - } + }*/ // show measurement associated with a given source MeasurementTable.prototype.showMeasurement = function(tables) { @@ -88,113 +85,56 @@ export let MeasurementTable = (function() { return; } - this.update(tables); - }; - - MeasurementTable.prototype.update = function(tables) { - this.tables = tables; - - this.curTableIdx = 0; - - let table = tables[this.curTableIdx]; - this.element.innerHTML = ""; - - /// Create tabs element - let tabsElement = this.createTabs(); - this.element.appendChild(tabsElement); - - /// Create table element - let tableElement = document.createElement('table'); - tableElement.style.borderColor = table['color']; - - // table header creation - const thead = MeasurementTable.createTableHeader(table); - // table body creation - const tbody = document.createElement('tbody'); - tableElement.appendChild(thead); - tableElement.appendChild(tbody); - - this.element.appendChild(tableElement); - this.updateTableBody(); - - this.show(); - } - - MeasurementTable.prototype.createTabs = function() { - let tabsElement = document.createElement('div') - tabsElement.setAttribute('class', 'tabs'); - - /// Create catalog tabs - let tabsButtonElement = []; - - let self = this; - this.tables.forEach(function(table, index) { - let tabButtonElement = document.createElement("button"); - tabButtonElement.setAttribute('title', table["name"]) - - tabButtonElement.innerText = table["name"]; - tabButtonElement.style.overflow = 'hidden'; - tabButtonElement.style.textOverflow = 'ellipsis'; - tabButtonElement.style.whiteSpace = 'nowrap'; - tabButtonElement.style.maxWidth = '20%'; - - tabButtonElement.addEventListener( - 'click', - () => { - self.curTableIdx = index; + let layout = tables.map((table) => { + let content = new Table(table); - let tableElement = self.element.querySelector('table'); - tableElement.style.borderColor = table["color"] - - let thead = self.element.querySelector("thead"); - // replace the old header with the one of the current table - thead.parentNode.replaceChild(MeasurementTable.createTableHeader(table), thead); - - self.updateTableBody() - } - ,false - ); - - tabButtonElement.style.backgroundColor = table["color"]; - - let hexStdColor = Color.standardizeColor(table["color"]); + //let backgroundColor = table["color"]; + let hexStdColor = Color.standardizeColor(table.color); let rgbColor = Color.hexToRgb(hexStdColor); rgbColor = 'rgb(' + rgbColor.r + ', ' + rgbColor.g + ', ' + rgbColor.b + ')'; - let labelColor = Color.getLabelColorForBackground(rgbColor); - tabButtonElement.style.color = labelColor - tabsButtonElement.push(tabButtonElement); - tabsElement.appendChild(tabButtonElement); + let textContent = '
    ' + + table.name + '
    '; + + let label = Layout.horizontal({ + layout: [ + '
    ', + textContent + ] + }); + + return { + title: table.name, + label: label, + content: content, + cssStyle: { + backgroundColor: rgbColor, + color: labelColor, + padding: '2px', + } + } }); - return tabsElement; - } - - MeasurementTable.createTableHeader = function(table) { - let theadElement = document.createElement('thead'); - var content = ''; - - for (let [_, field] of Object.entries(table["fields"])) { - if (field.name) { - content += '' + field.name + ''; - } + if (this.table) { + this.table.remove(); } - content += ''; - theadElement.innerHTML = content; - - return theadElement; - } - - MeasurementTable.prototype.show = function() { - this.element.style.visibility = "visible"; + this.table = new Tabs({ + layout: layout, + cssStyle: { + position: 'absolute', + bottom: '20px', + zIndex: 100, + maxWidth: '100%', + } + }, this.target); }; MeasurementTable.prototype.hide = function() { - this.curTableIdx = 0; - - this.element.style.visibility = "hidden"; + if (this.table) { + this.table.remove(); + } }; return MeasurementTable; diff --git a/src/js/ProjectionEnum.js b/src/js/ProjectionEnum.js index c35f9aa12..61dc7cf47 100644 --- a/src/js/ProjectionEnum.js +++ b/src/js/ProjectionEnum.js @@ -29,10 +29,10 @@ *****************************************************************************/ export let ProjectionEnum = { // Zenithal - TAN: {id: 1, fov: 180, label: "gnomonic"}, /* Gnomonic projection */ - STG: {id: 2, fov: 360, label: "stereographic"}, /* Stereographic projection */ - SIN: {id: 3, fov: 180, label: "orthographic"}, /* Orthographic */ - ZEA: {id: 4, fov: 360, label: "zenital equal-area"}, /* Equal-area */ + TAN: {id: 1, fov: 180, label: "gnomonic"}, /* Gnomonic projection */ + STG: {id: 2, fov: 360, label: "stereographic"}, /* Stereographic projection */ + SIN: {id: 3, fov: 180, label: "orthographic"}, /* Orthographic */ + ZEA: {id: 4, fov: 360, label: "zenital equal-area"}, /* Equal-area */ FEYE: {id: 5, fov: 190, label: "fish eye"}, AIR: {id: 6, fov: 360, label: "airy"}, //AZP: {fov: 180}, @@ -54,12 +54,13 @@ export let ProjectionEnum = { HPX: {id: 19, fov: 360, label: "healpix"}, }; +/* export let projectionNames = [ // Zenithal - "SIN", /* Orthographic */ - "TAN", /* Gnomonic projection */ - "STG", /* Stereographic projection */ - "ZEA", /* Equal-area */ + "SIN", // Orthographic + "TAN", // Gnomonic projection + "STG", // Stereographic projection + "ZEA", // Equal-area "FEYE", "AIR", //"AZP", @@ -80,3 +81,4 @@ export let projectionNames = [ // Hybrid "HPX" ] +*/ diff --git a/src/js/Reticle.js b/src/js/Reticle.js new file mode 100644 index 000000000..9d01ea929 --- /dev/null +++ b/src/js/Reticle.js @@ -0,0 +1,137 @@ +// 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 iconUrl from './../../assets/icons/reticle.svg' +import { Color } from './Color.js'; + +/****************************************************************************** + * Aladin Lite project + * + * File Source + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +import { Aladin } from "./Aladin"; + +export let Reticle = (function() { + // constructor + let Reticle = function(options, aladin) { + this.el = document.createElement('object'); + this.el.className = 'aladin-reticle'; + this.el.type = "image/svg+xml"; + this.el.data = iconUrl; + + aladin.aladinDiv.appendChild(this.el); + + let reticleColor = options && options.color || Aladin.DEFAULT_OPTIONS.reticleColor; + let reticleSize = options && options.size || Aladin.DEFAULT_OPTIONS.reticleSize; + + let reticleShow; + if (options.showReticle === undefined) { + reticleShow = Aladin.DEFAULT_OPTIONS.showReticle; + } else { + reticleShow = options && options.showReticle; + } + + let self = this; + this.loaded = new Promise((resolve, reject) => { + function handleLoad(event) { + /* Removes the listeners */ + self.el.removeEventListener('load', handleLoad); + resolve(event); // works just fine + } + + function handleError(event) { + /* Removes the listeners */ + self.el.removeEventListener('error', handleError); + reject(event); // works just fine + } + + self.el.addEventListener('load', handleLoad); + self.el.addEventListener('error', handleError); + }); + + this.setColor(reticleColor) + this.setSize(reticleSize); + this.show(reticleShow); + }; + + Reticle.prototype.setColor = async function(color) { + if (!color) { + return; + } + + await this.loaded; + + // 1. the user has maybe given some + let reticleColor = new Color(color); + // a dynamic way to set the color + this.color = 'rgb(' + reticleColor.r + ', ' + reticleColor.g + ', ' + reticleColor.b + ')'; + + this.el.contentDocument + .getElementById("reticle") + .setAttribute('fill', this.color); + + } + + Reticle.prototype.setSize = async function(size) { + if (!size) { + return; + } + + await this.loaded; + + this.size = size; + this.el.style.width = this.size + 'px'; + this.el.style.height = this.size + 'px'; + } + + Reticle.prototype.show = async function(show) { + if (show === undefined) { + return; + } + + await this.loaded; + + if (show === true) { + this.el.style.visibility = 'visible'; + } else { + this.el.style.visibility = 'hidden'; + } + + this.visible = show; + } + + Reticle.prototype.getColor = function() { + return this.color; + } + + Reticle.prototype.getSize = function() { + return this.size; + } + + Reticle.prototype.isVisible = function() { + return this.visible; + } + + return Reticle; + })(); + \ No newline at end of file diff --git a/src/js/Source.js b/src/js/Source.js index 22d1ce006..1e7023f4d 100644 --- a/src/js/Source.js +++ b/src/js/Source.js @@ -28,7 +28,7 @@ * *****************************************************************************/ -import { ActionButton } from "./gui/widgets/ActionButton"; +import { ActionButton } from "./gui/Widgets/ActionButton"; import { Datalink } from "./vo/Datalink"; import { Utils } from "./Utils"; import { ObsCore } from "./vo/ObsCore"; diff --git a/src/js/View.js b/src/js/View.js index dc511fefb..e318d304a 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -49,13 +49,12 @@ import { ColorCfg } from "./ColorCfg.js"; import { Footprint } from "./Footprint.js"; import { Selector } from "./Selector.js"; import $ from 'jquery'; -import { ActionButton } from "./gui/widgets/ActionButton.js"; import { ObsCore } from "./vo/ObsCore.js"; export let View = (function () { /** Constructor */ - function View(aladin, location, fovDiv, cooFrame, zoom) { + function View(aladin) { this.aladin = aladin; // Add a reference to the WebGL API this.options = aladin.options; @@ -65,6 +64,8 @@ export let View = (function () { this.loadingState = false; let self = this; + + self.redrawClbk = this.redraw.bind(this); // Init the WebGL context // At this point, the view has been created so the image canvas too try { @@ -123,8 +124,7 @@ export let View = (function () { this.aladinDiv.ondragover = Utils.dragOverHandler; - this.location = location; - this.fovDiv = fovDiv; + //this.location = location; this.mustClearCatalog = true; this.mode = View.PAN; @@ -142,11 +142,7 @@ export let View = (function () { this.viewCenter = { lon: lon, lat: lat }; // position of center of view - if (cooFrame) { - this.cooFrame = cooFrame; - } else { - this.cooFrame = CooFrameEnum.GAL; - } + this.cooFrame = CooFrameEnum.fromString(this.options.cooFrame, CooFrameEnum.J2000); // Frame setting this.changeFrame(this.cooFrame); @@ -159,7 +155,7 @@ export let View = (function () { const si = 500000.0; const alpha = 40.0; this.fovLimit = undefined; - let initialFov = zoom || 180.0; + let initialFov = this.options.fov || 180.0; this.pinchZoomParameters = { isPinching: false, // true if a pinch zoom is ongoing initialFov: undefined, @@ -272,6 +268,7 @@ export let View = (function () { resizeObserver.observe(this.aladinDiv); //$(window).resize(() => { self.fixLayoutDimensions(); + self.redraw() //self.requestRedraw(); //}); // in some contexts (Jupyter notebook for instance), the parent div changes little time after Aladin Lite creation @@ -510,6 +507,17 @@ export let View = (function () { const xymouse = Utils.relMouseCoords(e); + ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { + state: { + mode: view.mode, + dragging: view.dragging, + rightClickPressed: view.rightClick + }, + type: e.type, + xy: xymouse, + }); + + if (e.which === 3 || e.button === 2) { view.rightClick = true; view.rightClickTimeStart = Date.now(); @@ -568,11 +576,25 @@ export let View = (function () { }); $(view.catalogCanvas).bind("mouseup", function (e) { - if (view.rightClick) { + e.preventDefault(); + e.stopPropagation(); + const xymouse = Utils.relMouseCoords(e); + + ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { + state: { + mode: view.mode, + dragging: view.dragging, + rightClickPressed: view.rightClick + }, + xy: xymouse, + ev: e, + }); + + if (view.rightClick) { const rightClickDurationMs = Date.now() - view.rightClickTimeStart; - if (rightClickDurationMs<300) { - view.aladin.contextMenu && view.aladin.contextMenu._showMenu(e); + if (rightClickDurationMs < 300) { + view.aladin.contextMenu && view.aladin.contextMenu.show({e: e}); } view.rightClick = false; @@ -583,8 +605,21 @@ export let View = (function () { return; } }); + + // reacting on 'click' rather on 'mouseup' is more reliable when panning the view + $(view.catalogCanvas).bind("click mouseout touchend touchcancel", function (e) { + const xymouse = Utils.relMouseCoords(e); + + ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { + state: { + mode: view.mode, + dragging: view.dragging, + rightClickPressed: view.rightClick + }, + type: e.type, + ev: e, + }); - $(view.catalogCanvas).bind("click mouseout touchend touchcancel", function (e) { // reacting on 'click' rather on 'mouseup' is more reliable when panning the view if ((e.type === 'touchend' || e.type === 'touchcancel') && view.pinchZoomParameters.isPinching) { view.pinchZoomParameters.isPinching = false; view.pinchZoomParameters.initialFov = view.pinchZoomParameters.initialDistance = undefined; @@ -625,11 +660,8 @@ export let View = (function () { view.mustClearCatalog = true; view.dragCoo = null; - const xymouse = Utils.relMouseCoords(e); if (e.type === "mouseout" || e.type === "touchend" || e.type === "touchcancel") { - view.updateLocation(xymouse.x, xymouse.y, true); - if (e.type === "mouseout") { if (view.mode === View.TOOL_SIMBAD_POINTER) { view.setMode(View.PAN); @@ -642,6 +674,8 @@ export let View = (function () { if (view.mode == View.TOOL_SIMBAD_POINTER) { // call Simbad pointer or Planetary features GenericPointer(view, e); + // exit the simbad pointer mode + view.setMode(View.PAN); return; // when in TOOL_SIMBAD_POINTER mode, we do not call the listeners } @@ -731,8 +765,19 @@ export let View = (function () { var lastMouseMovePos = null; $(view.catalogCanvas).bind("mousemove touchmove", function (e) { e.preventDefault(); + const xymouse = Utils.relMouseCoords(e); + ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { + state: { + mode: view.mode, + dragging: view.dragging, + rightClickPressed: view.rightClick + }, + type: e.type, + xy: xymouse, + }); + if (view.rightClick) { var onRightClickMoveFunction = view.aladin.callbacksByEventName['rightClickMove']; if (typeof onRightClickMoveFunction === 'function') { @@ -796,14 +841,14 @@ export let View = (function () { return; } - if (!view.dragging && !view.moving) { + if (!view.dragging /*&& !view.moving*/) { view.updateObjectsLookup(); } - if (!view.dragging || hasTouchEvents) { + /*if (!view.dragging || hasTouchEvents) { // update location box - view.updateLocation(xymouse.x, xymouse.y, false); - } + view.updateLocation({mouseX: xymouse.x, mouseY: xymouse.y}); + }*/ if (!view.dragging) { // call listener of 'mouseMove' event @@ -883,9 +928,9 @@ export let View = (function () { view.wasm.moveMouse(s1.x, s1.y, s2.x, s2.y); view.wasm.goFromTo(s1.x, s1.y, s2.x, s2.y); - const [ra, dec] = view.wasm.getCenter(); - view.viewCenter.lon = ra; - view.viewCenter.lat = dec; + view.updateCenter(); + + ALEvent.POSITION_CHANGED.dispatchedTo(view.aladin.aladinDiv, view.viewCenter); // Apply position changed callback after the move view.throttledPositionChanged(); @@ -896,22 +941,32 @@ export let View = (function () { var eventCount = 0; var eventCountStart; var isTouchPad; - var oldTime = 0; - var newTime = 0; - $(view.catalogCanvas).on('wheel', function (event) { - event.preventDefault(); - event.stopPropagation(); + $(view.catalogCanvas).on('wheel', function (e) { + e.preventDefault(); + e.stopPropagation(); + + const xymouse = Utils.relMouseCoords(e); + + ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { + state: { + mode: view.mode, + dragging: view.dragging, + rightClickPressed: view.rightClick + }, + type: e.type, + xy: xymouse, + }); if (view.rightClick) { return; } - var delta = event.deltaY; + var delta = e.deltaY; // this seems to happen in context of Jupyter notebook --> we have to invert the direction of scroll // hope this won't trigger some side effects ... - if (event.hasOwnProperty('originalEvent')) { - delta = -event.originalEvent.deltaY; + if (e.hasOwnProperty('originalEvent')) { + delta = -e.originalEvent.deltaY; } // See https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript @@ -945,18 +1000,23 @@ export let View = (function () { }; if (isTouchPadDefined) { - if (isTouchPad) { - // touchpad - newTime = new Date().getTime(); + let dt = performance.now() - view.then + + let a0, a1; - //if ( newTime - oldTime > 20 ) { - triggerZoom(0.002); - oldTime = new Date().getTime(); - //} + // touchpad + if (isTouchPad) { + a1 = 0.002; + a0 = 0.00002; } else { - // mouse - triggerZoom(0.007); + a1 = 0.0025; + a0 = 0.0001; } + + const alpha = Math.pow(view.fov / view.fovLimit, 0.3); + + const lerp = a0 * alpha + a1 * (1.0 - alpha); + triggerZoom(lerp); } if (!view.debounceProgCatOnZoom) { @@ -990,21 +1050,6 @@ export let View = (function () { view.displayReticle = true; }; - View.prototype.updateLocation = function (mouseX, mouseY, isViewCenterPosition) { - if (isViewCenterPosition) { - this.location.update(this.viewCenter.lon, this.viewCenter.lat, this.cooFrame, true); - } else { - let radec = this.wasm.screenToWorld(mouseX, mouseY); // This is given in the frame of the view - if (radec) { - if (radec[0] < 0) { - radec = [radec[0] + 360.0, radec[1]]; - } - - this.location.update(radec[0], radec[1], this.cooFrame, false); - } - } - } - View.prototype.requestRedrawAtDate = function (date) { this.dateRequestDraw = date; }; @@ -1027,9 +1072,9 @@ export let View = (function () { // request another frame // Elapsed time since last loop - const now = Date.now(); + const now = performance.now(); const elapsedTime = now - this.then; - + this.dt = elapsedTime; // If enough time has elapsed, draw the next frame //if (elapsedTime >= View.FPS_INTERVAL) { // Get ready for next frame by setting then=now, but also adjust for your @@ -1051,7 +1096,7 @@ export let View = (function () { this.then = now; //this.then = now % View.FPS_INTERVAL; - requestAnimFrame(this.redraw.bind(this)); + requestAnimFrame(this.redrawClbk); //} }; @@ -1133,8 +1178,8 @@ export let View = (function () { ////// 4. Draw reticle /////// // TODO: reticle should be placed in a static DIV, no need to waste a canvas - var reticleCtx = catalogCtx; - if (this.mode == View.SELECT) { + //var reticleCtx = catalogCtx; + /*if (this.mode == View.SELECT) { // VIEW mode, we do not want to display the reticle in this // but draw a selection box if (this.dragging) { @@ -1179,10 +1224,10 @@ export let View = (function () { } reticleCtx.drawImage(this.reticleCache, this.width / 2 - this.reticleCache.width / 2, this.height / 2 - this.reticleCache.height / 2); } - } + }*/ - ////// 5. Draw all-sky ring ///// - if (this.projection == ProjectionEnum.SIN && this.fov >= 60 && this.aladin.options['showAllskyRing'] === true) { + ////// 5. Draw all-sky ring. This option is now disabled in v3, it is too projection dependant ///// + /*if (this.projection == ProjectionEnum.SIN && this.fov >= 60 && this.aladin.options['showAllskyRing'] === true) { if (!catalogCanvasCleared) { reticleCtx.clearRect(0, 0, this.width, this.height); catalogCanvasCleared = true; @@ -1196,7 +1241,7 @@ export let View = (function () { const radius = (maxCxCy - (ringWidth / 2.0) + 1) / this.zoomFactor; reticleCtx.arc(this.cx, this.cy, radius, 0, 2 * Math.PI, true); reticleCtx.stroke(); - } + }*/ }; View.prototype.refreshProgressiveCats = function () { @@ -1327,26 +1372,28 @@ export let View = (function () { } View.prototype.setGridConfig = function (gridCfg) { - this.wasm.setGridConfig(gridCfg); + this.gridCfg = {...this.gridCfg, ...gridCfg}; + this.wasm.setGridConfig(this.gridCfg); // send events - if (gridCfg) { - if (gridCfg.hasOwnProperty('enabled')) { - if (gridCfg.enabled === true) { - ALEvent.COO_GRID_ENABLED.dispatchedTo(this.aladinDiv); - } - else { - ALEvent.COO_GRID_DISABLED.dispatchedTo(this.aladinDiv); - } + /*if (this.gridCfg.hasOwnProperty('enabled')) { + if (this.gridCfg.enabled === true) { + ALEvent.COO_GRID_ENABLED.dispatchedTo(this.aladinDiv); } - if (gridCfg.color) { - ALEvent.COO_GRID_UPDATED.dispatchedTo(this.aladinDiv, { color: gridCfg.color, opacity: gridCfg.opacity }); + else { + ALEvent.COO_GRID_DISABLED.dispatchedTo(this.aladinDiv); } - } + }*/ + + ALEvent.COO_GRID_UPDATED.dispatchedTo(this.aladinDiv, this.gridCfg); this.requestRedraw(); }; + View.prototype.getGridConfig = function() { + return this.gridCfg; + } + View.prototype.updateZoomState = function () { // Get the new zoom values from the backend this.zoomFactor = this.wasm.getClipZoomFactor(); @@ -1361,31 +1408,12 @@ export let View = (function () { this.fov = fov; this.computeNorder(); - // Update the lower left FoV div - if (isNaN(this.fov)) { - this.fovDiv.html("FoV:"); - return; - } - var fovStr; - /*if (this.projection == ProjectionEnum.SIN && fov >= 180.0) { - fov = 180.0; - } else if (fov >= 360.0) { - fov = 360.0; - }*/ - if (this.projection.fov <= fov) { - fov = this.projection.fov; - } + let fovX = this.fov; + let fovY = this.height / this.width * fovX; + fovX = Math.min(fovX, 360); + fovY = Math.min(fovY, 180); - if (fov > 1) { - fovStr = Math.round(fov * 100) / 100 + "°"; - } - else if (fov * 60 > 1) { - fovStr = Math.round(fov * 60 * 100) / 100 + "'"; - } - else { - fovStr = Math.round(fov * 3600 * 100) / 100 + '"'; - } - this.fovDiv.html("FoV: " + fovStr); + ALEvent.ZOOM_CHANGED.dispatchedTo(this.aladinDiv, { fovX: fovX, fovY: fovY }); }; /** @@ -1461,7 +1489,10 @@ export let View = (function () { const promise = imageLayer.add(layer); self.loadingState = true; - ALEvent.LOADING_STATE.dispatchedTo(this.aladinDiv, { loading: true }); + ALEvent.LOADING_START.dispatchedTo(this.aladinDiv, { + label: layer.layer, + msg: 'Load the layer: ' + layer.name + }); return promise; }) @@ -1482,7 +1513,7 @@ export let View = (function () { .finally(() => { // Loading state is over self.loadingState = false; - ALEvent.LOADING_STATE.dispatchedTo(this.aladinDiv, { loading: false }); + ALEvent.LOADING_STOP.dispatchedTo(this.aladinDiv, { label: layer.layer }); self.imageLayersBeingQueried.delete(layer); @@ -1553,12 +1584,14 @@ export let View = (function () { } View.prototype.removeImageLayer = function (layer) { + console.log("remove image layer", layer) // Get the survey to remove to dissociate it from the view let imageLayer = this.imageLayers.get(layer); if (imageLayer === undefined) { // there is nothing to remove return; } + console.log("remove image layer", layer) // Update the backend if (imageLayer.added) { @@ -1573,6 +1606,7 @@ export let View = (function () { // layer not found return; } + console.log("remove image layer", layer) // Delete it this.imageLayers.delete(layer); @@ -1591,6 +1625,7 @@ export let View = (function () { } } + console.log("removed image layer", layer) ALEvent.HIPS_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: layer }); // check if there are no more surveys @@ -1622,110 +1657,35 @@ export let View = (function () { this.needRedraw = true; }; - View.prototype.setProjection = function (projectionName) { + View.prototype.setProjection = function (projName) { this.fovLimit = 1000.0; - /* - TAN: {id: 1, fov: 180}, - STG: {id: 2, fov: 360}, - SIN: {id: 3, fov: 180}, - ZEA: {id: 4, fov: 360}, - FEYE: {id: 5, fov: 190}, - AIR: {id: 6, fov: 360}, - //AZP: {fov: 180}, - ARC: {id: 7, fov: 360}, - NCP: {id: 8, fov: 180}, - // Cylindrical - MER: {id: 9, fov: 360}, - CAR: {id: 10, fov: 360}, - CEA: {id: 11, fov: 360}, - CYP: {id: 12, fov: 360}, - // Pseudo-cylindrical - AIT: {id: 13, fov: 360}, - PAR: {id: 14, fov: 360}, - SFL: {id: 15, fov: 360}, - MOL: {id: 16, fov: 360}, - // Conic - COD: {id: 17, fov: 360}, - // Hybrid - HPX: {id: 19, fov: 360}, - */ - switch (projectionName) { + this.projection = ProjectionEnum[projName]; + switch (projName) { // Zenithal (TAN, STG, SIN, ZEA, FEYE, AIR, AZP, ARC, NCP) case "TAN": - this.projection = ProjectionEnum.TAN; - this.fovLimit = 180.0; - break; - case "STG": - this.projection = ProjectionEnum.STG; + this.fovLimit = 150.0; break; - case "SIN": - this.projection = ProjectionEnum.SIN; - break; - case "ZEA": - this.projection = ProjectionEnum.ZEA; - break; - case "FEYE": - this.projection = ProjectionEnum.FEYE; - break; - case "AIR": - this.projection = ProjectionEnum.AIR; - break; - case "ARC": - this.projection = ProjectionEnum.ARC; - break; - case "NCP": - this.projection = ProjectionEnum.NCP; - break; - case "ARC": - this.projection = ProjectionEnum.ARC; - break; - case "ZEA": - this.projection = ProjectionEnum.ZEA; - break; - // Pseudo-cylindrical (AIT, MOL, PAR, SFL) - case "MOL": - this.projection = ProjectionEnum.MOL; - break; - case "AIT": - this.projection = ProjectionEnum.AIT; - break; - case "PAR": - this.projection = ProjectionEnum.PAR; - break; - case "SFL": - this.projection = ProjectionEnum.SFL; - break; - // Cylindrical (MER, CAR, CEA, CYP) case "MER": - this.projection = ProjectionEnum.MER; this.fovLimit = 360.0; break; case "CAR": - this.projection = ProjectionEnum.CAR; this.fovLimit = 360.0; break; case "CEA": - this.projection = ProjectionEnum.CEA; this.fovLimit = 360.0; break; case "CYP": - this.projection = ProjectionEnum.CYP; this.fovLimit = 360.0; break; - // Conic - case "COD": - this.projection = ProjectionEnum.COD; - break; // Hybrid case "HPX": - this.projection = ProjectionEnum.HPX; this.fovLimit = 360.0; break; default: break; } // Change the projection here - this.wasm.setProjection(projectionName); + this.wasm.setProjection(projName); this.updateZoomState(); this.requestRedraw(); @@ -1751,14 +1711,19 @@ export let View = (function () { } // Get the new view center position (given in icrs) - let [ra, dec] = this.wasm.getCenter(); - this.viewCenter.lon = ra; - this.viewCenter.lat = dec; - this.location.update(this.viewCenter.lon, this.viewCenter.lat, this.cooFrame, true); + this.updateCenter(); + + ALEvent.FRAME_CHANGED.dispatchedTo(this.aladinDiv, {cooFrame: this.cooFrame}); this.requestRedraw(); }; + View.prototype.updateCenter = function() { + const [ra, dec] = this.wasm.getCenter(); + this.viewCenter.lon = ra; + this.viewCenter.lat = dec; + } + View.prototype.showHealpixGrid = function (show) { this.displayHpxGrid = show; @@ -1813,16 +1778,17 @@ export let View = (function () { } this.viewCenter.lon = ra; this.viewCenter.lat = dec; - this.location.update(this.viewCenter.lon, this.viewCenter.lat, this.cooFrame, true); + //this.updateLocation({lon: this.viewCenter.lon, lat: this.viewCenter.lat}); // Put a javascript code here to do some animation this.wasm.setCenter(this.viewCenter.lon, this.viewCenter.lat); + ALEvent.POSITION_CHANGED.dispatchedTo(this.aladin.aladinDiv, this.viewCenter); + this.requestRedraw(); var self = this; setTimeout(function () { self.refreshProgressiveCats(); }, 1000); - // Apply position changed callback after the move self.throttledPositionChanged(); }; diff --git a/src/js/events/ALEvent.js b/src/js/events/ALEvent.js index f52b19cc6..fac0eb829 100644 --- a/src/js/events/ALEvent.js +++ b/src/js/events/ALEvent.js @@ -33,7 +33,8 @@ export class ALEvent { static AL_USE_WASM = new ALEvent("AL:Wasm"); - static LOADING_STATE = new ALEvent("AL:Layer.loading"); + static LOADING_START = new ALEvent("AL:loading.started"); + static LOADING_STOP = new ALEvent("AL:loading.stopped"); static BACKGROUND_COLOR_CHANGED = new ALEvent("AL:BackgroundColor.changed") @@ -42,6 +43,10 @@ export class ALEvent { static COO_GRID_UPDATED = new ALEvent("AL:cooGrid.updated"); static PROJECTION_CHANGED = new ALEvent("AL:projection.changed"); + static FRAME_CHANGED = new ALEvent("AL:frame.changed"); + + static POSITION_CHANGED = new ALEvent("AL:position.changed"); + static ZOOM_CHANGED = new ALEvent("AL:zoom.changed"); static HIPS_LAYER_ADDED = new ALEvent("AL:HiPSLayer.added"); static HIPS_LAYER_REMOVED = new ALEvent("AL:HiPSLayer.removed"); @@ -56,6 +61,7 @@ export class ALEvent { static GRAPHIC_OVERLAY_LAYER_CHANGED = new ALEvent("AL:GraphicOverlayLayer.changed"); static SAMP_AVAILABILITY = new ALEvent("AL:samp.started"); + static CANVAS_EVENT = new ALEvent("AL:Event"); constructor(name) { this.name = name; diff --git a/src/js/gui/CatalogSelector.js b/src/js/gui/CatalogSelector.js index 533ff7680..6c92af618 100644 --- a/src/js/gui/CatalogSelector.js +++ b/src/js/gui/CatalogSelector.js @@ -48,7 +48,7 @@ import $ from 'jquery'; const self = this; this.mainDiv = document.createElement('div'); - this.mainDiv.classList.add('aladin-dialog'); + this.mainDiv.classList.add('aladin-box', 'aladin-anchor-center'); this.mainDiv.style.display = 'block'; const autocompleteId = 'autocomplete-' + Utils.uuidv4(); diff --git a/src/js/gui/FoV.js b/src/js/gui/FoV.js new file mode 100644 index 000000000..b495550ea --- /dev/null +++ b/src/js/gui/FoV.js @@ -0,0 +1,99 @@ +// 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 { Numbers } from "../libs/astro/coo.js"; +import { Layout } from "./Layout.js"; + +import { DOMElement } from "./widgets/Widget.js"; + +import { ALEvent } from "../events/ALEvent.js"; + +export class FoV extends DOMElement { + // constructor + constructor(aladin) { + let el = Layout.horizontal({ + layout: [ + '
    ', + '
    ×
    ', + '
    ', + ], + tooltip: { + content: 'Field of View in ra and dec direction', + position: { direction: 'bottom' }, + } + }); + el.addClass('aladin-fov'); + + super(el) + + let self = this; + ALEvent.ZOOM_CHANGED.listenedBy(aladin.aladinDiv, function (e) { + let [fovXDeg, fovYDeg] = aladin.getFov(); + + self._update(fovXDeg, fovYDeg) + }); + + let [fovXDeg, fovYDeg] = aladin.getFov(); + self._update(fovXDeg, fovYDeg) + }; + + _update(fovXDeg, fovYDeg) { + let [fovX, fovY] = this.el.querySelectorAll('.aladin-monospace-text') + fovX.innerText = this._format(fovXDeg) + fovY.innerText = this._format(fovYDeg) + } + + _format(fovDeg) { + let suffix; + let fov; + if (Math.floor(fovDeg) == 0) { + let fovMin = fovDeg*60.0; + + if (Math.floor(fovMin) == 0) { + // sec + suffix = '"'; + fov = fovMin*60.0; + } else { + // min + suffix = '\''; + fov = fovMin; + } + } else { + // d + suffix = '°'; + fov = fovDeg; + } + + return Numbers.toDecimal(fov, 1) + suffix; + } + +}; + diff --git a/src/js/gui/HiPSLayer.js b/src/js/gui/HiPSLayer.js index 1f60ee0ed..5df688f80 100644 --- a/src/js/gui/HiPSLayer.js +++ b/src/js/gui/HiPSLayer.js @@ -30,129 +30,290 @@ import { ImageLayer } from "../ImageLayer.js"; import { ALEvent } from "../events/ALEvent.js"; -import { HiPSSelector } from "./HiPSSelector.js"; +import { HiPSSelectorBox } from "./HiPSSelectorBox.js"; -import $ from 'jquery'; -import { ActionButton } from "./widgets/ActionButton.js"; +import { ActionButton } from "./Widgets/ActionButton.js"; +import { ContextMenu } from "./Widgets/ContextMenu.js"; +import { Box } from "./Widgets/Box.js"; +import { Form } from "./Widgets/Form.js"; +import { Layout } from "./Layout.js"; +import selectIconImg from '../../../assets/icons/select.svg'; +import searchIconImg from '../../../assets/icons/search.svg'; +import { DOMElement } from "./Widgets/Widget.js"; -export class HiPSLayer { +export class HiPSLayer extends DOMElement { // Constructor - constructor(aladin, layer) { - this.aladin = aladin; - this.layer = layer; - this.hidden = false; - this.lastOpacity = 1.0; + constructor(aladin, layer, parent) { + //aladin = aladin; + //layer = layer; + let hipsSelector; - // HiPS header div - this.headerDiv = $( - '
    ' + - '
    ' + - '
    ' - ); - - let layerHeaderEl = this.headerDiv[0].querySelector(".aladin-layer-header"); - - let self = this; - - let clickOpenerBtn = new ActionButton({ - content: "▶", - backgroundColor: '#eaeaea', - color: 'black', - info: 'Open the survey edition panel', - action(e) { - if (clickOpenerBtn.opt.content === '▶') { - clickOpenerBtn.attach({ - info: 'Close the survey edition panel', - content: '▼', - }); - self.mainDiv.slideDown(300); - } - else { - clickOpenerBtn.attach({ - info: 'Open the survey edition panel', - content: '▶', - }); - self.mainDiv.slideUp(300); - } - } - }, layerHeaderEl); + let boxes = []; - layerHeaderEl.appendChild((() => { - let selector = $(''); - return selector[0]; - })()); - - let hideLayerBtn = new ActionButton({ + let hideBtn = ActionButton.createIconBtn({ content: "👁️", - backgroundColor: '#eaeaea', - info: 'Hide the layer', - action(e) { - self.hidden = !self.hidden; - let opacitySlider = self.mainDiv.find('.opacity').eq(0); - - let newOpacity = 0.0; - if (self.hidden) { - self.lastOpacity = self.layer.getOpacity(); - hideLayerBtn.attach({content: ' '}); - } else { - newOpacity = self.lastOpacity; - hideLayerBtn.attach({content: '👁️'}); + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + }, + tooltip: {content: 'Hide the layer', position: {direction: 'right'}} + }); + + let layoutImageSelection = { + label: Layout.horizontal({ + layout: [ + ActionButton.createIconBtn({ + iconURL: selectIconImg, + tooltip: {content: 'Select an image layer among those already defined', position: { direction: 'bottom' }}, + cssStyle: { + backgroundPosition: 'center center', + backgroundColor: '#bababa', + border: '1px solid rgb(72, 72, 72)', + cursor: 'help', + }, + }), + 'Choose a survey' + ] + }), + subMenu: [] + }; + + layoutImageSelection.subMenu.push({ + label: Layout.horizontal({layout: [ + ActionButton.createIconBtn({ + iconURL: searchIconImg, + tooltip: {content: 'Search for a new survey in our database...', position: {direction: 'bottom'}}, + cssStyle: { + backgroundPosition: 'center center', + backgroundColor: '#bababa', + border: '1px solid rgb(72, 72, 72)', + cursor: 'help', + }, + }), + 'Search for a new survey' + ]}), + action(o) { + if (!hipsSelector) { + hipsSelector = new HiPSSelectorBox(aladin, layer); } - // Update the opacity slider - opacitySlider.val(newOpacity); - opacitySlider.get(0).disabled = self.hidden; + } + }) - self.layer.setOpacity(newOpacity); + let action = (e) => { + let layerName = e.srcElement.innerText; + let cfg = ImageLayer.LAYERS.find((layer) => layer.name === layerName); + let newLayer; + + // Max order is specific for surveys + if (cfg.subtype === "fits") { + // FITS + newLayer = aladin.createImageFITS( + cfg.url, + cfg.name, + cfg.options, + ); + } else { + // HiPS + newLayer = aladin.createImageSurvey( + cfg.id, + cfg.name, + cfg.url, + undefined, + cfg.maxOrder, + cfg.options + ); } - }, layerHeaderEl); - new ActionButton({ - content: "🔍", - backgroundColor: '#eaeaea', - info: 'Search for a survey (HiPS)', - action(e) { - if (!self.hipsSelector) { - self.hipsSelector = new HiPSSelector(self.aladin.aladinDiv, (IDOrURL) => { - const layerName = self.layer.layer; - self.aladin.setOverlayImageLayer(IDOrURL, layerName); - }, self.aladin); - } - - self.hipsSelector.show(); + aladin.setOverlayImageLayer(newLayer, layer.layer); + } + + // Sort the layers by name order + let layers = ImageLayer.LAYERS.sort(function (a, b) { + if (!a.order) { + return a.name > b.name ? 1 : -1; } - }, layerHeaderEl); + return a.maxOrder && a.maxOrder > b.maxOrder ? 1 : -1; + }); + + for(let layer of layers) { + layoutImageSelection.subMenu.push({label: layer.name, action}) + } - let deleteLayerBtn = new ActionButton({ - content: "❌", - backgroundColor: '#eaeaea', - info: 'Delete this layer', + let updateContextMenu = () => { + ContextMenu.getInstance(aladin).attach([ + layoutImageSelection, + { + label: "Edit", + subMenu: [ + { + label: 'Color panel', + action(o) { + let box = HiPSLayer.createColorBox(aladin, layer, parent); + boxes.push(box); + } + }, + { + label: 'Tile format', + subMenu: [ + { + label: 'webp', + disabled: !layer.getAvailableFormats().includes('webp'), + selected: layer.imgFormat === 'webp', + action(o) { + layer.setImageFormat('webp'); + } + }, + { + label: 'jpeg', + disabled: !layer.getAvailableFormats().includes('jpeg'), + selected: layer.imgFormat === 'jpeg', + action(o) { + layer.setImageFormat('jpeg'); + } + }, + { + label: 'png', + disabled: !layer.getAvailableFormats().includes('png'), + selected: layer.imgFormat === 'png', + action(o) { + layer.setImageFormat('png'); + } + }, + { + label: 'fits', + disabled: !layer.getAvailableFormats().includes('fits'), + selected: layer.imgFormat === 'fits', + action(o) { + layer.setImageFormat('fits'); + } + } + ] + }, + { + label: 'Stretch', + subMenu: [ + { + label: 'pow', + selected: layer.getColorCfg().stretch === 'pow2', + action(o) { + layer.setColormap(layer.getColorCfg().getColormap(), { stretch: 'pow2' }); + } + }, + { + label: 'linear', + selected: layer.getColorCfg().stretch === 'linear', + action(o) { + layer.setColormap(layer.getColorCfg().getColormap(), { stretch: 'linear' }); + } + }, + { + label: 'sqrt', + selected: layer.getColorCfg().stretch === 'sqrt', + action(o) { + layer.setColormap(layer.getColorCfg().getColormap(), { stretch: 'sqrt' }); + console.log(layer.getColorCfg().stretch === 'sqrt') + } + }, + { + label: 'asinh', + selected: layer.getColorCfg().stretch === 'asinh', + action(o) { + layer.setColormap(layer.getColorCfg().getColormap(), { stretch: 'asinh' }); + } + }, + { + label: 'log', + selected: layer.getColorCfg().stretch === 'log', + action(o) { + layer.setColormap(layer.getColorCfg().getColormap(), { stretch: 'log' }); + } + } + ] + } + ] + }, + { + label: Layout.horizontal({layout: [hideBtn, (() => { if(layer.getOpacity() === 0.0) {return 'Show'} else {return 'Hide'}})()]}), + action(o) { + let opacity = layer.getOpacity(); + if (opacity === 0.0) { + layer.setOpacity(1.0); + } else { + layer.setOpacity(0.0); + } + } + }, + { + label: Layout.horizontal({layout: [ + ActionButton.createIconBtn({ + content: "❌", + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + }, + }), + "Delete layer" + ]}), + disabled: layer.layer === aladin.getBaseImageLayer().layer, + action(o) { + aladin.removeImageLayer(layer.layer); + } + }, + ]); + }; + + let settingsBtn = ActionButton.createIconBtn({ + content: "☰", + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + fontSize: '1.5em', + textAlign: 'center', + lineHeight: '0px', + }, + tooltip: {content: 'Layer settings', position: {direction: 'bottom'} }, action(e) { - self.aladin.aladinDiv.dispatchEvent(new CustomEvent('remove-layer', { - detail: self.layer.layer - })); + ContextMenu.getInstance(aladin)._hide() + updateContextMenu(); + ContextMenu.getInstance(aladin).show(e, { + position: { + anchor: settingsBtn.element(), + direction: 'bottom' + } + }) } - }, layerHeaderEl); + }); + + // HiPS header div + let el = Layout.horizontal({layout: [settingsBtn, '
    ' + layer.name + '
    ']}).element(); + el.classList.add("aladin-stack-item"); + super(el) + this.boxes = boxes; // Add a centered on button for images - if (this.layer.subtype === "fits") { + /*if (this.layer.subtype === "fits") { let layerSelector = this.headerDiv[0].querySelector(".aladin-layerSelection"); - new ActionButton({ + ActionButton.createIconBtn({ content: "🎯", - backgroundColor: '#eaeaea', + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + }, info: 'Focus on the FITS', action(e) { self.layer.focusOn(); } }, layerSelector, 'afterend'); - } + }*/ - if (this.layer.layer === "base") { - deleteLayerBtn.attach({backgroundColor: 'lightgray', disable: true}); - } // HiPS main options div - let cmListStr = ''; + /*let cmListStr = ''; for (const cm of this.aladin.wasm.getAvailableColormapList()) { cmListStr += ''; } @@ -191,319 +352,195 @@ export class HiPSLayer { } self._updateLayersDropdownList(); }; - ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, this.layerChangedListener); + ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, this.layerChangedListener);*/ } - destroy() { - ALEvent.HIPS_LAYER_CHANGED.remove(this.aladin.aladinDiv, this.layerChangedListener); + remove() { + // remove the floating boxes first + for (let box of this.boxes) { + box.remove(); + } + super.remove() } - _addListeners() { - const self = this; - // HEADER DIV listeners - - this.headerDiv.off("click"); - this.headerDiv.on("click", () => { - self.aladin.aladinDiv.dispatchEvent(new CustomEvent('select-layer', { - detail: self - })); - }) - - // Click on aladin options should select the layer clicked - // Update list of surveys - self._updateLayersDropdownList(); - const layerSelector = this.headerDiv.find('.aladin-layerSelection'); - layerSelector.off("change"); - layerSelector.on("change", (e) => { - let cfg = ImageLayer.LAYERS[layerSelector[0].selectedIndex]; - let layer; - - // Max order is specific for surveys - if (cfg.subtype === "fits") { - // FITS - layer = self.aladin.createImageFITS( - cfg.url, - cfg.name, - cfg.options, - ); - } else { - // HiPS - layer = self.aladin.createImageSurvey( - cfg.id, - cfg.name, - cfg.url, - undefined, - cfg.maxOrder, - cfg.options - ); - } - - if (self.hidden) { - layer.setAlpha(0.0); - } - - self.aladin.setOverlayImageLayer(layer, self.layer.layer); - }); - - // MAIN DIV listeners - // blending method - const blendingSelector = this.mainDiv.find('.blending').eq(0); - - blendingSelector.off("change"); - blendingSelector.change(function () { - let mode = blendingSelector.val() - self.layer.setBlendingConfig( mode === "additive" ); - }); - - // image format - const format4ImgLayer = this.mainDiv.find('.format').eq(0); - const minCut4ImgLayer = this.mainDiv.find('.min-cut').eq(0); - const maxCut4ImgLayer = this.mainDiv.find('.max-cut').eq(0); - format4ImgLayer.off("change"); - format4ImgLayer.on("change", function () { - const imgFormat = format4ImgLayer.val(); - - self.layer.setImageFormat(imgFormat); - - let minCut = 0; - let maxCut = 1; - - if (imgFormat === "fits") { - // FITS format - minCut = self.layer.properties.minCutout; - maxCut = self.layer.properties.maxCutout; - } - self.layer.setCuts(minCut, maxCut); - - // update the cuts only - minCut4ImgLayer.val(parseFloat(minCut.toFixed(5))); - maxCut4ImgLayer.val(parseFloat(maxCut.toFixed(5))); - }); - // min/max cut - minCut4ImgLayer.off("change blur"); - maxCut4ImgLayer.off("change blur"); - minCut4ImgLayer.add(maxCut4ImgLayer).on('change blur', function (e) { - let minCutValue = parseFloat(minCut4ImgLayer.val()); - let maxCutValue = parseFloat(maxCut4ImgLayer.val()); - - if (isNaN(minCutValue) || isNaN(maxCutValue)) { - return; + static createColorBox(aladin, layer, parent) { + let box; + let contentBox; + let settingsSelector = new Form({ + label: "Settings", + name: 'param', + type: 'select', + value: 'colormap', + options: ['colormap', 'pixel cutouts', 'post-fx', 'blending'], + change(e) { + let val = e.target.value; + + let content; + switch (val) { + case 'colormap': + content = colorSettings; + break; + case 'pixel cutouts': + content = pixelCutsSettings; + break; + case 'post-fx': + content = postFXSettings; + break; + case 'blending': + content = blendingSettings; + break; + } + box.update({ + title: "Color settings", + content: Layout.vertical({ + layout: ['For ' + layer.name, settingsSelector, content] + }) + }) } - self.layer.setCuts(minCutValue, maxCutValue); - }); - - // colormap - const colorMapSelect4ImgLayer = this.mainDiv.find('.colormap-selector').eq(0); - const stretchSelect4ImgLayer = this.mainDiv.find('.stretch').eq(0); - const reverseCmCb = this.mainDiv.find('.reversed').eq(0); - - reverseCmCb.off("change"); - colorMapSelect4ImgLayer.off("change"); - stretchSelect4ImgLayer.off("change"); - colorMapSelect4ImgLayer.add(reverseCmCb).add(stretchSelect4ImgLayer).change(function () { - const stretch = stretchSelect4ImgLayer.val(); - const reverse = reverseCmCb[0].checked; - - // Color map case - const cmap = colorMapSelect4ImgLayer.val(); - self.layer.setColormap(cmap, { reversed: reverse, stretch: stretch }); - }); - - // opacity - const opacity4ImgLayer = self.mainDiv.find('.opacity').eq(0); - opacity4ImgLayer.off("input"); - opacity4ImgLayer.on('input', function () { - const opacity = +opacity4ImgLayer.val(); - self.layer.setOpacity(opacity); - }); - - // gamma - const gamma4ImgLayer = self.mainDiv.find('.gamma').eq(0); - gamma4ImgLayer.off("change blur"); - gamma4ImgLayer.on('change blur', function () { - const gamma = parseFloat(gamma4ImgLayer.val()) || 1.0; - - self.layer.setGamma(gamma); + }) - const trueGamma = self.layer.getColorCfg().getGamma(); - if (gamma !== trueGamma) { - gamma4ImgLayer.val(trueGamma); - } + let cssForm = { + border: '2px solid #d2d2d2', + margin: '0px', + padding: '4px' + }; + // Color Settings form + let colorSettings = new Form({ + cssStyle: cssForm, + subInputs: [ + { + label: "Colormap", + name: 'cmap', + type: "select", + value: layer.getColorCfg().getColormap(), + options: aladin.wasm.getAvailableColormapList(), + change(e) { + let cmap = e.target.value; + layer.setColormap(cmap); + } + }, + { + label: "Reverse", + name: 'reversed', + type: "checkbox", + checked: layer.getColorCfg().getReversed(), + change(e) { + let reversed = e.target.checked; + layer.setColormap(layer.getColorCfg().getColormap(), { reversed: reversed }); + } + } + ] }); - // saturation - const sat4ImgLayer = self.mainDiv.find('.saturation').eq(0); - sat4ImgLayer.off("input"); - sat4ImgLayer.on('input', function (e) { - const saturation = parseFloat(sat4ImgLayer.val()) || 0.0; + ALEvent.HIPS_LAYER_CHANGED.listenedBy(aladin.aladinDiv, (e) => { + const layer = e.detail.layer; + if (layer.layer === layer.layer) { + let colorCfg = layer.getColorCfg(); - self.layer.setSaturation(saturation); + let cmap = colorCfg.getColormap(); + let reversed = colorCfg.getReversed(); - const trueSaturation = self.layer.getColorCfg().getSaturation(); - if (saturation !== trueSaturation) { - sat4ImgLayer.val(trueSaturation); + colorSettings.set('cmap', cmap); + colorSettings.set('reversed', reversed); } }); - // contrast - const contrast4ImgLayer = self.mainDiv.find('.contrast').eq(0); - contrast4ImgLayer.off("input"); - contrast4ImgLayer.on('input', function (e) { - const contrast = parseFloat(contrast4ImgLayer.val()) || 0.0; - - self.layer.setContrast(contrast); - - const trueContrast = self.layer.getColorCfg().getContrast(); - if (contrast !== trueContrast) { - contrast4ImgLayer.val(trueContrast); - } + // Pixel cutouts form + let pixelCutsSettings = new Form({ + cssStyle: cssForm, + subInputs: [ + { + label: "Mincut", + name: "mincut", + type: "number", + value: layer.getColorCfg().getCuts()[0], + change(e) { + let lowCut = +e.target.value; + layer.setCuts(lowCut, layer.getColorCfg().getCuts()[1]); + } + }, + { + label: "Maxcut", + name: "maxcut", + type: "number", + value: layer.getColorCfg().getCuts()[1], + change(e) { + let highCut = +e.target.value; + layer.setCuts(layer.getColorCfg().getCuts()[0], highCut); + } + } + ] }); - // brightness - const brightness4ImgLayer = self.mainDiv.find('.brightness').eq(0); - brightness4ImgLayer.off("input"); - brightness4ImgLayer.on('input', function (e) { - const brightness = parseFloat(brightness4ImgLayer.val()) || 0.0; - - self.layer.setBrightness(brightness); - - const trueBrightness = self.layer.getColorCfg().getBrightness(); - if (brightness !== trueBrightness) { - brightness4ImgLayer.val(trueBrightness); + ALEvent.HIPS_LAYER_CHANGED.listenedBy(aladin.aladinDiv, (e) => { + const layer = e.detail.layer; + if (layer.layer === layer.layer) { + let [minCut, maxCut] = layer.getColorCfg().getCuts(); + pixelCutsSettings.set('mincut', minCut); + pixelCutsSettings.set('maxcut', maxCut); } }); - } - - _updateHiPSLayerOptions() { - const reverseCmCb = this.mainDiv.find('.reversed').eq(0); - const colorMapSelect4ImgLayer = this.mainDiv.find('.colormap-selector').eq(0); - const stretchSelect4ImgLayer = this.mainDiv.find('.stretch').eq(0); - const formatSelect4ImgLayer = this.mainDiv.find('.format').eq(0); - const opacity4ImgLayer = this.mainDiv.find('.opacity').eq(0); - const gamma4ImgLayer = this.mainDiv.find('.gamma').eq(0); - const contrast4ImgLayer = this.mainDiv.find('.contrast').eq(0); - const brightness4ImgLayer = this.mainDiv.find('.brightness').eq(0); - const sat4ImgLayer = this.mainDiv.find('.saturation').eq(0); - const blendingSelect4ImgLayer = this.mainDiv.find('.blending').eq(0); - - const minCut = this.mainDiv.find('.min-cut').eq(0); - const maxCut = this.mainDiv.find('.max-cut').eq(0); - - formatSelect4ImgLayer.empty(); - - this.layer.properties.formats.forEach(fmt => { - formatSelect4ImgLayer.append($('