diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 57160ccb1..867f43c5e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -21,6 +21,7 @@ jobs: run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" + rustup default nightly - name: "Install wasm-pack" run: | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e9c092e9..cc5afb278 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" + rustup default nightly - name: "Install wasm-pack" run: | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y diff --git a/.gitignore b/.gitignore deleted file mode 100644 index bd402ac05..000000000 --- a/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -node_modules - -dist -examples/.DS_Store -examples/fits -.DS_Store -package-lock.json -src/core/target/ -src/core/Cargo.lock - -aladin-lite*.tgz - -.vscode - -deploy.sh -deploy-beta.sh 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/camera.svg b/assets/icons/camera.svg new file mode 100644 index 000000000..a5036b9f7 --- /dev/null +++ b/assets/icons/camera.svg @@ -0,0 +1,5 @@ + + + + + \ 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/copy.svg b/assets/icons/copy.svg new file mode 100644 index 000000000..29c228971 --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1,7 @@ + + + + + + + \ 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/finish.svg b/assets/icons/finish.svg new file mode 100644 index 000000000..711ed70da --- /dev/null +++ b/assets/icons/finish.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ 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/help.svg b/assets/icons/help.svg new file mode 100644 index 000000000..83d83794f --- /dev/null +++ b/assets/icons/help.svg @@ -0,0 +1,2 @@ + + \ 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/info.svg b/assets/icons/info.svg new file mode 100644 index 000000000..bfc9e20d9 --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/loading.svg b/assets/icons/loading.svg new file mode 100644 index 000000000..ae475d1a2 --- /dev/null +++ b/assets/icons/loading.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/icons/map.svg b/assets/icons/map.svg new file mode 100644 index 000000000..f7d54bb04 --- /dev/null +++ b/assets/icons/map.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg new file mode 100644 index 000000000..b334f2dfc --- /dev/null +++ b/assets/icons/maximize.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/minus.svg b/assets/icons/minus.svg new file mode 100644 index 000000000..bc96d6103 --- /dev/null +++ b/assets/icons/minus.svg @@ -0,0 +1,4 @@ + + + + \ 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/plus.svg b/assets/icons/plus.svg new file mode 100644 index 000000000..637f5d9f0 --- /dev/null +++ b/assets/icons/plus.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..4255c3bcc --- /dev/null +++ b/assets/icons/projection.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + \ 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..8e2aad9d7 --- /dev/null +++ b/assets/icons/restore.svg @@ -0,0 +1,8 @@ + + + + + + + + \ 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-white.svg b/assets/icons/search-white.svg new file mode 100644 index 000000000..4a06d0bed --- /dev/null +++ b/assets/icons/search-white.svg @@ -0,0 +1,4 @@ + + + + \ 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/share.svg b/assets/icons/share.svg new file mode 100644 index 000000000..e25e1d111 --- /dev/null +++ b/assets/icons/share.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ 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/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/icons/telescope.svg b/assets/icons/telescope.svg new file mode 100644 index 000000000..9d3ce1319 --- /dev/null +++ b/assets/icons/telescope.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ 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/tooltip.svg b/assets/icons/tooltip.svg new file mode 100644 index 000000000..1ec834709 --- /dev/null +++ b/assets/icons/tooltip.svg @@ -0,0 +1,2 @@ + + \ 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/assets/icons/wave-off.svg b/assets/icons/wave-off.svg new file mode 100644 index 000000000..76d8038df --- /dev/null +++ b/assets/icons/wave-off.svg @@ -0,0 +1,12 @@ + + + + radio-waves-off + + + + + + + + \ No newline at end of file diff --git a/assets/icons/wave-on.svg b/assets/icons/wave-on.svg new file mode 100644 index 000000000..17b3b8b2e --- /dev/null +++ b/assets/icons/wave-on.svg @@ -0,0 +1,12 @@ + + + + radio-waves + + + + + + + + \ 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-adass2022.html b/examples/al-adass2022.html index 6c6063780..ad96b8084 100644 --- a/examples/al-adass2022.html +++ b/examples/al-adass2022.html @@ -10,7 +10,7 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {survey: ["P/PanSTARRS/DR1/color-i-r-g"], showReticle: false, gridOptions: {opacity: 0.5}, projection: "AIT", cooFrame: 'galactic', target: "93.2721132 -20.9942421", fov: 1000, showGotoControl: false, showFrame: false, fullScreen: true, showLayersControl: true, showCooGrid: false, showCooGridControl: false}); + aladin = A.aladin('#aladin-lite-div', {survey: ["P/PanSTARRS/DR1/color-i-r-g"], showReticle: false, gridOptions: {opacity: 0.5, color: 'rgba(255, 0, 0)'}, projection: "AIT", cooFrame: 'icrs', target: "stephan's quintet", fov: 1000, showGotoControl: false, showFrame: false, fullScreen: true, showLayersControl: true, showCooGrid: true, showCooGridControl: false}); const chft = aladin.createImageSurvey('CFHT', "CFHT deep view of NGC7331 and Stephan's quintet u+g+r", "https://cds.unistra.fr/~derriere/PR_HiPS/2022_Duc/", null, null, {imgFormat: 'png'}); const nircamJWST = aladin.createImageSurvey('Nircam', "Stephans Quintet NIRCam+MIRI", "http://alasky.cds.unistra.fr/JWST/CDS_P_JWST_Stephans-Quintet_NIRCam+MIRI/", null, null, {imgFormat: 'png', colormap: "viridis"}); @@ -21,7 +21,7 @@ aladin.getOverlayImageLayer("CFHT").toggle(); aladin.getOverlayImageLayer("Nircam").toggle(); - let fov = 1000; + let fov = 360; let rotation = 0; setInterval(function zoom() { @@ -30,7 +30,7 @@ rotation += 0.07; aladin.setRotation(rotation) - aladin.setFov(fov); + aladin.setFoV(fov); if (fov < 3 && fov > 0.5) { let opacity = 1.0 - (fov - 0.5)/(3 - 0.5); diff --git a/examples/al-animation-CS-CDS-2022.html b/examples/al-animation-CS-CDS-2022.html index a5d2ca832..1840629b8 100644 --- a/examples/al-animation-CS-CDS-2022.html +++ b/examples/al-animation-CS-CDS-2022.html @@ -71,9 +71,9 @@ aladin.setFoV(400) await delay(2000) - aladin.setProjection('HPX') - aladin.setFoV(400) - await delay(2000) + //aladin.setProjection('HPX') + //aladin.setFoV(400) + //await delay(2000) aladin.setProjection('AIT') aladin.setFoV(400) @@ -326,7 +326,7 @@ await delay(1000) - notify('Projection support:', 'Orthographic, Mercator, HEALPix, Aitoff', 6500) + notify('Projection support:', 'Orthographic, Mercator, Aitoff', 6500) await delay(1000) diff --git a/examples/al-catalog-hips.html b/examples/al-catalog-hips.html index fce33d494..379623bd6 100644 --- a/examples/al-catalog-hips.html +++ b/examples/al-catalog-hips.html @@ -11,7 +11,7 @@ let aladin; A.init.then(() => { aladin = A.aladin('#aladin-lite-div', {target: 'LMC', fov: 55, showContextMenu: true}); - var hips = A.catalogHiPS('https://axel.u-strasbg.fr/HiPSCatService/Simbad', {onClick: 'showTable', name: 'Simbad'}); + var hips = A.catalogHiPS('https://axel.u-strasbg.fr/HiPSCatService/Simbad', {onClick: 'showPopup', name: 'Simbad'}); aladin.addCatalog(hips); }); 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/examples/al-customize-button.html b/examples/al-customize-button.html new file mode 100644 index 000000000..6c6fc5b15 --- /dev/null +++ b/examples/al-customize-button.html @@ -0,0 +1,80 @@ + + + + + + +
+ + + + + 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/examples/al-displayFITS.html b/examples/al-displayFITS.html index 59859a809..107080c75 100644 --- a/examples/al-displayFITS.html +++ b/examples/al-displayFITS.html @@ -22,6 +22,7 @@ { minCut: 5000, maxCut: 17000, + colormap: 'viridis' }, // no optional params (ra, dec, fov, image) => { // ra, dec and fov are centered around the fits image diff --git a/examples/al-displayJPG.html b/examples/al-displayJPG.html index db635cec6..265c5dc7d 100644 --- a/examples/al-displayJPG.html +++ b/examples/al-displayJPG.html @@ -23,7 +23,9 @@ aladin = A.aladin('#aladin-lite-div', {target: "0 0", cooFrame: "gal"}); var callback = function(ra, dec, fov) { - aladin.addCatalog(A.catalogFromSimbad( {ra: ra, dec: dec} , 1 , {shape: 'circle', color: '#5d5', onClick: 'showTable'})); + A.catalogFromSimbad( {ra: ra, dec: dec} , 1 , {shape: 'circle', color: '#5d5', onClick: 'showTable'}, (cat) => { + aladin.addCatalog(cat) + }); // we must return true, so that the default action (set view to center of image) is performed return true; } @@ -39,7 +41,9 @@ // the JPG to transform to HiPS 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg', // no options - {}, + { + transparency: 0.6, + }, // A callback fn once the overlay is set callback ); diff --git a/examples/al-easy-access-simbad-ned.html b/examples/al-easy-access-simbad-ned.html index fb8adff2e..64340aa28 100644 --- a/examples/al-easy-access-simbad-ned.html +++ b/examples/al-easy-access-simbad-ned.html @@ -5,15 +5,33 @@
- + diff --git a/examples/al-easy-access-vizier.html b/examples/al-easy-access-vizier.html index c568cfc38..3c1d5e2b3 100644 --- a/examples/al-easy-access-vizier.html +++ b/examples/al-easy-access-vizier.html @@ -9,12 +9,34 @@ diff --git a/examples/al-ellipse.html b/examples/al-ellipse.html index 3281d7d80..651c4d6ca 100644 --- a/examples/al-ellipse.html +++ b/examples/al-ellipse.html @@ -12,6 +12,8 @@ A.init.then(() => { // Start up Aladin Lite aladin = A.aladin('#aladin-lite-div', {survey: "CDS/P/DSS2/color", target: 'M 31', fov: 3}); + + var overlay = A.graphicOverlay({color: '#ee2345', lineWidth: 3}); aladin.addOverlay(overlay); overlay.addFootprints([ diff --git a/examples/al-gw.html b/examples/al-gw.html index bee55f15e..fab63c514 100644 --- a/examples/al-gw.html +++ b/examples/al-gw.html @@ -4,16 +4,18 @@
+ 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-obscore.html b/examples/al-obscore.html index 635fe0425..16731de62 100644 --- a/examples/al-obscore.html +++ b/examples/al-obscore.html @@ -10,15 +10,13 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {target: '14 18 16.868 +56 44 29.37', fov: 360, projection: 'AIT', showContextMenu: true}); + aladin = A.aladin('#aladin-lite-div', {target: 'NGC 1367', fov: 360, samp: false, projection: 'AIT', fullScreen: true, showContextMenu: true}); A.catalogFromURL('https://raw.githubusercontent.com/VisIVOLab/SKA-Discovery-Service-Mockup/main/ObsCore/ObsCore_003.xml', {onClick: 'showTable'}, (catalog) => { aladin.addCatalog(catalog) }); - A.catalogFromVizieR('B/assocdata/obscore', '14 18 16.868 +56 44 29.37', 100, {onClick: 'showTable', limit: 1000}, (catalog) => { - aladin.addCatalog(catalog); - }); + aladin.addCatalog(A.catalogFromVizieR("B/assocdata/obscore", "0 +0", 20, {limit: 1000})) }); diff --git a/examples/al-panstarrs-id.html b/examples/al-panstarrs-id.html new file mode 100644 index 000000000..de191c464 --- /dev/null +++ b/examples/al-panstarrs-id.html @@ -0,0 +1,32 @@ + + + + + + +
+ + + + + diff --git a/examples/al-response-div.html b/examples/al-response-div.html deleted file mode 100644 index f330d96ab..000000000 --- a/examples/al-response-div.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - -
- - - - - diff --git a/examples/al-simbad-density.html b/examples/al-simbad-density.html index cf86e2c81..205a5e38e 100644 --- a/examples/al-simbad-density.html +++ b/examples/al-simbad-density.html @@ -9,7 +9,7 @@ let aladin; A.init.then(() => { // Start up Aladin Lite - aladin = A.aladin('#aladin-lite-div', {fov: 180.0, fullScreen: true, survey: "CDS/P/DM/simbad-biblio/allObjects", target: '12 25 41.512 +12 48 47.2', showCooGrid: true}); + aladin = A.aladin('#aladin-lite-div', {fov: 180.0, fullScreen: true, survey: "https://alaskybis.cds.unistra.fr/ancillary/simbad-biblio/allObjects", target: '12 25 41.512 +12 48 47.2', showCooGrid: true}); aladin.setProjection("TAN"); aladin.getBaseImageLayer().setColormap("redtemperature") }); diff --git a/examples/al-snippet.html b/examples/al-snippet.html index b54fc09a0..380fbe10c 100644 --- a/examples/al-snippet.html +++ b/examples/al-snippet.html @@ -5,7 +5,7 @@
- + + + diff --git a/examples/al-soda-ska.html b/examples/al-soda-ska.html new file mode 100644 index 000000000..f57ee40d5 --- /dev/null +++ b/examples/al-soda-ska.html @@ -0,0 +1,33 @@ + + + + + + +
+ + + + diff --git a/examples/al-stcs-footprints.html b/examples/al-stcs-footprints.html index c5da6ff3b..d20252e7c 100644 --- a/examples/al-stcs-footprints.html +++ b/examples/al-stcs-footprints.html @@ -11,10 +11,10 @@ A.init.then(() => { // Start up Aladin Lite - let aladin = A.aladin('#aladin-lite-div', {survey: "CDS/P/DSS2/color", target: 'Sgr a*', fov: 0.5}); + let aladin = A.aladin('#aladin-lite-div', {survey: "CDS/P/DSS2/color", target: 'Sgr a*', fov: 0.5, showContextMenu: true}); // This table contains a s_region column containing stcs expressed regions // that are automatically parsed - aladin.addCatalog(A.catalogFromURL('https://aladin.cds.unistra.fr/AladinLite/doc/API/examples/data/alma-footprints.xml', {onClick: 'showTable'})); + aladin.addCatalog(A.catalogFromURL('https://aladin.cds.unistra.fr/AladinLite/doc/API/examples/data/alma-footprints.xml', {name: 'ALMA footprints', onClick: 'showTable'})); }); diff --git a/examples/al-vertices.html b/examples/al-vertices.html new file mode 100644 index 000000000..a407c093d --- /dev/null +++ b/examples/al-vertices.html @@ -0,0 +1,25 @@ + + + + + + +
+ + + + + diff --git a/examples/al-zoom-meerkat.html b/examples/al-zoom-meerkat.html index ffb0e6db5..138a62142 100644 --- a/examples/al-zoom-meerkat.html +++ b/examples/al-zoom-meerkat.html @@ -12,17 +12,17 @@ let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {survey: ["P/Mellinger"], cooFrame: 'galactic', fov: 1000, fullScreen: true, showCooGrid: true}); + aladin = A.aladin('#aladin-lite-div', {survey: ["P/Mellinger"], cooFrame: 'galactic', fov: 1000, fullScreen: true}); const meerkat = aladin.newImageSurvey('P/MeerKAT/Galactic-Centre-1284MHz-StokesI', {imgFormat: 'fits'}); meerkat.setColormap('magma', {stretch: "asinh"}) aladin.setOverlayImageLayer(meerkat) - aladin.setProjection("MOL") + aladin.setProjection("AIT") let fov = 1000; setInterval(function zoom() { if (fov > 1) { fov *= 0.995; - aladin.setFov(fov); + aladin.setFoV(fov); } }, 10); diff --git a/examples/index.html b/examples/index.html index 24d3a21df..9849dac16 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,17 +1,16 @@ - + -
+
' + NL; - s += ''; - - return s; -}; - -// @API -/* - * Creates remotely a HiPS from a FITS image URL and displays it - */ -Aladin.prototype.displayFITS = function ( - url, - options, - successCallback, - errorCallback, - layer = "base" -) { - successCallback = successCallback || ((ra, dec, fov, _) => { - this.gotoRaDec(ra, dec); - this.setFoV(fov); - }); - const imageFits = this.createImageFITS(url, url, options, successCallback, errorCallback); - return this.setOverlayImageLayer(imageFits, layer); -}; - -// @API -/* - * Creates remotely a HiPS from a JPEG or PNG image with astrometry info - * and display it + // @API + /* + * hide popup + */ + Aladin.prototype.hidePopup = function () { + this.popup.hide(); + }; + + // @API + /* + * return a URL allowing to share the current view + */ + Aladin.prototype.getShareURL = function () { + var radec = this.getRaDec(); + var coo = new Coo(); + coo.prec = 7; + coo.lon = radec[0]; + coo.lat = radec[1]; + + return Aladin.URL_PREVIEWER + '?target=' + encodeURIComponent(coo.format('s')) + + '&fov=' + this.getFov()[0].toFixed(2) + '&survey=' + encodeURIComponent(this.getBaseImageLayer().id || this.getBaseImageLayer().rootUrl); + }; + + // @API + /* + * return, as a string, the HTML embed code + */ + Aladin.prototype.getEmbedCode = function () { + var radec = this.getRaDec(); + var coo = new Coo(); + coo.prec = 7; + coo.lon = radec[0]; + coo.lat = radec[1]; + + var survey = this.getBaseImageLayer().url; + var fov = this.getFov()[0]; + let s = ''; + const NL = "\n"; + s += '
' + NL; + s += '' + NL; + s += ''; + + return s; + }; + +/** + * Display a JPEG image in the Aladin Lite view. + * + * @memberof Aladin + * + * @param {string} url - The URL of the JPEG image. + * @param {Object} options - Options to customize the display. Can include the following properties: + * @param {string} options.label - A label for the displayed image. + * @param {number} options.order - The desired HEALPix order format. + * @param {boolean} options.nocache - True if you want to disable the cache + * @param {number} options.transparency - Opacity of the image rendered in aladin lite. Between 0 and 1. + * @param {Function} successCallback - The callback function to be executed on a successful display. + * The callback gives the ra, dec, and fov of the image; + * @param {Function} errorCallback - The callback function to be executed if an error occurs during display. + * + * @example + * aladin.displayJPG( + * // the JPG to transform to HiPS + * 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg', + * { + * transparency: 0.6, + * label: 'NOIRLab image' + * }, + * (ra, dec, fov) => { + * // your code here + * }) + *); */ -Aladin.prototype.displayJPG = Aladin.prototype.displayPNG = function (url, options, successCallback, errorCallback) { - options = options || {}; - options.color = true; - options.label = "JPG/PNG image"; - options.outputFormat = 'png'; + Aladin.prototype.displayFITS = function ( + url, + options, + successCallback, + errorCallback, + layer = "base" + ) { + successCallback = successCallback || ((ra, dec, fov, _) => { + this.gotoRaDec(ra, dec); + this.setFoV(fov); + }); + const imageFits = this.createImageFITS(url, url, options, successCallback, errorCallback); + return this.setOverlayImageLayer(imageFits, layer); + }; - options = options || {}; +/** + * Display a JPEG image in the Aladin Lite view. + * + * @memberof Aladin + * + * @param {string} url - The URL of the JPEG image. + * @param {Object} options - Options to customize the display. Can include the following properties: + * @param {string} options.label - A label for the displayed image. + * @param {number} options.order - The desired HEALPix order format. + * @param {boolean} options.nocache - True if you want to disable the cache + * @param {number} options.transparency - Opacity of the image rendered in aladin lite. Between 0 and 1. + * @param {Function} successCallback - The callback function to be executed on a successful display. + * The callback gives the ra, dec, and fov of the image; + * @param {Function} errorCallback - The callback function to be executed if an error occurs during display. + * + * @example + * aladin.displayJPG( + * // the JPG to transform to HiPS + * 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg', + * { + * transparency: 0.6, + * label: 'NOIRLab image' + * }, + * (ra, dec, fov) => { + * // your code here + * }) + *); + */ + Aladin.prototype.displayJPG = Aladin.prototype.displayPNG = function (url, options, successCallback, errorCallback) { + options = options || {}; + options.color = true; + options.label = options.label || "JPG/PNG image"; + options.outputFormat = 'png'; - var data = { url: url }; - if (options.color) { - data.color = true; - } - if (options.outputFormat) { - data.format = options.outputFormat; - } - if (options.order) { - data.order = options.order; - } - if (options.nocache) { - data.nocache = options.nocache; - } - let self = this; + options = options || {}; - const request = ( url, params = {}, method = 'GET' ) => { - let options = { - method - }; - if ( 'GET' === method ) { - url += '?' + ( new URLSearchParams( params ) ).toString(); - } else { - options.body = JSON.stringify( params ); + var data = { url }; + if (options.color) { + data.color = true; + } + if (options.outputFormat) { + data.format = options.outputFormat; + } + if (options.order) { + data.order = options.order; } + if (options.nocache) { + data.nocache = options.nocache; + } + let self = this; - return fetch( url, options ).then( response => response.json() ); - }; - const get = ( url, params ) => request( url, params, 'GET' ); + const request = ( url, params = {}, method = 'GET' ) => { + let options = { + method + }; + if ( 'GET' === method ) { + url += '?' + ( new URLSearchParams( params ) ).toString(); + } else { + options.body = JSON.stringify( params ); + } - get('https://alasky.unistra.fr/cgi/fits2HiPS', data) - .then(async (response) => { - if (response.status != 'success') { - console.error('An error occured: ' + response.message); - if (errorCallback) { - errorCallback(response.message); + return fetch( url, options ).then( response => response.json() ); + }; + const get = ( url, params ) => request( url, params, 'GET' ); + + get('https://alasky.unistra.fr/cgi/fits2HiPS', data) + .then(async (response) => { + if (response.status != 'success') { + console.error('An error occured: ' + response.message); + if (errorCallback) { + errorCallback(response.message); + } + return; } - return; - } - var label = options.label || "FITS image"; - var meta = response.data.meta; + var label = options.label; + var meta = response.data.meta; - const survey = self.createImageSurvey(response.data.url, label, response.data.url); - self.setOverlayImageLayer(survey, "overlay"); + const survey = self.createImageSurvey(response.data.url, label, response.data.url); + self.setOverlayImageLayer(survey, "overlay"); - var transparency = (options && options.transparency) || 1.0; + var transparency = (options && options.transparency) || 1.0; + survey.setOpacity(transparency); - var executeDefaultSuccessAction = true; - if (successCallback) { - executeDefaultSuccessAction = successCallback(meta.ra, meta.dec, meta.fov); - } - if (executeDefaultSuccessAction === true) { - self.wasm.setCenter(meta.ra, meta.dec); - self.setFoV(meta.fov); - } + var executeDefaultSuccessAction = true; + if (successCallback) { + executeDefaultSuccessAction = successCallback(meta.ra, meta.dec, meta.fov); + } + if (executeDefaultSuccessAction === true) { + self.wasm.setCenter(meta.ra, meta.dec); + self.setFoV(meta.fov); + } - // TODO! set an image survey once the already loaded surveys - // are READY! Otherwise it can lead to some congestion and avoid - // downloading the base tiles of the other surveys loading! - // This has to be fixed in the backend but a fast fix is just to wait - // before setting a new image survey - }); -}; + // TODO! set an image survey once the already loaded surveys + // are READY! Otherwise it can lead to some congestion and avoid + // downloading the base tiles of the other surveys loading! + // This has to be fixed in the backend but a fast fix is just to wait + // before setting a new image survey + }); + }; -Aladin.prototype.setReduceDeformations = function (reduce) { - this.reduceDeformations = reduce; - this.view.requestRedraw(); -} + /* + Aladin.prototype.setReduceDeformations = function (reduce) { + this.reduceDeformations = reduce; + this.view.requestRedraw(); + } + */ + + return Aladin; +})(); diff --git a/src/js/AladinUtils.js b/src/js/AladinUtils.js index 834de95c2..c955598d1 100644 --- a/src/js/AladinUtils.js +++ b/src/js/AladinUtils.js @@ -27,13 +27,126 @@ * Author: Thomas Boch[CDS] * *****************************************************************************/ +import { HPXVertices } from "../core/pkg/core"; +import A from "./A"; +import { Aladin } from "./Aladin"; -import { Projection } from "./libs/astro/projection.js"; -import { CooFrameEnum } from "./CooFrameEnum.js"; +/** + * @namespace AladinUtils + * @description Aladin Lite utils API namespace for basic functions + */ +export let AladinUtils = { + /** + * @namespace HEALPix + * @memberof AladinUtils + * @description Namespace for HEALPix-related utilities within the Aladin Lite API. + */ + HEALPix: { + /** + * Represents a geographical point with longitude and latitude coordinates. + * + * @typedef {Object} LonLat + * @property {number} lon - The longitude coordinate. + * @property {number} lat - The latitude coordinate. + */ + + /** + * Represents the vertices of a HEALPix cell, where each vertex is a LonLat object. + * + * @typedef {Object} HpxCellVertices + * @property {LonLat} v1 - The first vertex. + * @property {LonLat} v2 - The second vertex. + * @property {LonLat} v3 - The third vertex. + * @property {LonLat} v4 - The fourth vertex. + */ + + /** + * Computes HEALPix vertices for a given NSIDE and pixel index (ipix). + * + * @function + * @memberof AladinUtils.HEALPix + * @name vertices + * + * @param {number} nside - NSIDE parameter for the HEALPix grid. + * @param {number | number[]} ipix - Pixel index or an array of pixel indices. + * @throws {string} Throws an error if A.init is not called first. + * @returns {HpxCellVertices[]} vertices - An array representing HEALPix cell vertices. Each element has v1, v2, v3, v4 properties. Each vi is an object having a lon and a lat property. + */ + vertices: function(nside, ipix) { + let wasm = Aladin.wasmLibs.core; + if (!wasm) { + throw 'A.init must be called first' + } + + // Cast to 1d array + if (!Array.isArray(ipix)) { + ipix = [ipix]; + } + + const vertices = wasm.HEALPixVertices(nside, ipix) + return vertices; + }, + + /** + * Computes HEALPix pixel indices from angular coordinates (longitude and latitude). + * + * @function + * @memberof AladinUtils.HEALPix + * @name ang2pix + * + * @param {number} nside - NSIDE parameter for the HEALPix grid. + * @param {number | number[]} lon - Longitude or an array of longitudes. + * @param {number | number[]} lat - Latitude or an array of latitudes. + * @throws {string} Throws an error if A.init is not called first. + * @returns {number[]} ipix - Pixel index or an array of pixel indices. + */ + ang2pix: function(nside, lon, lat) { + let wasm = Aladin.wasmLibs.core; + if (!wasm) { + throw 'A.init must be called first' + } + + if (!Array.isArray(lon)) { + lon = [lon]; + } + + if (!Array.isArray(lat)) { + lat = [lat]; + } + + const ipix = wasm.HEALPixAng2Pix(nside, lon, lat) + return ipix; + }, + + /** + * Computes angular coordinates (longitude and latitude) from HEALPix pixel indices. + * + * @function + * @memberof AladinUtils.HEALPix + * @name pix2ang + * + * @param {number} nside - NSIDE parameter for the HEALPix grid. + * @param {number | number[]} ipix - Pixel index or an array of pixel indices. + * + * @throws {string} Throws an error if A.init is not called first. + * @returns {LonLat[]} lonlat - Longitude and latitude or an array of longitudes and latitudes. + */ + pix2ang: function(nside, ipix) { + let wasm = Aladin.wasmLibs.core; + if (!wasm) { + throw 'A.init must be called first' + } + + // Cast to 1d array + if (!Array.isArray(ipix)) { + ipix = [ipix]; + } -export let AladinUtils = (function() { + const lonlat = wasm.HEALPixPix2Ang(nside, ipix) + return lonlat; + } + }, - return { /** * passage de xy projection à xy dans la vue écran * @param x @@ -43,6 +156,7 @@ export let AladinUtils = (function() { * @param largestDim largest dimension of the view * @returns position in the view */ + /* xyToView: function(x, y, width, height, largestDim, zoomFactor, round) { if (round==undefined) { // we round by default @@ -57,7 +171,7 @@ export let AladinUtils = (function() { else { return {vx: largestDim/2*(1+zoomFactor*x)-(largestDim-width)/2, vy: largestDim/2*(1+zoomFactor*y)-(largestDim-height)/2}; } - }, + },*/ /** * passage de xy dans la vue écran à xy projection @@ -69,9 +183,9 @@ export let AladinUtils = (function() { * @param zoomFactor * @returns position in xy projection */ - viewToXy: function(vx, vy, width, height, largestDim, zoomFactor) { + /*viewToXy: function(vx, vy, width, height, largestDim, zoomFactor) { return {x: ((2*vx+(largestDim-width))/largestDim-1)/zoomFactor, y: ((2*vy+(largestDim-height))/largestDim-1)/zoomFactor}; - }, + },*/ /** * convert a @@ -92,16 +206,44 @@ export let AladinUtils = (function() { return AladinUtils.xyToView(xy.X, xy.Y, width, height, largestDim, zoomFactor, false); },*/ - radecToViewXy: function(ra, dec, view) { - let xy = view.wasm.worldToScreen(ra, dec); + + /** + * Converts celestial coordinates (ra, dec) to screen coordinates (x, y) in pixels within the view. + * + * @function + * @memberof AladinUtils + * @name radecToViewXy + * + * @param {number} ra - Right Ascension (RA) coordinate in degrees. + * @param {number} dec - Declination (Dec) coordinate in degrees. + * @param {Aladin} aladin - Aladin Lite object containing the WebAssembly API. + * @returns {number[]} xy - A 2 elements array representing the screen coordinates [X, Y] in pixels. + */ + radecToViewXy: function(ra, dec, aladin) { + let xy = aladin.view.wasm.worldToScreen(ra, dec); return xy; }, - viewXyToClipXy: function(x, y, view) { - let xy = view.wasm.screenToClip(x, y); + /** + * Converts screen coordinates (X, Y) to clip coordinates within the view (coordinates lying between 0 and 1). + * + * @function + * @memberof AladinUtils + * @name viewXyToClipXy + * + * @param {number} x - X-coordinate in pixel screen coordinates + * @param {number} y - Y-coordinate in pixel screen coordinates. + * @param {Aladin} aladin - Aladin Lite object containing the WebAssembly API. + * @returns {number[]} xy - An array representing the coordinates [X, Y] in clipping space. + */ + viewXyToClipXy: function(x, y, aladin) { + let xy = aladin.view.wasm.screenToClip(x, y); return xy; }, + /** + * @deprecated since version 2.0 + */ myRound: function(a) { if (a<0) { return -1*( (-a) | 0); @@ -119,16 +261,16 @@ export let AladinUtils = (function() { * @param height * @returns a boolean whether (vx, vy) is in the screen */ - isInsideViewXy: function(vx, vy, width, height) { + /*isInsideViewXy: function(vx, vy, width, height) { return vx >= 0 && vx < width && vy >= 0 && vy < height - }, + },*/ /** * tests whether a healpix pixel is visible or not * @param pixCorners array of position (xy view) of the corners of the pixel * @param viewW */ - isHpxPixVisible: function(pixCorners, viewWidth, viewHeight) { + /*isHpxPixVisible: function(pixCorners, viewWidth, viewHeight) { for (var i = 0; i=-20 && pixCorners[i].vx<(viewWidth+20) && pixCorners[i].vy>=-20 && pixCorners[i].vy<(viewHeight+20) ) { @@ -136,13 +278,8 @@ export let AladinUtils = (function() { } } return false; - }, - - ipixToIpix: function(npixIn, norderIn, norderOut) { - var npixOut = []; - if (norderIn>=norderOut) { - } - }, + },*/ + // Zoom is handled in the backend /*getZoomFactorForAngle: function(angleInDegrees, projectionMethod) { var p1 = {ra: 0, dec: 0}; @@ -157,17 +294,17 @@ export let AladinUtils = (function() { return zoomFactor; },*/ - counterClockwiseTriangle: function(x1, y1, x2, y2, x3, y3) { + /*counterClockwiseTriangle: function(x1, y1, x2, y2, x3, y3) { // From: https://math.stackexchange.com/questions/1324179/how-to-tell-if-3-connected-points-are-connected-clockwise-or-counter-clockwise // | x1, y1, 1 | // | x2, y2, 1 | > 0 => the triangle is given in anticlockwise order // | x3, y3, 1 | return x1*y2 + y1*x3 + x2*y3 - x3*y2 - y3*x1 - x2*y1 >= 0; - }, + },*/ // grow array b of vx,vy view positions by *val* pixels - grow2: function(b, val) { + /*grow2: function(b, val) { var j=0; for ( var i=0; i<4; i++ ) { if ( b[i]==null ) { @@ -225,19 +362,7 @@ export let AladinUtils = (function() { b1[c].vy+=chouilla; } return b1; - }, - - // SVG icons templates are stored here rather than in a CSS, as to allow - // to dynamically change the fill color - // Pretty ugly, haven't found a prettier solution yet - // - // TODO: store this in the Stack class once it will exist - // - SVG_ICONS: { - CATALOG: '', - MOC: '', - OVERLAY: '' - }, + },*/ /** * @function degreesToString @@ -277,5 +402,3 @@ export let AladinUtils = (function() { } }; - -})(); \ No newline at end of file diff --git a/src/js/Catalog.js b/src/js/Catalog.js index ad057dcef..ba6dddc2f 100644 --- a/src/js/Catalog.js +++ b/src/js/Catalog.js @@ -34,21 +34,64 @@ import { Color } from "./Color.js" import { Utils } from "./Utils"; 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'; - -// TODO : harmoniser parsing avec classe ProgressiveCat +/** + * Represents a catalog with configurable options for display and interaction. + * + * @namespace + * @typedef {Object} Catalog + */ export let Catalog = (function() { - + /** + * Constructor function for creating a new catalog instance. + * + * @constructor + * @memberof Catalog + * @param {Object} options - Configuration options for the catalog. + * @param {string} options.url - The URL of the catalog. + * @param {string} [options.name="catalog"] - The name of the catalog. + * @param {string} [options.color] - The color associated with the catalog. + * @param {number} [options.sourceSize=8] - The size of the sources in the catalog. + * @param {number} [options.markerSize=12] - The size of the markers associated with sources. + * @param {string} [options.shape="square"] - The shape of the sources (can be, "square", "circle", "plus", "cross", "rhomb", "triangle"). + * @param {number} [options.limit] - The maximum number of sources to display. + * @param {function} [options.onClick] - The callback function to execute on a source click. + * @param {boolean} [options.readOnly=false] - Whether the catalog is read-only. + * @param {string} [options.raField] - The ID or name of the field holding Right Ascension (RA). + * @param {string} [options.decField] - The ID or name of the field holding Declination (dec). + * @param {function} [options.filter] - The filtering function for sources. + * @param {boolean} [options.displayLabel=false] - Whether to display labels for sources. + * @param {string} [options.labelColor] - The color of the source labels. + * @param {string} [options.labelFont="10px sans-serif"] - The font for the source labels. + * + * @example + * const catalogOptions = { + * url: "https://example.com/catalog", + * name: "My Catalog", + * color: "#ff0000", + * sourceSize: 10, + * markerSize: 15, + * shape: "circle", + * limit: 1000, + * onClick: (source) => { /* handle source click * }, + * readOnly: true, + * raField: "ra", + * decField: "dec", + * filter: (source) => source.mag < 15, + * displayLabel: true, + * labelColor: "#00ff00", + * labelFont: "12px Arial" + * }; + * const myCatalog = new Catalog(catalogOptions); + */ function Catalog(options) { - let self = this; options = options || {}; - this.uuid = Utils.uuidv4(); - this.type = 'catalog'; + this.url = options.url; + this.name = options.name || "catalog"; this.color = options.color || Color.getNextColor(); this.sourceSize = options.sourceSize || 8; @@ -63,16 +106,7 @@ export let Catalog = (function() { this.decField = options.decField || undefined; // ID or name of the field holding dec // 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.fields = undefined; - - this.indexationNorder = 5; // à quel niveau indexe-t-on les sources - this.sources = []; - this.ra = []; - this.dec = []; - this.footprints = []; + this.filterFn = options.filter || undefined; // TODO: do the same for catalog this.displayLabel = options.displayLabel || false; this.labelColor = options.labelColor || this.color; @@ -86,6 +120,19 @@ export let Catalog = (function() { this.selectionColor = '#00ff00'; + this.showFieldCallback = {}; // callbacks when the user clicks on a cell in the measurement table associated + this.fields = undefined; + this.uuid = Utils.uuidv4(); + this.type = 'catalog'; + + this.indexationNorder = 5; // à quel niveau indexe-t-on les sources + this.sources = []; + this.ra = []; + this.dec = []; + this.footprints = []; + + + // create this.cacheCanvas // cacheCanvas permet de ne créer le path de la source qu'une fois, et de le réutiliser (cf. http://simonsarris.com/blog/427-increasing-performance-by-caching-paths-on-canvas) this.updateShape(options); @@ -212,7 +259,7 @@ export let Catalog = (function() { var field = fields[l]; if ( ! raFieldIdx) { if (field.ucd) { - var ucd = $.trim(field.ucd.toLowerCase()); + var ucd = field.ucd.toLowerCase().trim(); if (ucd.indexOf('pos.eq.ra')==0 || ucd.indexOf('pos_eq_ra')==0) { raFieldIdx = l; continue; @@ -222,7 +269,7 @@ export let Catalog = (function() { if ( ! decFieldIdx) { if (field.ucd) { - var ucd = $.trim(field.ucd.toLowerCase()); + var ucd = field.ucd.toLowerCase().trim(); if (ucd.indexOf('pos.eq.dec')==0 || ucd.indexOf('pos_eq_dec')==0) { decFieldIdx = l; continue; @@ -272,7 +319,9 @@ export let Catalog = (function() { let parsedFields = {}; let fieldIdx = 0; fields.forEach((field) => { - let key = field.name ? field.name : field.id; + let key = field.name ? field.name : field.ID; + + key = key.split(' ').join('_') let nameField; if (fieldIdx == raFieldIdx) { @@ -283,6 +332,7 @@ export let Catalog = (function() { nameField = key; } + // remove the space character parsedFields[nameField] = { name: key, idx: fieldIdx, @@ -297,13 +347,33 @@ export let Catalog = (function() { // return an array of Source(s) from a VOTable url // callback function is called each time a TABLE element has been parsed Catalog.parseVOTable = function(url, successCallback, errorCallback, maxNbSources, useProxy, raField, decField) { - VOTable.parse( + let rowIdx = 0; + new VOTable( url, - (fields, rows) => { + (rsc) => { + let table = VOTable.parseRsc(rsc) + if (!table || !table.rows || !table.fields) { + errorCallback('Parsing error of the votable located at: ' + url); + return; + } + + let { fields, rows } = table; + 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 = []; var coo = new Coo(); + rows.every(row => { let ra, dec, region; var mesures = {}; @@ -331,6 +401,7 @@ export let Catalog = (function() { } source = new Source(ra, dec, mesures); + source.rowIdx = rowIdx; } let footprint = null; @@ -341,6 +412,9 @@ export let Catalog = (function() { if (footprint) { footprints.push(footprint); + if (maxNbSources && footprints.length == maxNbSources) { + return false; + } } else if(source) { sources.push(source); if (maxNbSources && sources.length == maxNbSources) { @@ -348,17 +422,23 @@ export let Catalog = (function() { } } + rowIdx++; return true; }) if (successCallback) { - successCallback(sources, footprints, fields); + successCallback({ + sources: sources, + footprints: footprints, + fields: fields, + type: type + }); } }, errorCallback, useProxy, raField, - decField + decField, ) }; @@ -417,12 +497,13 @@ export let Catalog = (function() { this.dec.push(sources[k].dec); } - ALEvent.AL_USE_WASM.dispatchedTo(document.body, { + this.reportChange(); + /*ALEvent.AL_USE_WASM.dispatchedTo(document.body, { callback: (wasm) => { wasm.setCatalog(this); this.reportChange(); } - }); + });*/ }; Catalog.prototype.addFootprints = function(footprintsToAdd) { @@ -441,12 +522,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 @@ -593,7 +670,7 @@ export let Catalog = (function() { this.footprints = []; }; - Catalog.prototype.draw = function(ctx, frame, width, height, largestDim, zoomFactor) { + Catalog.prototype.draw = function(ctx, frame, width, height, largestDim) { if (! this.isShowing) { return; } diff --git a/src/js/Circle.js b/src/js/Circle.js index 57445276c..b84497af5 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; }; @@ -153,7 +157,7 @@ export let Circle = (function() { } noStroke = noStroke===true || false; - var centerXyview = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1], view); + var centerXyview = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1], view.aladin); if (!centerXyview) { // the center goes out of the projection // we do not draw it @@ -175,7 +179,7 @@ export let Circle = (function() { ra = this.centerRaDec[0] + cardDirRa * this.radiusDegrees; dec = this.centerRaDec[1] + cardDirDec * this.radiusDegrees; - vertOnCircle = AladinUtils.radecToViewXy(ra, dec, view); + vertOnCircle = AladinUtils.radecToViewXy(ra, dec, view.aladin); if (vertOnCircle) { dx = vertOnCircle[0] - this.center.x; diff --git a/src/js/Color.js b/src/js/Color.js index 9c51e2b2c..718c18466 100644 --- a/src/js/Color.js +++ b/src/js/Color.js @@ -28,12 +28,47 @@ * *****************************************************************************/ export let Color = (function() { + let Color = function(label) { + let color; + if (label.r && label.g && label.b) { + color = label; + // hex string form + } else 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 + color = Color.hexToRgb(label); + } else { + // 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]); + color = {r: r, g: g, b: b} + } else { + if (!Color.standardizedColors[label]) { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = label; + const colorHexa = ctx.fillStyle; + + Color.standardizedColors[label] = colorHexa; + } + + color = Color.hexToRgb(Color.standardizedColors[label]) + } + } + this.r = color.r; + this.g = color.g; + this.b = color.b; + }; + + Color.prototype.toHex = function() { + return Color.rgbToHex(this.r, this.g, this.b) + } - 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.colors = ['#ff0000', '#0000ff', '#99cc00', '#ffff00', '#00ffff', '#9900cc', '#0099cc', '#cc9900', '#cc0099', '#00cc99', '#663333', '#ffcc9a', '#ff9acc', '#ccff33', '#660000', '#ffcc33', '#ff00ff', '#00ff00', '#ffffff']; Color.standardizedColors = {}; Color.getNextColor = function() { @@ -60,7 +95,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 +142,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 c4ead39d6..964ff5543 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) { @@ -183,7 +183,7 @@ // @api // Optional arguments, - ColorCfg.prototype.setColormap = function(colormap = "native", options = {}) { + ColorCfg.prototype.setColormap = function(colormap = "native", options) { /// colormap // Make it case insensitive let cmap = formatColormap(colormap); @@ -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/ColorMap.js b/src/js/ColorMap.js index 41aa87c86..f90a7e8c7 100644 --- a/src/js/ColorMap.js +++ b/src/js/ColorMap.js @@ -30,6 +30,9 @@ import { AladinUtils } from "./AladinUtils.js"; +/** + * @deprecated since version 3.0 + */ export let ColorMap = (function() { diff --git a/src/js/CooFrameEnum.js b/src/js/CooFrameEnum.js index 7f8accb77..f2f156315 100644 --- a/src/js/CooFrameEnum.js +++ b/src/js/CooFrameEnum.js @@ -28,8 +28,6 @@ * *****************************************************************************/ -import $ from 'jquery'; - export let CooFrameEnum = (function() { var systems = {J2000: 'J2000', GAL: 'Galactic'}; diff --git a/src/js/DefaultActionsForContextMenu.js b/src/js/DefaultActionsForContextMenu.js index 17c89c206..add61266d 100644 --- a/src/js/DefaultActionsForContextMenu.js +++ b/src/js/DefaultActionsForContextMenu.js @@ -30,12 +30,18 @@ import { GenericPointer } from "./GenericPointer.js"; import A from "./A.js"; +import { CatalogQueryBox } from "./gui/Box/CatalogQueryBox.js"; export let DefaultActionsForContextMenu = (function () { let DefaultActionsForContextMenu = {}; DefaultActionsForContextMenu.getDefaultActions = function (aladinInstance) { + const a = aladinInstance; + + const selectObjects = (selection) => { + a.view.selectObjects(selection); + }; return [ { label: "Copy position", action(o) { @@ -43,30 +49,53 @@ export let DefaultActionsForContextMenu = (function () { r.selectNode(o.target); window.getSelection().removeAllRanges(); window.getSelection().addRange(r); + let statusBarMsg; try { let successful = document.execCommand('copy'); - let msg = successful ? 'successful' : 'unsuccessful'; - //console.log('Copying text command was ' + msg); + statusBarMsg = successful ? 'Position copied!' : 'Position could not be copied!'; } catch (err) { - console.error('Oops, unable to copy to clipboard'); + statusBarMsg = 'Oops, unable to copy to clipboard'; + } + + if (a.statusBar) { + a.statusBar.appendMessage({ + message: statusBarMsg, + duration: 2000, + type: 'info' + }) } + window.getSelection().removeAllRanges(); } }, { - label: "Take snapshot", action(o) { aladinInstance.exportAsPNG(); } + label: "Take snapshot", action(o) { a.exportAsPNG(); } }, { label: "Add", subMenu: [ { label: 'New image layer', action(o) { - aladinInstance.addNewImageLayer(); + a.addNewImageLayer(); + if (a.menu) { + a.menu.open('stack') + } } }, { label: 'New catalogue layer', action(o) { - aladinInstance.stack._onAddCatalogue(); + let catBox = new CatalogQueryBox(a) + if (catBox.isHidden) { + catBox._show({ + header: { + title: 'Add a new catalogue', + draggable: true + }, + position: { + anchor :'center center' + }, + }); + } } }, ] @@ -86,19 +115,19 @@ export let DefaultActionsForContextMenu = (function () { const name = file.name; // Consider other cases - const image = aladinInstance.createImageFITS( + const image = a.createImageFITS( url, name, undefined, (ra, dec, fov, _) => { // Center the view around the new fits object - aladinInstance.gotoRaDec(ra, dec); - aladinInstance.setFoV(fov * 1.1); + a.gotoRaDec(ra, dec); + a.setFoV(fov * 1.1); }, undefined ); - aladinInstance.setOverlayImageLayer(image, name) + a.setOverlayImageLayer(image, name) }); }; input.click(); @@ -114,7 +143,7 @@ export let DefaultActionsForContextMenu = (function () { files.forEach(file => { const url = URL.createObjectURL(file); let moc = A.MOCFromURL(url, { name: file.name, fill: true, opacity: 0.4 }); - aladinInstance.addMOC(moc); + a.addMOC(moc); }); }; input.click(); @@ -130,7 +159,7 @@ export let DefaultActionsForContextMenu = (function () { files.forEach(file => { const url = URL.createObjectURL(file); A.catalogFromURL(url, { name: file.name, onClick: 'showTable'}, (catalog) => { - aladinInstance.addCatalog(catalog); + a.addCatalog(catalog); }, false); }); }; @@ -141,12 +170,11 @@ export let DefaultActionsForContextMenu = (function () { }, { label: "What is this?", action(e) { - GenericPointer(aladinInstance.view, e); + GenericPointer(a.view, e); } }, { label: "HiPS2FITS cutout", action(o) { - const a = aladinInstance; let hips2fitsUrl = 'https://alasky.cds.unistra.fr/hips-image-services/hips2fits#'; let radec = a.getRaDec(); let fov = Math.max.apply(null, a.getFov()); @@ -157,11 +185,21 @@ export let DefaultActionsForContextMenu = (function () { } }, { - label: "Select sources", action(o) { - const a = aladinInstance; - - a.select(); - } + label: "Select sources", + subMenu: [ + { + label: 'Circular', + action(o) { + a.select('circle', selectObjects) + } + }, + { + label: 'Rectangular', + action(o) { + a.select('rect', selectObjects) + } + } + ] }, ] } diff --git a/src/js/Ellipse.js b/src/js/Ellipse.js index 599cfb7ac..0dc5a43ad 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; }; @@ -171,15 +175,15 @@ export let Ellipse = (function() { return; } - var centerXyview = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1], view); + var centerXyview = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1], view.aladin); if (!centerXyview) { // the center goes out of the projection // we do not draw it return; } - let circlePtXyViewRa = AladinUtils.radecToViewXy(this.centerRaDec[0] + this.radiusXDegrees, this.centerRaDec[1], view); - let circlePtXyViewDec = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1] + this.radiusYDegrees, view); + let circlePtXyViewRa = AladinUtils.radecToViewXy(this.centerRaDec[0] + this.radiusXDegrees, this.centerRaDec[1], view.aladin); + let circlePtXyViewDec = AladinUtils.radecToViewXy(this.centerRaDec[0], this.centerRaDec[1] + this.radiusYDegrees, view.aladin); if (!circlePtXyViewRa || !circlePtXyViewDec) { // the circle border goes out of the projection diff --git a/src/js/FiniteStateMachine.js b/src/js/FiniteStateMachine.js new file mode 100644 index 000000000..e228dfc48 --- /dev/null +++ b/src/js/FiniteStateMachine.js @@ -0,0 +1,21 @@ +export class FSM { + // Constructor + constructor(options) { + this.state = options && options.state; + this.transitions = options && options.transitions || {}; + } + + // Do nothing if the to is inaccesible + dispatch(to, params) { + const action = this.transitions[this.state][to]; + if (action) { + this.state = to; + + if (params) { + action(params); + } else { + action() + } + } + } +} \ No newline at end of file diff --git a/src/js/FiniteStateMachine/CircleSelect.js b/src/js/FiniteStateMachine/CircleSelect.js new file mode 100644 index 000000000..fdd93005f --- /dev/null +++ b/src/js/FiniteStateMachine/CircleSelect.js @@ -0,0 +1,152 @@ +// 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. +// + +import { FSM } from "../FiniteStateMachine"; +import { View } from "../View"; +/****************************************************************************** + * Aladin Lite project + * + * Class Selector + * + * The circle selection finite state machine + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export class CircleSelect extends FSM { + // constructor + constructor(options, view) { + let start = (params) => { + const {callback} = params; + this.callback = callback; + view.setMode(View.SELECT) + } + + let mousedown = (params) => { + const {coo} = params; + // start a new selection + this.startCoo = coo; + } + + let mousemove = (params) => { + const {coo} = params; + this.coo = coo; + + view.requestRedraw(); + }; + + let draw = () => { + let ctx = view.catalogCtx; + + if (!view.catalogCanvasCleared) { + ctx.clearRect(0, 0, view.width, view.height); + view.catalogCanvasCleared = true; + } + // draw the selection + ctx.fillStyle = options.color + '7f'; + ctx.strokeStyle = options.color; + ctx.lineWidth = options.lineWidth; + + var r2 = (this.coo.x - this.startCoo.x) * (this.coo.x - this.startCoo.x) + (this.coo.y - this.startCoo.y) * (this.coo.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(); + } + + let mouseup = (params) => { + var x, y; + const {coo} = params; + this.coo = coo; + // finish the selection + var r2 = (this.coo.x - this.startCoo.x) * (this.coo.x - this.startCoo.x) + (this.coo.y - this.startCoo.y) * (this.coo.y - this.startCoo.y); + var r = Math.sqrt(r2); + + x = this.startCoo.x; + y = this.startCoo.y; + (typeof this.callback === 'function') && this.callback({ + x, y, r, + label: 'circle', + contains(s) { + let dx = (s.x - x) + let dy = (s.y - y); + + return dx*dx + dy*dy <= r2; + }, + bbox() { + return { + x: x - r, + y: y - r, + w: 2*r, + h: 2*r + } + } + }); + + // TODO: remove these modes in the future + view.aladin.showReticle(true) + view.setCursor('default'); + + // execute general callback + if (view.callbacksByEventName) { + var callback = view.callbacksByEventName['select']; + if (typeof callback === "function") { + // !todo + let selectedObjects = view.selectObjects(this); + callback(selectedObjects); + } + } + view.setMode(View.PAN) + view.requestRedraw(); + }; + + let mouseout = mouseup; + + super({ + state: 'mouseup', + transitions: { + start: { + mousedown + }, + mousedown: { + mousemove + }, + mousemove: { + draw, + mouseup, + mouseout + }, + draw: { + mousemove, + mouseup, + mouseout + }, + mouseout: { + start + }, + mouseup: { + start, + } + } + }) + }; +} \ No newline at end of file diff --git a/src/js/FiniteStateMachine/PolySelect.js b/src/js/FiniteStateMachine/PolySelect.js new file mode 100644 index 000000000..9fd828d11 --- /dev/null +++ b/src/js/FiniteStateMachine/PolySelect.js @@ -0,0 +1,270 @@ +// 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. +// + +import { FSM } from "../FiniteStateMachine"; +import { ActionButton } from "../gui/Widgets/ActionButton"; +import { View } from "../View"; +import finishIconUrl from '../../../assets/icons/finish.svg'; +import { Utils } from "../Utils"; + +/****************************************************************************** + * Aladin Lite project + * + * Class Selector + * + * A selector + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export class PolySelect extends FSM { + // constructor + constructor(options, view) { + // Off initial state + let off = () => { + view.aladin.showReticle(true) + view.setMode(View.PAN) + view.setCursor('default'); + + // in case of a mouseout we would like to erase the selection draw + // in the canvas + view.requestRedraw(); + } + let btn; + let mouseout = (params) => { + let {e, coo} = params; + + if (btn.el.contains(e.relatedTarget) || e.relatedTarget.contains(btn.el)) { + // mouseout on the btn + self.dispatch('mousemove', {coo}); + } else { + btn.remove(); + + off(); + } + }; + + let start = (params) => { + const {callback} = params; + view.setMode(View.SELECT) + + this.callback = callback; + // reset the coo + this.coos = []; + + } + + let click = (params) => { + const {coo} = params; + + const firstClick = this.coos.length === 0; + if (firstClick) { + // create a btn at the first click + btn = ActionButton.createIconBtn({ + position: { + left: coo.x, + top: coo.y, + }, + cssStyle: { + zIndex: 100, + }, + tooltip: {content: 'Finish the selection', position: {direction: 'bottom'}}, + icon: { + size: 'medium', + monochrome: true, + url: finishIconUrl + }, + action(e) { + e.stopPropagation(); + e.preventDefault() + + btn.remove(); + self.dispatch('finish'); + } + }); + + btn.attachTo(view.aladin.aladinDiv); + } + + this.coos.push(coo); + + view.requestRedraw(); + }; + + let mousemove = (params) => { + const {coo} = params; + this.moveCoo = coo; + + view.requestRedraw(); + }; + + let draw = () => { + let ctx = view.catalogCtx; + + if (!view.catalogCanvasCleared) { + ctx.clearRect(0, 0, view.width, view.height); + view.catalogCanvasCleared = true; + } + // draw the selection + ctx.save(); + ctx.fillStyle = options.color + '7f'; + ctx.strokeStyle = options.color; + ctx.lineWidth = options.lineWidth; + + ctx.beginPath(); + + const startCoo = this.coos[0]; + ctx.moveTo(startCoo.x, startCoo.y); + + for (var i = 1; i < this.coos.length; i++) { + ctx.lineTo(this.coos[i].x, this.coos[i].y); + } + + if (this.moveCoo) + ctx.lineTo(this.moveCoo.x, this.moveCoo.y); + + ctx.stroke(); + ctx.fill(); + ctx.restore(); + } + + let finish = () => { + // finish the selection + let xMin = this.coos[0].x + let yMin = this.coos[0].y + let xMax = this.coos[0].x + let yMax = this.coos[0].y + for (var i = 1; i < this.coos.length; i++) { + xMin = Math.min(xMin, this.coos[i].x) + yMin = Math.min(yMin, this.coos[i].y) + xMax = Math.max(xMax, this.coos[i].x) + yMax = Math.max(yMax, this.coos[i].y) + } + + let x = xMin; + let y = yMin; + let w = xMax - xMin; + let h = yMax - yMin; + (typeof this.callback === 'function') && this.callback({ + vertices: this.coos, + label: 'polygon', + bbox() { + return {x, y, w, h} + } + }); + + this.coos = []; + + // TODO execute general callback + view.mustClearCatalog = true; + view.requestRedraw(); + + this.dispatch('off'); + }; + + let fsm; + if (Utils.hasTouchScreen()) { + let mousedown = click; + let mouseup = click; + + // smartphone, tablet + fsm = { + state: 'off', + transitions: { + off: { + start, + }, + start: { + mousedown + }, + mousedown: { + //mouseout, + mousemove, + draw, + }, + mouseout: { + start, + mousemove, + }, + mousemove: { + draw, + mouseup, + finish + }, + mouseup: { + mousedown, + finish, + draw, + }, + draw: { + mouseup, + mouseout, + mousemove, + finish + }, + finish: { + off + } + } + } + } else { + // desktop, laptops... + fsm = { + state: 'off', + transitions: { + off: { + start, + }, + start: { + click + }, + click: { + //mouseout, + mousemove, + draw, + }, + mouseout: { + start, + mousemove, + }, + mousemove: { + draw, + click, + finish + }, + draw: { + click, + mouseout, + mousemove, + finish + }, + finish: { + off + } + } + } + } + + super(fsm) + let self = this; + + this.coos = []; + }; +} \ No newline at end of file diff --git a/src/js/FiniteStateMachine/RectSelect.js b/src/js/FiniteStateMachine/RectSelect.js new file mode 100644 index 000000000..f1de0aa88 --- /dev/null +++ b/src/js/FiniteStateMachine/RectSelect.js @@ -0,0 +1,154 @@ +// 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. +// + +import { FSM } from "../FiniteStateMachine"; +import { View } from "../View"; +/****************************************************************************** + * Aladin Lite project + * + * Class Selector + * + * A selector + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export class RectSelect extends FSM { + // constructor + constructor(options, view) { + let start = (params) => { + const {callback} = params; + + this.callback = callback; + view.setMode(View.SELECT) + } + + let mousedown = (params) => { + const {coo} = params; + // start a new selection + this.startCoo = coo; + } + + let mousemove = (params) => { + const {coo} = params; + this.coo = coo; + + view.requestRedraw(); + }; + + let draw = () => { + let ctx = view.catalogCtx; + + if (!view.catalogCanvasCleared) { + ctx.clearRect(0, 0, view.width, view.height); + view.catalogCanvasCleared = true; + } + // draw the selection + ctx.fillStyle = options.color + '7f'; + ctx.strokeStyle = options.color; + ctx.lineWidth = options.lineWidth; + + var w = this.coo.x - this.startCoo.x; + var h = this.coo.y - this.startCoo.y; + + ctx.fillRect(this.startCoo.x, this.startCoo.y, w, h); + ctx.strokeRect(this.startCoo.x, this.startCoo.y, w, h); + } + + let mouseup = (params) => { + var x, y; + const {coo} = params; + this.coo = coo; + // finish the selection + var w = this.coo.x - this.startCoo.x; + var h = this.coo.y - this.startCoo.y; + x = this.startCoo.x; + y = this.startCoo.y; + + if (w < 0) { + x = x + w; + w = -w; + } + if (h < 0) { + y = y + h; + h = -h; + } + + (typeof this.callback === 'function') && this.callback({ + x, y, w, h, + label: 'rect', + contains(s) { + return s.x >= x && s.x <= x + w && s.y >= y && s.y <= y + h; + }, + bbox() { + return {x, y, w, h} + } + }); + + // TODO: remove these modes in the future + view.aladin.showReticle(true) + view.setCursor('default'); + + // execute general callback + if (view.callbacksByEventName) { + var callback = view.callbacksByEventName['select']; + if (typeof callback === "function") { + // !todo + let selectedObjects = view.selectObjects(this); + console.log(selectedObjects) + + callback(selectedObjects); + } + } + view.setMode(View.PAN) + view.requestRedraw(); + }; + + let mouseout = mouseup; + + super({ + state: 'mouseup', + transitions: { + start: { + mousedown + }, + mousedown: { + mousemove + }, + mousemove: { + draw, + mouseup, + mouseout + }, + draw: { + mousemove, + mouseup, + mouseout + }, + mouseout: { + start + }, + mouseup: { + start, + } + } + }) + }; +} \ No newline at end of file diff --git a/src/js/Footprint.js b/src/js/Footprint.js index b9b61918b..38bff1067 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) { @@ -104,10 +110,8 @@ export let Footprint= (function() { Footprint.prototype.actionClicked = function() { if (this.source) { - this.source.actionClicked(); + this.source.actionClicked(this); } - - this.shapes.forEach((shape) => shape.select()) }; Footprint.prototype.actionOtherObjectClicked = function() { @@ -146,7 +150,7 @@ export let Footprint= (function() { y: s.y, }; } else { - var xy = AladinUtils.radecToViewXy(s.ra, s.dec, view); + var xy = AladinUtils.radecToViewXy(s.ra, s.dec, view.aladin); if (!xy) { return false; } diff --git a/src/js/GenericPointer.js b/src/js/GenericPointer.js index 12b97a264..49a493db6 100644 --- a/src/js/GenericPointer.js +++ b/src/js/GenericPointer.js @@ -10,14 +10,14 @@ import { PlanetaryFeaturesPointer } from "./PlanetaryFeaturesPointer.js"; 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); - let radec = view.wasm.screenToWorld(xymouse.x, xymouse.y); +export let GenericPointer = function (view, e) { + const xymouse = Utils.relMouseCoords(e); + + let radec = view.aladin.pix2world(xymouse.x, xymouse.y); if (radec) { // sky case if (view.aladin.getBaseImageLayer().isPlanetaryBody() === false) { const queryRadius = Math.min(1, 15 * view.fov / view.largestDim); - console.log('queryRadius "generic pointer": ', queryRadius); SimbadPointer.query(radec[0], radec[1], queryRadius, view.aladin); } // planetary body case @@ -27,7 +27,7 @@ export let GenericPointer = (function (view, e) { PlanetaryFeaturesPointer.query(radec[0], radec[1], Math.min(80, view.fov / 20.0), body, view.aladin); } } else { - console.log("The location you clicked on is out of the view."); - } + alert("The location you clicked on is out of the view."); + } } -) + diff --git a/src/js/HiPSDefinition.js b/src/js/HiPSDefinition.js index b73d3a5c4..ef0eb812f 100644 --- a/src/js/HiPSDefinition.js +++ b/src/js/HiPSDefinition.js @@ -29,7 +29,6 @@ *****************************************************************************/ import { Utils } from "./Utils"; -import $ from 'jquery'; export let HiPSDefinition = (function() { @@ -103,8 +102,8 @@ export let HiPSDefinition = (function() { propertiesStr = propertiesStr.replace(/[\r]/g, ''); // split on LF var lines = propertiesStr.split('\n'); - for (var k=0; k response.json()); + dataType: 'json' + }); // We get the property here // 1. Ensure there is exactly one survey matching @@ -78,8 +79,6 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) { urlOrId = Utils.getAbsoluteURL(urlOrId) urlOrId = new URL(urlOrId); - - } catch(e) { throw e; } @@ -106,7 +105,7 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) { init = { mode: 'cors' }; } - let result = await fetch(url, init) + let result = fetch(url, init) .then((response) => { if (response.status == 404) { return Promise.reject("Url points to nothing") @@ -114,20 +113,20 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) { return response.text(); } }) - .then((response) => { - // We get the property here - let metadata = HiPSDefinition.parseHiPSProperties(response); - - // 1. Ensure there is exactly one survey matching - if (metadata) { - // Set the service url if not found - metadata.hips_service_url = HiPSServiceUrl; - } else { - throw 'No surveys matching at this url: ' + rootURL; - } - - return metadata; - }) + .then( + (response) => new Promise((resolve, reject) => { + // We get the property here + let metadata = HiPSDefinition.parseHiPSProperties(response); + // 1. Ensure there is exactly one survey matching + if (metadata && Object.keys(metadata).length > 0) { + // Set the service url if not found + metadata.hips_service_url = HiPSServiceUrl; + resolve(metadata); + } else { + reject('No surveys matching at this url: ' + rootURL); + } + }) + ) return result; } @@ -139,7 +138,7 @@ HiPSProperties.getFasterMirrorUrl = function (metadata) { const controller = new AbortController() let startRequestTime = Date.now(); - const maxTime = 2000; + const maxTime = 500; // 5 second timeout: const timeoutId = setTimeout(() => controller.abort(), maxTime) const promise = fetch(hipsServiceUrl + '/properties', { cache: 'no-store', signal: controller.signal, mode: "cors" }).then(response => { @@ -157,6 +156,8 @@ HiPSProperties.getFasterMirrorUrl = function (metadata) { // Get all the possible hips_service_url urls let promises = []; + let urls = [metadata.hips_service_url]; + promises.push(pingHiPSServiceUrl(metadata.hips_service_url)); let numHiPSServiceURL = 1; @@ -166,6 +167,8 @@ HiPSProperties.getFasterMirrorUrl = function (metadata) { let curUrl = metadata[key]; promises.push(pingHiPSServiceUrl(curUrl)) numHiPSServiceURL += 1; + + urls.push(curUrl) } return Promise.all(promises) @@ -184,6 +187,8 @@ HiPSProperties.getFasterMirrorUrl = function (metadata) { return r1.duration - r2.duration; }); + //console.log(validResponses) + if (validResponses.length >= 2) { const isSecondUrlOk = ((validResponses[1].duration - validResponses[0].duration) / validResponses[0].duration) < 0.20; @@ -192,9 +197,12 @@ HiPSProperties.getFasterMirrorUrl = function (metadata) { } else { return validResponses[0].baseUrl; } - } else { + } else if (validResponses.length === 1) { return validResponses[0].baseUrl; + } else { + // no valid response => we return an error + return Promise.reject('Survey not found. All mirrors urls have been tested:' + urls) } }) - .then((url) => Utils.fixURLForHTTPS(url)); + .then((url) => Utils.fixURLForHTTPS(url)) } diff --git a/src/js/ImageFITS.js b/src/js/ImageFITS.js index 61b0e71c3..ba0ca390e 100644 --- a/src/js/ImageFITS.js +++ b/src/js/ImageFITS.js @@ -70,10 +70,6 @@ export let ImageFITS = (function () { this.query = Promise.resolve(self); } - ImageFITS.prototype.isReady = function() { - return this.added; - } - // @api ImageFITS.prototype.setOpacity = function (opacity) { let self = this; @@ -110,6 +106,10 @@ export let ImageFITS = (function () { }); }; + ImageFITS.prototype.getAvailableFormats = function() { + return this.properties.formats; + } + // @api ImageFITS.prototype.setSaturation = function (saturation) { updateMetadata(this, () => { @@ -215,7 +215,7 @@ export let ImageFITS = (function () { return self; }).catch((e) => { - window.alert(e + ". See the console for more logging details. It may be possible CORS headers have not been set in the server where you want to download the file. If it is the case, try to manually download the FITS file first and then open it into aladin lite (e.g. by a drag and drop)") + window.alert(e + ". See the javascript console for more logging details.") if (self.errorCallback) { self.errorCallback() @@ -269,6 +269,11 @@ export let ImageFITS = (function () { return this.colorCfg; }; + // @api + ImageFITS.prototype.getCuts = function () { + return this.colorCfg.getCuts(); + }; + // @api ImageFITS.prototype.getOpacity = function () { return this.colorCfg.getOpacity(); diff --git a/src/js/ImageSurvey.js b/src/js/ImageSurvey.js index 8f0cd59e7..27485036c 100644 --- a/src/js/ImageSurvey.js +++ b/src/js/ImageSurvey.js @@ -29,7 +29,6 @@ *****************************************************************************/ import { Utils } from "./Utils"; import { ALEvent } from "./events/ALEvent.js"; -import { CooFrameEnum } from "./CooFrameEnum.js" import { ColorCfg } from "./ColorCfg.js"; import { ImageLayer } from "./ImageLayer.js"; import { HiPSProperties } from "./HiPSProperties.js"; @@ -56,11 +55,8 @@ PropertyParser.frame = function(options, properties = {}) { frame = "ICRS"; } else if (frame == "galactic") { frame = "GAL"; - } else if (frame === undefined) { - frame = "ICRS"; - console.warn('No cooframe given. Coordinate systems supported: "ICRS", "ICRSd", "j2000" or "galactic". ICRS is chosen by default'); } else { - frame = "ICRSd"; + frame = "ICRS"; console.warn('Invalid cooframe given: ' + cooFrame + '. Coordinate systems supported: "ICRS", "ICRSd", "j2000" or "galactic". ICRS is chosen by default'); } @@ -128,14 +124,42 @@ PropertyParser.isPlanetaryBody = function(options, properties = {}) { return properties.hips_body !== undefined; } +/** + * @typedef {Object} ImageSurveyOptions + * + * @property {number} [opacity=1.0] - Opacity of the survey or image (value between 0 and 1). + * @property {string} [colormap="native"] - The colormap configuration for the survey or image. + * @property {string} [stretch="linear"] - The stretch configuration for the survey or image. + * @property {boolean} [reversed=false] - If true, the colormap is reversed; otherwise, it is not reversed. + * @property {number} [minCut] - The minimum cut value for the color configuration. If not given, 0.0 for JPEG/PNG surveys, the value of the property file for FITS surveys + * @property {number} [maxCut] - The maximum cut value for the color configuration. If not given, 1.0 for JPEG/PNG surveys, the value of the property file for FITS surveys + * @property {boolean} [additive=false] - If true, additive blending is applied; otherwise, it is not applied. + * @property {number} [gamma=1.0] - The gamma correction value for the color configuration. + * @property {number} [saturation=0.0] - The saturation value for the color configuration. + * @property {number} [brightness=0.0] - The brightness value for the color configuration. + * @property {number} [contrast=0.0] - The contrast value for the color configuration. + * @property {number} [maxOrder] - If not given, retrieved from the properties of the survey. + * @property {number} [minOrder] - If not given, retrieved from the properties of the survey. + * @property {boolean} [longitudeReversed=false] - Set it to True for planetary survey visualization + * @property {string} [imgFormat] - If not given, look into the properties to see the accepted format. The format is chosen from PNG > WEBP > JPEG > FITS (in this importance order). + * @property {string} [cooFrame] - If not given, look into the properties. If it is a planet, then ICRS is chosen, otherwise its hips_frame key is read. If no value is found in the properties, ICRS is chosen by default. + */ export let ImageSurvey = (function () { - /** Constructor - * cooFrame and maxOrder can be set to null - * They will be determined by reading the properties file + /** + * The object describing an image survey * + * @class + * @constructs ImageSurvey + * + * @param {string} [id] - Optional, a uniq id for the survey. See {@link https://aladin.cds.unistra.fr/hips/list|here} for the list of IDs. + * Keep in mind that it is better to directly provide an url as it will not request our mocserver first to get final survey tiles retrieval url. + * @param {string} [name] - The name of the survey to be displayed in the UI + * @param {string} url - The url where the survey is located. Check the hips list {@link https://aladin.cds.unistra.fr/hips/list|here} for the valid survey urls to display. + * @param {ImageSurveyOptions} [options] - The option for the survey + * + * @description Prefer provide an url better than an id. If both are given, the url will be requested first for the survey data. */ function ImageSurvey(id, name, url, view, options) { - // A reference to the view this.view = view; this.wasm = view.wasm; this.added = false; @@ -148,7 +172,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 +190,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 @@ -177,8 +203,15 @@ export let ImageSurvey = (function () { // Request all the properties to see which mirror is the fastest HiPSProperties.getFasterMirrorUrl(properties) .then((url) => { - self.setUrl(url); - }); + self._setUrl(url); + }) + .catch(e => { + //alert(e); + console.error(e); + // the survey has been added so we remove it from the stack + self.view.removeImageLayer(self.layer) + //throw e; + }) // Max order maxOrder = PropertyParser.maxOrder(options, properties); @@ -222,10 +255,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 +265,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 +297,8 @@ export let ImageSurvey = (function () { } self.properties = { + creatorDid: creatorDid, + obsTitle: obsTitle, url: url, maxOrder: maxOrder, frame: frame, @@ -325,10 +358,10 @@ export let ImageSurvey = (function () { } } else { // user wants nothing then we choose one from the properties - if (formats.indexOf('png') >= 0) { - imgFormat = "png"; - } else if (formats.indexOf('webp') >= 0) { + if (formats.indexOf('webp') >= 0) { imgFormat = "webp"; + } else if (formats.indexOf('png') >= 0) { + imgFormat = "png"; } else if (formats.indexOf('jpeg') >= 0) { imgFormat = "jpeg"; } else if (formats.indexOf('fits') >= 0) { @@ -343,9 +376,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); } @@ -360,11 +398,7 @@ export let ImageSurvey = (function () { })(); }; - ImageSurvey.prototype.isReady = function() { - return this.added; - } - - ImageSurvey.prototype.setUrl = function (url) { + ImageSurvey.prototype._setUrl = function (url) { if (this.properties.url !== url) { console.info("Change url of ", this.id, " from ", this.properties.url, " to ", url) @@ -376,13 +410,30 @@ export let ImageSurvey = (function () { this.properties.url = url; } } - + /** + * Checks if the ImageSurvey represents a planetary body. + * + * This method returns a boolean indicating whether the ImageSurvey corresponds to a planetary body, e.g. the earth or a celestial body. + * + * @memberof ImageSurvey + * + * @returns {boolean} Returns true if the ImageSurvey represents a planetary body; otherwise, returns false. + */ ImageSurvey.prototype.isPlanetaryBody = function() { return this.properties.isPlanetaryBody; } - // @api - // TODO: include imgFormat inside the ImageSurvey's meta attribute + /** + * Sets the image format for the ImageSurvey. + * + * This method updates the image format of the ImageSurvey, performs format validation, and triggers the update of metadata. + * + * @memberof ImageSurvey + * + * @param {string} format - The desired image format. Should be one of ["fits", "png", "jpg", "webp"]. + * + * @throws {string} Throws an error if the provided format is not one of the supported formats or if the format is not available for the specific ImageSurvey. + */ ImageSurvey.prototype.setImageFormat = function (format) { let self = this; self.query @@ -424,13 +475,40 @@ 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; }); }) }; - // @api + /** + * Sets the opacity factor when rendering the ImageSurvey + * + * @memberof ImageSurvey + * + * @returns {string[]} Returns the formats accepted for the survey, i.e. the formats of tiles that are availables. Could be PNG, WEBP, JPG and FITS. + */ + ImageSurvey.prototype.getAvailableFormats = function() { + return this.properties.formats; + } + + /** + * Sets the opacity factor when rendering the ImageSurvey + * + * @memberof ImageSurvey + * + * @param {number} opacity - Opacity of the survey to set. Between 0 and 1 + */ ImageSurvey.prototype.setOpacity = function (opacity) { let self = this; updateMetadata(self, () => { @@ -438,47 +516,139 @@ export let ImageSurvey = (function () { }); }; - // @api + /** + * Sets the blending mode when rendering the ImageSurvey + * + * @memberof ImageSurvey + * + * @param {boolean} [additive=false] - + * + * @description Two rendering modes are availables i.e. the default one and the additive one. + * When rendering this survey on top of the already rendered ones, the final color of the screen is computed like: + *
+ *
opacity * this_survey_color + (1 - opacity) * already_rendered_color for the default mode + *
opacity * this_survey_color + already_rendered_color for the additive mode + *
+ *
+ * Additive mode allows you to do linear survey color combination i.e. let's define 3 surveys named s1, s2, s3. Each could be associated to one color channel, i.e. s1 with red, s2 with green and s3 with the blue color channel. + * If the additive blending mode is enabled, then the final pixel color of your screen will be: rgb = [s1_opacity * s1_color; s2_opacity * s2_color; s3_opacity * s3_color] + */ ImageSurvey.prototype.setBlendingConfig = function (additive = false) { updateMetadata(this, () => { this.colorCfg.setBlendingConfig(additive); }); }; - // @api + /** + * Sets the colormap when rendering the ImageSurvey. + * + * @memberof ImageSurvey + * + * @param {string} [colormap="grayscale"] - The colormap label to use. See {@link https://matplotlib.org/stable/users/explain/colors/colormaps.html|here} for more info about colormaps. + * Possible values are: + *
"blues" + *
"cividis" + *
"cubehelix" + *
"eosb" + *
"grayscale" + *
"inferno" + *
"magma" + *
"native" + *
"parula" + *
"plasma" + *
"rainbow" + *
"rdbu" + *
"rdylbu" + *
"redtemperature" + *
"sinebow" + *
"spectral" + *
"summer" + *
"viridis" + *
"ylgnbu" + *
"ylorbr" + *
"red" + *
"green" + *
"blue" + * @param {Object} [options] - Options for the colormap + * @param {string} [options.stretch] - Stretching function of the colormap. Possible values are 'linear', 'asinh', 'log', 'sqrt', 'pow'. If no given, will not change it. + * @param {boolean} [options.reversed=false] - Reverse the colormap axis. + */ ImageSurvey.prototype.setColormap = function (colormap, options) { updateMetadata(this, () => { this.colorCfg.setColormap(colormap, options); }); } - // @api + /** + * Sets the gamma correction factor for the ImageSurvey. + * + * This method updates the gamma of the ImageSurvey. + * + * @memberof ImageSurvey + * + * @param {number} lowCut - The low cut value to set for the ImageSurvey. + * @param {number} highCut - The high cut value to set for the ImageSurvey. + */ ImageSurvey.prototype.setCuts = function (lowCut, highCut) { updateMetadata(this, () => { this.colorCfg.setCuts(lowCut, highCut); }); }; - // @api + /** + * Sets the gamma correction factor for the ImageSurvey. + * + * This method updates the gamma of the ImageSurvey. + * + * @memberof ImageSurvey + * + * @param {number} gamma - The saturation value to set for the ImageSurvey. Between 0.1 and 10 + */ ImageSurvey.prototype.setGamma = function (gamma) { updateMetadata(this, () => { this.colorCfg.setGamma(gamma); }); }; - // @api + /** + * Sets the saturation for the ImageSurvey. + * + * This method updates the saturation of the ImageSurvey. + * + * @memberof ImageSurvey + * + * @param {number} saturation - The saturation value to set for the ImageSurvey. Between 0 and 1 + */ ImageSurvey.prototype.setSaturation = function (saturation) { updateMetadata(this, () => { this.colorCfg.setSaturation(saturation); }); }; + /** + * Sets the brightness for the ImageSurvey. + * + * This method updates the brightness of the ImageSurvey. + * + * @memberof ImageSurvey + * + * @param {number} brightness - The brightness value to set for the ImageSurvey. Between 0 and 1 + */ ImageSurvey.prototype.setBrightness = function (brightness) { updateMetadata(this, () => { this.colorCfg.setBrightness(brightness); }); }; + /** + * Sets the contrast for the ImageSurvey. + * + * This method updates the contrast of the ImageSurvey and triggers the update of metadata. + * + * @memberof ImageSurvey + * + * @param {number} contrast - The contrast value to set for the ImageSurvey. Between 0 and 1 + */ ImageSurvey.prototype.setContrast = function (contrast) { updateMetadata(this, () => { this.colorCfg.setContrast(contrast); @@ -494,7 +664,7 @@ export let ImageSurvey = (function () { } // Private method for updating the backend with the new meta - var updateMetadata = function (self, callback = undefined) { + var updateMetadata = function (self, callback) { if (callback) { callback(); } @@ -517,7 +687,7 @@ export let ImageSurvey = (function () { this.layer = layer; this.wasm.addImageSurvey({ - layer: this.layer, + layer, properties: this.properties, meta: this.metadata(), }); diff --git a/src/js/Logger.js b/src/js/Logger.js index 83499b02c..a0f939d38 100644 --- a/src/js/Logger.js +++ b/src/js/Logger.js @@ -17,26 +17,23 @@ // along with Aladin Lite. // -import $ from 'jquery'; +import { Utils } from "./Utils"; // log export let Logger = {}; Logger.log = function(action, params) { try { - var logUrl = "//alasky.unistra.fr/cgi/AladinLiteLogger/log.py"; + var logUrl = "https://alasky.unistra.fr/cgi/AladinLiteLogger/log.py"; var paramStr = ""; if (params) { paramStr = JSON.stringify(params); } - - $.ajax({ + Utils.fetch({ url: logUrl, data: {"action": action, "params": paramStr, "pageUrl": window.location.href, "referer": document.referrer ? document.referrer : ""}, method: 'GET', - dataType: 'json' // as alasky supports CORS, we do not need JSONP any longer }); - } catch(e) { window.console && console.log('Exception: ' + e); diff --git a/src/js/MOC.js b/src/js/MOC.js index 4adb05b52..24fc3c01a 100644 --- a/src/js/MOC.js +++ b/src/js/MOC.js @@ -82,19 +82,17 @@ export let MOC = (function() { * set MOC data by parsing a MOC serialized in JSON * (as defined in IVOA MOC document, section 3.1.1) */ - MOC.prototype.dataFromJSON = function(jsonMOC) { - this.dataJSON = jsonMOC; - }; - - /** - * set MOC data by parsing a URL pointing to a FITS MOC file - */ - MOC.prototype.dataFromFITSURL = function(mocURL, successCallback) { - this.dataURL = mocURL; - this.promiseFetchData = fetch(this.dataURL) - .then((resp) => resp.arrayBuffer()); + MOC.prototype.parse = function(data, successCallback) { + if (typeof data === 'string' || data instanceof String) { + let url = data; + this.promiseFetchData = fetch(url) + .then((resp) => resp.arrayBuffer()); + } else { + this.promiseFetchData = Promise.resolve(data) + } this.successCallback = successCallback; + this.errorCallback = this.errorCallback; }; MOC.prototype.setView = function(view) { @@ -103,45 +101,44 @@ export let MOC = (function() { this.view = view; this.mocParams = new Aladin.wasmLibs.core.MOC(this.uuid, this.opacity, this.lineWidth, this.perimeter, this.fill, this.edge, this.isShowing, this.color, this.fillColor); - if (this.dataURL) { - this.promiseFetchData - .then((arrayBuffer) => { - // Add the fetched moc to the rust backend - self.view.wasm.addFITSMoc(self.mocParams, new Uint8Array(arrayBuffer)); - self.ready = true; - - if (self.successCallback) { - self.successCallback(self) - } - - // Cache the sky fraction - self.skyFrac = self.view.wasm.mocSkyFraction(this.mocParams); - - // Add it to the view - self.view.mocs.push(self); - self.view.allOverlayLayers.push(self); - - // Tell the MOC has been fully loaded and can be sent as an event - ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(self.view.aladinDiv, {layer: self}); - - self.view.requestRedraw(); - }) - } else if (this.dataFromJSON) { - self.view.wasm.addJSONMoc(self.mocParams, self.dataJSON); - self.ready = true; - - // Cache the sky fraction - self.skyFrac = self.view.wasm.mocSkyFraction(self.mocParams); - - // Add it to the view - self.view.mocs.push(self); - self.view.allOverlayLayers.push(self); - - // Tell the MOC has been fully loaded and can be sent as an event - ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(self.view.aladinDiv, {layer: self}); - - self.view.requestRedraw(); - } + this.promiseFetchData + .then((data) => { + if (data instanceof ArrayBuffer) { + // from an url + const buf = data; + self.view.wasm.addFITSMoc(self.mocParams, new Uint8Array(buf)); + } else if(data.ra && data.dec && data.radius) { + // circle + const c = data; + self.view.wasm.addConeMOC(self.mocParams, c.ra, c.dec, c.radius); + } else if(data.ra && data.dec) { + // polygon + const p = data; + self.view.wasm.addPolyMOC(self.mocParams, p.ra, p.dec); + } else { + // json moc + self.view.wasm.addJSONMoc(self.mocParams, data); + } + // Add the fetched moc to the rust backend + self.ready = true; + + if (self.successCallback) { + self.successCallback(self) + } + + // Cache the sky fraction + self.skyFrac = self.view.wasm.getMOCSkyFraction(this.mocParams); + + // Add it to the view + self.view.mocs.push(self); + self.view.allOverlayLayers.push(self); + + // Tell the MOC has been fully loaded and can be sent as an event + ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(self.view.aladinDiv, {layer: self}); + + self.view.requestRedraw(); + }) + .catch(e => alert('MOC load error:' + e)) }; MOC.prototype.reportChange = function() { diff --git a/src/js/MeasurementTable.js b/src/js/MeasurementTable.js index 6b37d0484..7721689a2 100644 --- a/src/js/MeasurementTable.js +++ b/src/js/MeasurementTable.js @@ -31,268 +31,68 @@ *****************************************************************************/ import { Color } from "./Color.js" +import { Icon } from "./gui/Widgets/Icon.js"; +import { Tabs } from "./gui/Widgets/Tab.js"; +import { Table } from "./gui/Widgets/Table.js"; +import { ActionButton } from "./gui/Widgets/ActionButton.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; - - this.savedTablesIdx = 0; - this.savedTables = []; - - aladinLiteDiv.appendChild(this.element); - } - - MeasurementTable.prototype.updateRows = function() { - let tbody = this.element.querySelector('tbody'); - - tbody.innerHTML = ""; - - let table = this.tables[this.curTableIdx]; - - let result = ''; - table["rows"].forEach((row) => { - result += '' - for (let key in row.data) { - // check the type here - const val = row.data[key] || '--'; - result += '' - if (typeof(val) === "string") { - try { - let url = new URL(val); - let link = '' + url + ''; - result += link; - } catch(e) { - result += val - } - } else { - result += val - } - result += '' - } - 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]; - callback(table["rows"][index].data) - - e.preventDefault(); - }, false) - }) - } - } - } - - MeasurementTable.prototype.showPreviousMeasurement = function() { - this.savedTablesIdx--; - if (this.savedTablesIdx < 0) { - this.savedTablesIdx = 0; - } - - let tables = this.savedTables[this.savedTablesIdx]; - - if (tables) { - this.update(tables); - this.updateStateNavigation(); - } - } - - MeasurementTable.prototype.showNextMeasurement = function() { - this.savedTablesIdx++; - if (this.savedTablesIdx >= this.savedTables.length) { - this.savedTablesIdx = this.savedTables.length - 1; - } - - let tables = this.savedTables[this.savedTablesIdx]; - - if (tables) { - this.update(tables); - this.updateStateNavigation(); - } + function MeasurementTable(aladin) { + this.aladin = aladin; } // show measurement associated with a given source - MeasurementTable.prototype.showMeasurement = function(tables, options) { + MeasurementTable.prototype.showMeasurement = function(tables) { if (tables.length === 0) { return; } - this.update(tables); - - if (options && options["save"]) { - this.saveState(); - - this.updateStateNavigation(); - } - }; - - MeasurementTable.prototype.updateStateNavigation = function() { - // update the previous/next buttons - let tabsElement = this.element.querySelector(".tabs"); - if (this.savedTables.length >= 2) { - /// Create previous tab - let prevTableElement = document.createElement('button'); - prevTableElement.setAttribute('title', 'Go back to the previous table') - if (this.savedTablesIdx == 0) { - prevTableElement.disabled = true; - } - - prevTableElement.addEventListener( - 'click', () => this.showPreviousMeasurement(), false - ); - - prevTableElement.innerText = '<'; - tabsElement.appendChild(prevTableElement); - - /// Create next tab - let nextTableElement = document.createElement('button'); - nextTableElement.setAttribute('title', 'Go to the next table') - - if (this.savedTables.length == 0 || this.savedTablesIdx == this.savedTables.length - 1) { - nextTableElement.disabled = true; - } - - nextTableElement.addEventListener( - 'click', () => this.showNextMeasurement(), false - ); - - nextTableElement.innerText = '>'; - tabsElement.appendChild(nextTableElement); - } - }; - - MeasurementTable.prototype.saveState = function() { - if (this.savedTables.length === 0) { - this.savedTables.push(this.tables); - } else { - if (this.tables !== this.savedTables[this.savedTablesIdx]) { - // Remove all the tables past to the current one - this.savedTables = this.savedTables.slice(0, this.savedTablesIdx + 1); - // Save the current tables - this.savedTables.push(this.tables); - this.savedTablesIdx = this.savedTables.length - 1; + let layout = tables.map((table) => { + let content = new Table(table); + + let textContent = '
' + + table.name + '
'; + + let label = new ActionButton({ + icon: { + size: 'small', + url: Icon.dataURLFromSVG({svg: Icon.SVG_ICONS.CATALOG, color: table.color}), + }, + content: textContent, + }) + + return { + title: table.name, + label, + content, + /*cssStyle: { + backgroundColor: rgbColor, + color: labelColor, + padding: '2px', + }*/ } - } - } - - 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.updateRows(); - - 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 - - tabButtonElement.addEventListener( - 'click', - () => { - self.curTableIdx = index; - - 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.updateRows() - } - ,false - ); - - tabButtonElement.style.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); }); - return tabsElement; - } + this.hide(); - MeasurementTable.createTableHeader = function(table) { - let theadElement = document.createElement('thead'); - var content = ''; - - for (let [_, field] of Object.entries(table["fields"])) { - content += '' + field.name + ''; - } - content += ''; - - theadElement.innerHTML = content; - - return theadElement; - } - - MeasurementTable.prototype.show = function() { - this.element.style.visibility = "visible"; + this.table = new Tabs({ + aladin: this.aladin, + layout, + cssStyle: { + position: 'absolute', + bottom: '2.4rem', + maxWidth: '100%', + } + }, this.aladin.aladinDiv); }; MeasurementTable.prototype.hide = function() { - this.savedTables = []; - this.savedTablesIdx = 0; - this.curTableIdx = 0; - - this.element.style.visibility = "hidden"; + if (this.table) { + this.table.remove(); + } }; return MeasurementTable; diff --git a/src/js/MocServer.js b/src/js/MocServer.js index 0f8603e7d..0af83b1b9 100644 --- a/src/js/MocServer.js +++ b/src/js/MocServer.js @@ -45,36 +45,36 @@ export class MocServer { static _allCatalogHiPSes = undefined; static getAllHiPSes() { - if (this._allHiPSes === undefined) { - (async () => { - const params = { - expr: "dataproduct_type=image||dataproduct_type=cube", - get: "record", - fmt: "json", - 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, { - data: params, - }).then(response => response.json()); - })(); + if (!this._allHiPSes) { + const params = { + //expr: "dataproduct_type=image||dataproduct_type=cube", + expr: "dataproduct_type=image", + get: "record", + fmt: "json", + fields: "ID,hips_creator,hips_copyright,hips_frame,hips_tile_format,obs_title,obs_description,obs_copyright,obs_regime", + //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 = Utils.loadFromUrls(MocServer.MIRRORS_HTTPS, { + data: params, + dataType: 'json' + }) } return this._allHiPSes; } static getAllCatalogHiPSes() { - if (this._allCatalogHiPSes === undefined) { - (async () => { - const params = { - expr: "dataproduct_type=catalog", - get: "record", - 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}) - .then(response => response.json()); - })(); + if (!this._allCatalogHiPSes) { + const params = { + expr: "dataproduct_type=catalog", + get: "record", + fmt: "json", + fields: "ID,hips_copyright,obs_title,obs_description,obs_copyright,cs_service_url,hips_service_url", + //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 = Utils.loadFromUrls(MocServer.MIRRORS_HTTPS, {data: params, dataType: 'json'}) } return this._allCatalogHiPSes; diff --git a/src/js/Overlay.js b/src/js/Overlay.js index 7f12798a6..701e4173d 100644 --- a/src/js/Overlay.js +++ b/src/js/Overlay.js @@ -79,6 +79,14 @@ export let Overlay = (function() { this.reportChange(); }; + Overlay.prototype.toggle = function() { + if (! this.isShowing) { + this.show() + } else { + this.hide() + } + }; + // return an array of Footprint from a STC-S string Overlay.parseSTCS = function(stcs, options) { options = options || {}; diff --git a/src/js/PlanetaryFeaturesNameResolver.js b/src/js/PlanetaryFeaturesNameResolver.js index e99d67ed1..f29146c9f 100644 --- a/src/js/PlanetaryFeaturesNameResolver.js +++ b/src/js/PlanetaryFeaturesNameResolver.js @@ -30,8 +30,6 @@ import { Utils } from "./Utils"; -import $ from 'jquery'; - export let PlanetaryFeaturesNameResolver = (function() { let PlanetaryFeaturesNameResolver = {}; @@ -59,8 +57,9 @@ export let PlanetaryFeaturesNameResolver = (function() { PlanetaryFeaturesNameResolver.resolve = function(featureName, body, callbackFunctionSuccess, callbackFunctionError) { const url = PlanetaryFeaturesNameResolver.URL; - $.ajax({ + Utils.fetch({ url: url , + desc: "Resolving planet name: " + featureName, data: {"identifier": featureName, 'body': body, 'threshold': 0.7, 'format': 'csv'}, method: 'GET', dataType: 'text', @@ -90,7 +89,7 @@ export let PlanetaryFeaturesNameResolver = (function() { } }, error: callbackFunctionError - }); + }); }; return PlanetaryFeaturesNameResolver; diff --git a/src/js/PlanetaryFeaturesPointer.js b/src/js/PlanetaryFeaturesPointer.js index 88c4206a8..7810d709d 100644 --- a/src/js/PlanetaryFeaturesPointer.js +++ b/src/js/PlanetaryFeaturesPointer.js @@ -89,11 +89,8 @@ export let PlanetaryFeaturesPointer = (function() { return ret; }; - Utils.loadFromMirrors(PlanetaryFeaturesPointer.MIRRORS, {contentType: "text/plain", data: params}) - .then((response) => response.text()) + Utils.loadFromUrls(PlanetaryFeaturesPointer.MIRRORS, {contentType: "text/plain", data: params, dataType: 'text'}) .then((result) => { - aladinInstance.view.setCursor('pointer'); - const lines = result.split('\n'); const fields = csvToArray(lines[0])[0]; @@ -147,8 +144,7 @@ export let PlanetaryFeaturesPointer = (function() { } }) .catch((e) => { - - aladinInstance.view.setCursor('pointer'); + //aladinInstance.view.setCursor('pointer'); aladinInstance.hidePopup(); }) }; diff --git a/src/js/Polyline.js b/src/js/Polyline.js index 9299e4820..a6a897a3c 100644 --- a/src/js/Polyline.js +++ b/src/js/Polyline.js @@ -37,22 +37,22 @@ 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() { function _calculateMag2ForNoSinProjections(line, view) { // check if the line is too big (in the clip space) to be drawn - const [x1, y1] = AladinUtils.viewXyToClipXy(line.x1, line.y1, view); - const [x2, y2] = AladinUtils.viewXyToClipXy(line.x2, line.y2, view); + const [x1, y1] = AladinUtils.viewXyToClipXy(line.x1, line.y1, view.aladin); + const [x2, y2] = AladinUtils.viewXyToClipXy(line.x2, line.y2, view.aladin); const mag2 = (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2); return mag2; } function _isAcrossCollignonZoneForHpxProjection(line, view) { - const [x1, y1] = AladinUtils.viewXyToClipXy(line.x1, line.y1, view); - const [x2, y2] = AladinUtils.viewXyToClipXy(line.x2, line.y2, view); + const [x1, y1] = AladinUtils.viewXyToClipXy(line.x1, line.y1, view.aladin); + const [x2, y2] = AladinUtils.viewXyToClipXy(line.x2, line.y2, view.aladin); // x, y, between -1 and 1 let triIdxCollignionZone = function(x, y) { @@ -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(); @@ -226,7 +231,7 @@ export let Polyline= (function() { let ymax = Number.NEGATIVE_INFINITY; for (var k=0; k
'); - this.domEl.appendTo(parentDiv); + let el = document.createElement('div'); + el.className = 'aladin-popup-container'; + el.innerHTML = '
'; + this.domEl = el; - this.view = view; + parentDiv.appendChild(this.domEl); + this.view = view; var self = this; // close popup - this.domEl.find('.aladin-closeBtn').click(function() {self.hide();}); - + this.domEl.querySelector('.aladin-closeBtn').addEventListener("click", () => {self.hide()}); }; Popup.prototype.hide = function() { - this.domEl.hide(); + this.isShown = false; + this.domEl.style.display = "none"; this.view.mustClearCatalog=true; this.view.catalogForPopup.hide(); @@ -54,17 +56,31 @@ export let Popup = (function() { }; Popup.prototype.show = function() { - this.domEl.show(); + this.isShown = true; + this.domEl.style.display = "block"; }; Popup.prototype.setTitle = function(title) { - this.domEl.find('.aladin-popupTitle').html(title || ''); + this.domEl.querySelector('.aladin-popupTitle').innerHTML = title || ''; }; Popup.prototype.setText = function(text) { - this.domEl.find('.aladin-popupText').html(text || ''); - this.w = this.domEl.outerWidth(); - this.h = this.domEl.outerHeight(); + this.domEl.querySelector('.aladin-popupText').innerHTML = text || ''; + if (!this.isShown) { + // offsetWidth and offsetHeight are gettable + // only if the dom element is displayed + // so we display it and hide it just after + // if it is supposed to be hidden + this.domEl.style.display = "block"; + + this.w = this.domEl.offsetWidth; + this.h = this.domEl.offsetHeight; + + this.domEl.style.display = "none"; + } else { + this.w = this.domEl.offsetWidth; + this.h = this.domEl.offsetHeight; + } }; Popup.prototype.setSource = function(source) { @@ -80,12 +96,13 @@ export let Popup = (function() { Popup.prototype.setPosition = function(x, y) { var newX = x - this.w/2; var newY = y - this.h; + if (this.source) { newY += this.source.catalog.sourceSize/2; } - this.domEl[0].style.left = newX + 'px'; - this.domEl[0].style.top = newY + 'px'; + this.domEl.style.left = newX + 'px'; + this.domEl.style.top = newY + 'px'; }; return Popup; diff --git a/src/js/ProgressiveCat.js b/src/js/ProgressiveCat.js index a06d1748b..2028a9ae6 100644 --- a/src/js/ProgressiveCat.js +++ b/src/js/ProgressiveCat.js @@ -34,8 +34,6 @@ import { Coo } from "./libs/astro/coo.js"; import { Utils } from "./Utils"; import { CooFrameEnum } from "./CooFrameEnum.js"; -import $ from 'jquery'; - // TODO: index sources according to their HEALPix ipix // TODO : merge parsing with class Catalog export let ProgressiveCat = (function() { @@ -103,7 +101,7 @@ export let ProgressiveCat = (function() { } var propertiesURL = rootUrl + '/properties'; - $.ajax({ + Utils.fetch({ url: propertiesURL, method: 'GET', dataType: 'text', @@ -113,8 +111,8 @@ export let ProgressiveCat = (function() { for (var k=0; k { var f = {}; for (var i=0; i resp.text()) + .then((text) => { + let xml = ProgressiveCat.parser.parseFromString(text, "text/xml") + self.fields = getFields(self, xml); self._loadAllskyNewMethod(); - }, - error: function(err) { - self._loadAllskyOldMethod(); - } - }); + }) + .catch(err => self._loadAllskyOldMethod()); }, _loadAllskyNewMethod: function() { var self = this; - $.ajax({ + Utils.fetch({ + desc: "Loading allsky tiles of: " + self.name, url: self.rootUrl + '/' + 'Norder1/Allsky.tsv', method: 'GET', success: function(tsv) { @@ -283,7 +282,8 @@ export let ProgressiveCat = (function() { } }); - $.ajax({ + Utils.fetch({ + desc: "Loading allsky order 2 tiles of: " + self.name, url: self.rootUrl + '/' + 'Norder2/Allsky.tsv', method: 'GET', success: function(tsv) { @@ -309,12 +309,15 @@ export let ProgressiveCat = (function() { _loadLevel2Sources: function() { var self = this; - $.ajax({ + Utils.fetch({ + desc: "Loading level 2 sources of: " + self.name, url: self.rootUrl + '/' + 'Norder2/Allsky.xml', method: 'GET', - success: function(xml) { + success: function(text) { + let xml = ProgressiveCat.parser.parseFromString(text, "text/xml") + self.fields = getFields(self, xml); - self.order2Sources = getSources(self, $(xml).find('CSV').text(), self.fields); + self.order2Sources = getSources(self, xml.querySelectorAll('CSV').innerText, self.fields); if (self.order3Sources) { self.isReady = true; self._finishInitWhenReady(); @@ -328,11 +331,13 @@ export let ProgressiveCat = (function() { _loadLevel3Sources: function() { var self = this; - $.ajax({ + Utils.fetch({ + desc: "Loading level 3 sources of: " + self.name, url: self.rootUrl + '/' + 'Norder3/Allsky.xml', method: 'GET', - success: function(xml) { - self.order3Sources = getSources(self, $(xml).find('CSV').text(), self.fields); + success: function(text) { + let xml = ProgressiveCat.parser.parseFromString(text, "text/xml") + self.order3Sources = getSources(self, xml.querySelectorAll('CSV').innerText, self.fields); if (self.order2Sources) { self.isReady = true; self._finishInitWhenReady(); @@ -349,7 +354,7 @@ export let ProgressiveCat = (function() { this.loadNeededTiles(); }, - draw: function(ctx, frame, width, height, largestDim, zoomFactor) { + draw: function(ctx, frame, width, height, largestDim) { if (! this.isShowing || ! this.isReady) { return; } @@ -547,13 +552,14 @@ export let ProgressiveCat = (function() { if (!this.sourcesCache.get(key)) { (function(self, norder, ipix) { // wrapping function is needed to be able to retrieve norder and ipix in ajax success function var key = norder + '-' + ipix; - $.ajax({ + Utils.fetch({ /* url: Aladin.JSONP_PROXY, data: {"url": self.getTileURL(norder, ipix)}, */ // ATTENTIOn : je passe en JSON direct, car je n'arrive pas a choper les 404 en JSONP url: self.getTileURL(norder, ipix), + desc: "Get tile .tsv " + norder + ' ' + ipix + ' of ' + self.name, method: 'GET', //dataType: 'jsonp', success: function(tsv) { @@ -577,6 +583,7 @@ export let ProgressiveCat = (function() { }; // END OF .prototype functions + ProgressiveCat.parser = new DOMParser(); return ProgressiveCat; })(); diff --git a/src/js/ProjectionEnum.js b/src/js/ProjectionEnum.js index c35f9aa12..d85ac666c 100644 --- a/src/js/ProjectionEnum.js +++ b/src/js/ProjectionEnum.js @@ -29,37 +29,38 @@ *****************************************************************************/ 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 */ - FEYE: {id: 5, fov: 190, label: "fish eye"}, - AIR: {id: 6, fov: 360, label: "airy"}, + TAN: {id: 1, fov: 150, 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}, - ARC: {id: 7, fov: 360, label: "zenital equidistant"}, - NCP: {id: 8, fov: 180, label: "north celestial pole"}, + //ARC: {id: 7, fov: 360, label: "zenital equidistant"}, + //NCP: {id: 8, fov: 180, label: "north celestial pole"}, // Cylindrical MER: {id: 9, fov: 360, label: "mercator"}, - CAR: {id: 10, fov: 360, label: "plate carrée"}, - CEA: {id: 11, fov: 360, label: "cylindrical equal area"}, - CYP: {id: 12, fov: 360, label: "cylindrical perspective"}, + //CAR: {id: 10, fov: 360, label: "plate carrée"}, + //CEA: {id: 11, fov: 360, label: "cylindrical equal area"}, + //CYP: {id: 12, fov: 360, label: "cylindrical perspective"}, // Pseudo-cylindrical AIT: {id: 13, fov: 360, label: "hammer-aitoff"}, - PAR: {id: 14, fov: 360, label: "parabolic"}, - SFL: {id: 15, fov: 360, label: "sanson-flamsteed"}, + //PAR: {id: 14, fov: 360, label: "parabolic"}, + //SFL: {id: 15, fov: 360, label: "sanson-flamsteed"}, MOL: {id: 16, fov: 360, label: "mollweide"}, // Conic - COD: {id: 17, fov: 360, label: "conic equidistant"}, + //COD: {id: 17, fov: 360, label: "conic equidistant"}, // Hybrid - HPX: {id: 19, fov: 360, label: "healpix"}, + //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..a01c7bd06 --- /dev/null +++ b/src/js/Reticle.js @@ -0,0 +1,142 @@ +// 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 { Color } from './Color.js'; + +/****************************************************************************** + * Aladin Lite project + * + * File Source + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +import { Aladin } from "./Aladin"; +import { ALEvent } from './events/ALEvent'; + +export let Reticle = (function() { + // constructor + let Reticle = function(options, aladin) { + this.el = document.createElement('div'); + this.el.className = 'aladin-reticle'; + this.el.innerHTML = '' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + this.aladin = aladin; + + this.color = null; + + aladin.aladinDiv.appendChild(this.el); + + let color = options && options.color || Aladin.DEFAULT_OPTIONS.reticleColor; + let size = options && options.size || Aladin.DEFAULT_OPTIONS.reticleSize; + + let show; + if (options.showReticle === undefined) { + show = Aladin.DEFAULT_OPTIONS.showReticle; + } else { + show = options && options.showReticle; + } + + this.update({color, size, show}) + }; + + Reticle.prototype.update = async function(options) { + options = options || {}; + + if (options.size) { + this._setSize(options.size) + } + + if (options.color) { + this._setColor(options.color) + } + + if (options.show !== undefined) { + this._show(options.show) + } + + ALEvent.RETICLE_CHANGED.dispatchedTo(this.aladin.aladinDiv, { + color: this.color, + size: this.size, + show: this.visible + }) + } + + Reticle.prototype._setColor = async function(color) { + if (!color) { + return; + } + + // 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.getElementsByTagName("svg")[0] + .setAttribute('fill', this.color); + } + + Reticle.prototype._setSize = function(size) { + if (!size) { + return; + } + + this.size = size; + //this.el.style.width = this.size + 'px'; + //this.el.style.height = this.size + 'px'; + + this.el.getElementsByTagName("svg")[0].setAttribute('width', size); + this.el.getElementsByTagName("svg")[0].setAttribute('height', size); + + } + + Reticle.prototype._show = function(show) { + if (show === undefined) { + return; + } + + 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/Selector.js b/src/js/Selector.js new file mode 100644 index 000000000..50ba5c8d5 --- /dev/null +++ b/src/js/Selector.js @@ -0,0 +1,138 @@ +// 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. +// + +import { Color } from "./Color"; +import { CircleSelect } from "./FiniteStateMachine/CircleSelect"; +import { PolySelect } from "./FiniteStateMachine/PolySelect"; +import { RectSelect } from "./FiniteStateMachine/RectSelect"; +import { ALEvent } from "./events/ALEvent"; +/****************************************************************************** + * Aladin Lite project + * + * Class Selector + * + * A selector + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + +export class Selector { + // constructor + constructor(view, options) { + this.customColor = false; + this.color = options && options.color; + if (this.color) { + this.customColor = true; + } + + this.lineWidth = (options && options.lineWidth) || 2; + + this.select = null; + this.view = view; + + this._addListeners(view.aladin) + }; + + _addListeners(aladin) { + let self = this; + ALEvent.RETICLE_CHANGED.listenedBy(aladin.aladinDiv, function (e) { + if (!self.customColor) { + let reticleColor = e.detail.color; + // take the color of the reticle + self.color = new Color(reticleColor).toHex(); + } + }) + } + + setMode(mode) { + if (mode) { + let options = { + color: this.color, + lineWidth: this.lineWidth + }; + + if (mode === 'circle') { + this.select = new CircleSelect(options, this.view) + } else if (mode === 'rect') { + this.select = new RectSelect(options, this.view) + } else if (mode === 'poly') { + this.select = new PolySelect(options, this.view) + } + } + } + + dispatch(to, params) { + this.select.dispatch(to, params); + } + + static getObjects(selection, view) { + if (!selection) { + return; + } + + if (!selection.contains) { + // contains must be implemented for the region + return; + } + + + var objList = []; + var cat, sources, s; + var footprints, f; + var objListPerCatalog = []; + + if (view.catalogs) { + for (var k = 0; k < view.catalogs.length; k++) { + cat = view.catalogs[k]; + if (!cat.isShowing) { + continue; + } + sources = cat.getSources(); + for (var l = 0; l < sources.length; l++) { + s = sources[l]; + if (!s.isShowing || !s.x || !s.y) { + continue; + } + if (selection.contains(s)) { + objListPerCatalog.push(s); + } + } + // footprints + footprints = cat.getFootprints(); + if (footprints) { + const {x, y, w, h} = selection.bbox(); + for (var l = 0; l < footprints.length; l++) { + f = footprints[l]; + if (f.intersectsBBox(x, y, w, h, view)) { + objListPerCatalog.push(f); + } + } + } + + if (objListPerCatalog.length > 0) { + objList.push(objListPerCatalog); + } + objListPerCatalog = []; + } + } + + return objList; + } +} \ No newline at end of file diff --git a/src/js/Sesame.js b/src/js/Sesame.js index b8b3b5413..fd4dd9fcf 100644 --- a/src/js/Sesame.js +++ b/src/js/Sesame.js @@ -30,14 +30,15 @@ import { Utils } from "./Utils"; -import $ from 'jquery'; - export let Sesame = (function() { let Sesame = {}; Sesame.cache = {}; - Sesame.SESAME_URL = "http://cds.u-strasbg.fr/cgi-bin/nph-sesame.jsonp"; + Sesame.lastObjectNameResolved = undefined; + + //Sesame.SESAME_URL = "http://cds.unistra.fr/cgi-bin/nph-sesame.jsonp"; + Sesame.SESAME_URL = "https://cds.unistra.fr/cgi-bin/nph-sesame/-o/SVNI?"; /** find RA, DEC for any target (object name or position) * if successful, callback is called with an object {ra: , dec: } @@ -75,28 +76,58 @@ export let Sesame = (function() { } }; - Sesame.resolve = function(objectName, callbackFunctionSuccess, callbackFunctionError) { - var sesameUrl = Sesame.SESAME_URL; - if (Utils.isHttpsContext()) { - sesameUrl = sesameUrl.replace('http://', 'https://') + Sesame.resolve = function(objectName, callbackFunctionSuccess, callbackFunctionError, useCache = true) { + let self = this; + // check the cache first + if (useCache && this.cache[objectName]) { + let data = this.cache[objectName]; + callbackFunctionSuccess(data) + return; } - - - $.ajax({ - url: sesameUrl , - data: {"object": objectName}, - method: 'GET', - dataType: 'jsonp', - success: function(data) { - if (data.Target && data.Target.Resolver && data.Target.Resolver) { - callbackFunctionSuccess(data); + + var sesameUrl = Sesame.SESAME_URL; + + Sesame.lastObjectNameResolved = objectName; + Utils.fetch({ + desc: "Resolving name of: " + objectName, + url: sesameUrl + objectName, + dataType: 'text', + useProxy: false, + success: function(text) { + if (Sesame.lastObjectNameResolved !== objectName) { + return; } - else { - callbackFunctionError(data); + try { + let coo; + // Find the coo + if (text.includes('%J ')) { + let pos = text.slice(text.indexOf('%J ') + 3); + pos = pos.split(' ') + coo = { + jradeg: +pos[0], + jdedeg: +pos[1] + } + } else { + throw 'coo not found'; + } + + const data = {coo} + + // Cache the result + // check if there is no IMCCE in there + // i.e. planets are moving faster + // better not cache that + if (!text.includes('IMCCE')) { + self.cache[objectName] = data; + } + + callbackFunctionSuccess(data); + } catch(e) { + callbackFunctionError('Error resolving object: ' + objectName + '\nReason: ' + e); } }, error: callbackFunctionError - }); + }); }; return Sesame; diff --git a/src/js/SimbadPointer.js b/src/js/SimbadPointer.js index e03d918b8..d5c1b11ef 100644 --- a/src/js/SimbadPointer.js +++ b/src/js/SimbadPointer.js @@ -44,10 +44,8 @@ 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}) - .then((response) => response.text()) + Utils.loadFromUrls(SimbadPointer.MIRRORS, {contentType: "text/plain", data: params, dataType: 'text'}) .then((result) => { - aladinInstance.view.setCursor('pointer'); var regexp = /(.*?)\/(.*?)\((.*?),(.*?)\)/g; var match = regexp.exec(result); @@ -64,6 +62,7 @@ export let SimbadPointer = (function() { } content += '
Query in CDS portal'; content += ''; + aladinInstance.showPopup(objCoo.lon, objCoo.lat, title, content); } else { @@ -74,10 +73,8 @@ export let SimbadPointer = (function() { } }) .catch((e) => { - aladinInstance.view.setCursor('pointer'); aladinInstance.hidePopup(); - } - ) + }) }; return SimbadPointer; diff --git a/src/js/Source.js b/src/js/Source.js index 6132ad268..5510ef16b 100644 --- a/src/js/Source.js +++ b/src/js/Source.js @@ -97,33 +97,18 @@ export let Source = (function() { }; // function called when a source is clicked. Called by the View object - Source.prototype.actionClicked = function() { + Source.prototype.actionClicked = function(obj) { if (this.catalog && this.catalog.onClick) { var view = this.catalog.view; - if (this.catalog.onClick=='showTable') { - this.select(); - - let singleSourceTable = { - 'rows': [this], - 'fields': this.catalog.fields, - 'fieldsClickedActions': this.catalog.fieldsClickedActions, - '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); + if (this.catalog.onClick == 'showTable') { + if (!obj) { + obj = this; + } + view.selectObjects([obj]); } - else if (this.catalog.onClick=='showPopup') { - - view.popup.setTitle('

'); + else if (this.catalog.onClick == 'showPopup') { + view.aladin.popup.setTitle('

'); var m = '
'; m += ''; for (var key in this.data) { @@ -131,9 +116,9 @@ export let Source = (function() { } m += '
'; m += '
'; - view.popup.setText(m); - view.popup.setSource(this); - view.popup.show(); + view.aladin.popup.setText(m); + view.aladin.popup.setSource(this); + view.aladin.popup.show(); } else if (typeof this.catalog.onClick === 'function') { this.catalog.onClick(this); diff --git a/src/js/URLBuilder.js b/src/js/URLBuilder.js index 9f1a4865b..250376ac8 100644 --- a/src/js/URLBuilder.js +++ b/src/js/URLBuilder.js @@ -29,17 +29,21 @@ *****************************************************************************/ import { Coo } from './libs/astro/coo.js'; import { Utils } from './Utils'; + export let URLBuilder = (function() { let URLBuilder = { - buildSimbadCSURL: function(target, radiusDegrees) { - if (target && (typeof target === "object")) { - if ('ra' in target && 'dec' in target) { - var coo = new Coo(target.ra, target.dec, 7); - target = coo.format('s'); - } + buildSimbadCSURL: function(ra, dec, radiusDegrees, options) { + let url = 'https://simbad.cds.unistra.fr/cone?RA=' + ra + '&DEC=' + dec + '&SR=' + radiusDegrees + '&RESPONSEFORMAT=votable'; + + if (options && options.limit) { + url += '&MAXREC=' + options.limit; } - return 'https://alasky.unistra.fr/cgi/simbad-flat/simbad-cs.py?target=' + encodeURIComponent(target) + '&SR=' + radiusDegrees + '&format=votable&SRUNIT=deg&SORTBY=nbref'; + + const orderBy = options && options.orderBy || 'nb_ref'; + url += '&ORDER_BY=' + orderBy; + + return url; }, buildNEDPositionCSURL: function(ra, dec, radiusDegrees) { @@ -50,6 +54,30 @@ export let URLBuilder = (function() { return 'https://ned.ipac.caltech.edu/cgi-bin/nph-objsearch?search_type=Near+Name+Search&radius=' + (60 * radiusDegrees) + '&of=xml_main&objname=' + object; }, + buildSKAORucioCSURL: function(target, radiusDegrees) { + let posParam; + if (target && (typeof target === "object")) { + if ('ra' in target && 'dec' in target) { + posParam = encodeURIComponent(target.ra) + '%20' + encodeURIComponent(target.dec); + } + } + else { + var isObjectName = /[a-zA-Z]/.test(target); + if (isObjectName) { + posParam = encodeURIComponent(target); + } + else { + var coo = new Coo(); + coo.parse(target); + posParam = encodeURIComponent(coo.lon) + '%20' + encodeURIComponent(coo.lat); + } + } + + if (posParam) { + 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'; + } + }, + buildVizieRCSURL: function(vizCatId, target, radiusDegrees, options) { if (target && (typeof target === "object")) { if ('ra' in target && 'dec' in target) { diff --git a/src/js/Utils.ts b/src/js/Utils.ts index e9fe92059..f999b8cf8 100644 --- a/src/js/Utils.ts +++ b/src/js/Utils.ts @@ -28,6 +28,8 @@ *****************************************************************************/ import {JSONP_PROXY} from "@/js/Constants"; +import { ALEvent } from './events/ALEvent'; + export let Utils: any = {} @@ -38,7 +40,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) { if (event.offsetX) { return {x: event.offsetX, y: event.offsetY} } else { @@ -67,9 +69,10 @@ Utils.relMouseCoords = function (canvas: HTMLCanvasElement, event: MouseEvent) { var clientX = e.clientX var clientY = e.clientY - if (e.originalEvent.changedTouches) { - clientX = e.originalEvent.changedTouches[0].clientX - clientY = e.originalEvent.changedTouches[0].clientY + + if (e && e.changedTouches) { + clientX = e.changedTouches[0].clientX + clientY = e.changedTouches[0].clientY } var offsetX = clientX - borderLeftWidth - rect.left @@ -223,55 +226,66 @@ 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; + const dataType = (options && options.dataType) || 'text'; + // 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 url = urls[0]; + if (data) { + url += '?' + new URLSearchParams(data) + } + + let params = { + url, // *GET, POST, PUT, DELETE, etc. method: 'GET', - headers: { + /*headers: { 'Content-Type': contentType - }, + },*/ // no-cors, *cors, same-origin - mode: 'cors', + 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, + dataType } - const url = urls[0] + '?' + new URLSearchParams(data) - return fetch(url, init) - .then((response) => { + if (contentType) { + params.headers = { + 'Content-Type': contentType + }; + } + + return Utils.fetch({ + ...params, + success: (resp) => { // completed request before timeout fired clearTimeout(timeoutId) - - if (!response.ok) { - return Promise.reject('Url: ', urls[0], ' cannot be reached in some way.') + return Promise.resolve(resp) + }, + error: (e) => { + // The request aborted because it was to slow, fetch the next url given recursively + // 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 response + return Utils.loadFromUrls(urls.slice(1), options); } - }) - .catch((e) => { - // The request aborted because it was to slow, fetch the next url given recursively - return Utils.loadFromMirrors(urls.slice(1), options) - }) + } + }) } +/* // return the jquery ajax object configured with the requested parameters // by default, we use the proxy (safer, as we don't know if the remote server supports CORS) Utils.getAjaxObject = function (url, method, dataType, useProxy) { @@ -293,6 +307,101 @@ Utils.getAjaxObject = function (url, method, dataType, useProxy) { dataType: dataType }) } +*/ + +Utils.fetch = function(params) { + let url = new URL(params.url); + if (params.useProxy === true) { + url = Utils.handleCORSNotSameOrigin(url) + } + + if (params.data) { + // add the search params to the url object + for (const key in params.data) { + url.searchParams.append(key, params.data[key]); + } + } + + let request = new Request(url, { + method: params.method || 'GET', + ...params + }) + + let task = { + message: params.desc || 'fetching: ' + url, + id: Utils.uuidv4() + }; + ALEvent.FETCH.dispatchedTo(document, {task}); + + return fetch(request) + .then((resp) => { + if (params.dataType && params.dataType.includes('json')) { + return resp.json() + } else if (params.dataType && params.dataType.includes('blob')) { + return resp.blob() + } else { + return resp.text() + } + }) + .then(data => { + if (params.success) { + return params.success(data) + } + }) + .catch(e => { + if (params.error) { + params.error(e) + } else { + alert(e) + } + }) + .finally(() => { + ALEvent.RESOURCE_FETCHED.dispatchedTo(document, {task}); + }) +} + +Utils.on = function(element, events, callback) { + events.split(' ') + .forEach(e => { + element.addEventListener(e, callback) + }) +} + +Utils.isTouchScreenDevice = undefined; + +Utils.hasTouchScreen = function() { + if (Utils.isTouchScreenDevice === undefined) { + let hasTouchScreen = false; + if ("maxTouchPoints" in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } else if ("msMaxTouchPoints" in navigator) { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } else { + const mQ = matchMedia?.("(pointer:coarse)"); + if (mQ?.media === "(pointer:coarse)") { + hasTouchScreen = !!mQ.matches; + } else if ("orientation" in window) { + hasTouchScreen = true; // deprecated, but good fallback + } else { + // Only as a last resort, fall back to user agent sniffing + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + + Utils.isTouchScreenDevice = hasTouchScreen; + } + + return Utils.isTouchScreenDevice; +} + + + +Utils.openNewTab = function(url) { + window.open(url, '_blank').focus(); +} // return true if script is executed in a HTTPS context // return false otherwise @@ -410,4 +519,13 @@ Utils.download = function(url, name = undefined) { a.download = name; } a.click() -} \ No newline at end of file +} + +Utils.measureTime = function(msg, method) { + let startTime = performance.now() + let output = method() + let deltaTime = performance.now() - startTime; + console.log(msg, deltaTime); + + return output; +}; diff --git a/src/js/View.js b/src/js/View.js index 221470446..c33ee53aa 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -31,7 +31,6 @@ import { Aladin } from "./Aladin.js"; import A from "./A.js"; -import { Popup } from "./Popup.js"; import { HealpixGrid } from "./HealpixGrid.js"; import { ProjectionEnum } from "./ProjectionEnum.js"; import { Utils } from "./Utils"; @@ -43,26 +42,32 @@ import { Polyline } from "./Polyline.js"; import { CooFrameEnum } from "./CooFrameEnum.js"; import { requestAnimFrame } from "./libs/RequestAnimationFrame.js"; 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 { ObsCore } from "./vo/ObsCore.js"; +import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js"; +import { Layout } from "./gui/Layout.js"; +import { SAMPActionButton } from "./gui/Button/SAMP.js"; +import { Icon } from "./gui/Widgets/Icon.js"; +import openerIconUrl from '../../assets/icons/loading.svg' 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; this.aladinDiv = this.aladin.aladinDiv; - this.popup = new Popup(this.aladinDiv, this); this.createCanvases(); + 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 { @@ -121,8 +126,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; @@ -135,25 +139,19 @@ export let View = (function () { lon = lat = 0; this.projection = ProjectionEnum.SIN; - // Prev time of the last frame - this.zoomFactor = this.wasm.getClipZoomFactor(); - 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); + this.selector = new Selector(this); + // Zoom starting setting 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, @@ -206,8 +204,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'; @@ -227,10 +224,24 @@ export let View = (function () { init(this); // listen to window resize and reshape canvases this.resizeTimer = null; - let resizeObserver = new ResizeObserver(() => { - self.fixLayoutDimensions(); - self.requestRedraw(); - }); + /*if ('ontouchstart' in window) { + Utils.on(document, 'orientationchange', (e) => { + self.fixLayoutDimensions(); + }) + } else {*/ + let resizeLayout = () => { + self.fixLayoutDimensions(); + } + let doit; + this.resizeObserver = new ResizeObserver(() => { + clearTimeout(doit); + doit = setTimeout(resizeLayout, 100); + }); + + self.resizeObserver.observe(this.aladinDiv); + //} + + /**/ this.throttledPositionChanged = Utils.throttle( () => { @@ -264,28 +275,26 @@ export let View = (function () { View.CALLBACKS_THROTTLE_TIME_MS, ); - resizeObserver.observe(this.aladinDiv); - //$(window).resize(() => { self.fixLayoutDimensions(); - //self.requestRedraw(); - //}); + self.redraw() + // in some contexts (Jupyter notebook for instance), the parent div changes little time after Aladin Lite creation // this results in canvas dimension to be incorrect. // The following line tries to fix this issue - //setTimeout(function () { - //var computedWidth = $(self.aladinDiv).width(); - //var computedHeight = $(self.aladinDiv).height(); + /*setTimeout(function () { + var computedWidth = $(self.aladinDiv).width(); + var computedHeight = $(self.aladinDiv).height(); - //if (self.width !== computedWidth || self.height === computedHeight) { - //self.fixLayoutDimensions(); - // As the WebGL backend has been resized correctly by - // the previous call, we can get the zoom factor from it + if (self.width !== computedWidth || self.height === computedHeight) { + self.fixLayoutDimensions(); + // As the WebGL backend has been resized correctly by + // the previous call, we can get the zoom factor from it - //self.setZoom(self.fov); // needed to force recomputation of displayed FoV - //} + self.setZoom(self.fov); // needed to force recomputation of displayed FoV + } - self.requestRedraw(); - //}, 1000); + self.requestRedraw(); + }, 1000);*/ }; @@ -304,17 +313,36 @@ export let View = (function () { // (re)create needed canvases View.prototype.createCanvases = function () { - var a = $(this.aladinDiv); - a.find('.aladin-imageCanvas').remove(); - a.find('.aladin-gridCanvas').remove(); - a.find('.aladin-catalogCanvas').remove(); + let imageCanvas = this.aladinDiv.querySelector('.aladin-imageCanvas'); + if (imageCanvas) { + imageCanvas.remove(); + } + + let gridCanvas = this.aladinDiv.querySelector('.aladin-gridCanvas'); + if (gridCanvas) { + gridCanvas.remove(); + } + + let catalogCanvas = this.aladinDiv.querySelector('.aladin-catalogCanvas') + if (catalogCanvas) { + catalogCanvas.remove(); + } // canvas to draw the images - this.imageCanvas = $("").appendTo(this.aladinDiv)[0]; - this.gridCanvas = $("").appendTo(this.aladinDiv)[0]; + let createCanvas = (name) => { + // Create a new canvas element + let canvas = document.createElement('canvas'); + canvas.className = name; + + // Append the canvas to the aladinDiv + this.aladinDiv.insertBefore(canvas, this.aladinDiv.firstChild); + + return canvas; + }; - // canvas to draw the catalogs - this.catalogCanvas = $("").appendTo(this.aladinDiv)[0]; + this.catalogCanvas = createCanvas('aladin-catalogCanvas'); + this.gridCanvas = createCanvas('aladin-gridCanvas'); + this.imageCanvas = createCanvas('aladin-imageCanvas'); }; // called at startup and when window is resized @@ -322,8 +350,8 @@ export let View = (function () { View.prototype.fixLayoutDimensions = function () { Utils.cssScale = undefined; - var computedWidth = $(this.aladinDiv).width(); - var computedHeight = $(this.aladinDiv).height(); + var computedWidth = parseFloat(window.getComputedStyle(this.aladinDiv).width); + var computedHeight = parseFloat(window.getComputedStyle(this.aladinDiv).height); this.width = Math.max(computedWidth, 1); this.height = Math.max(computedHeight, 1); // this prevents many problems when div size is equal to 0 @@ -356,17 +384,20 @@ export let View = (function () { // change logo if (!this.logoDiv) { - this.logoDiv = $(this.aladinDiv).find('.aladin-logo')[0]; + this.logoDiv = this.aladinDiv.querySelector('.aladin-logo'); } - if (this.width > 800) { - $(this.logoDiv).removeClass('aladin-logo-small'); - $(this.logoDiv).addClass('aladin-logo-large'); - $(this.logoDiv).css('width', '90px'); - } - else { - $(this.logoDiv).addClass('aladin-logo-small'); - $(this.logoDiv).removeClass('aladin-logo-large'); - $(this.logoDiv).css('width', '32px'); + + if (this.logoDiv) { + if (this.width > 800) { + this.logoDiv.classList.remove('aladin-logo-small'); + this.logoDiv.classList.add('aladin-logo-large'); + this.logoDiv.style.width = '90px'; + } + else { + this.logoDiv.classList.add('aladin-logo-small'); + this.logoDiv.classList.remove('aladin-logo-large'); + this.logoDiv.style.width = '32px'; + } } this.computeNorder(); @@ -381,29 +412,35 @@ export let View = (function () { ctx.oImageSmoothingEnabled = enableSmoothing; } + View.prototype.startSelection = function(mode, callback) { + this.selector.setMode(mode); + this.selector.dispatch('start', {callback}); + } View.prototype.setMode = function (mode) { this.mode = mode; - if (this.mode == View.SELECT) { - this.setCursor('crosshair'); - } - else if (this.mode == View.TOOL_SIMBAD_POINTER) { - this.popup.hide(); + + if (this.mode == View.TOOL_SIMBAD_POINTER) { + this.aladin.popup.hide(); this.catalogCanvas.style.cursor = ''; - $(this.catalogCanvas).addClass('aladin-sp-cursor'); + this.catalogCanvas.classList.add('aladin-sp-cursor'); } - else { + else if (this.mode == View.PAN) { this.setCursor('default'); } + else if (this.mode == View.SELECT) { + this.setCursor('crosshair'); + this.aladin.showReticle(false) + } + + ALEvent.MODE.dispatchedTo(this.aladin.aladinDiv, {mode}); }; View.prototype.setCursor = function (cursor) { if (this.catalogCanvas.style.cursor == cursor) { return; } - if (this.mode == View.TOOL_SIMBAD_POINTER) { - return; - } + this.catalogCanvas.style.cursor = cursor; }; @@ -445,7 +482,7 @@ export let View = (function () { }; - View.prototype.setActiveHiPSLayer = function (layer) { + View.prototype.selectLayer = function (layer) { if (!this.imageLayers.has(layer)) { throw layer + ' does not exists. So cannot be selected'; } @@ -453,34 +490,35 @@ export let View = (function () { }; var createListeners = function (view) { - var hasTouchEvents = false; - if ('ontouchstart' in window) { - hasTouchEvents = true; + if ('virtualKeyboard' in navigator) { + // The VirtualKeyboard API is supported! + navigator.virtualKeyboard.overlaysContent = true; } // 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.deselectObjects() + view.unselectObjects() try { const lonlat = view.wasm.screenToWorld(xymouse.x, xymouse.y); var radec = view.wasm.viewToICRSCooSys(lonlat[0], lonlat[1]); - view.pointTo(radec[0], radec[1], { forceAnimation: true }); + view.pointTo(radec[0], radec[1]); } catch (err) { return; } }; - if (!hasTouchEvents) { - $(view.catalogCanvas).dblclick(onDblClick); + + if (!Utils.hasTouchScreen()) { + Utils.on(view.catalogCanvas, 'dblclick', onDblClick); } // prevent default context menu from appearing (potential clash with right-click cuts control) - $(view.catalogCanvas).bind("contextmenu", function (e) { + Utils.on(view.catalogCanvas, "contextmenu", function (e) { // do something here... e.preventDefault(); }, false); @@ -489,11 +527,34 @@ export let View = (function () { let cutMinInit = null let cutMaxInit = null; - $(view.catalogCanvas).bind("mousedown touchstart", function (e) { + var onlongtouch = function(e) { + if (view.aladin.statusBar) { + view.aladin.statusBar.removeMessage('opening-ctxmenu') + } + + view.aladin.contextMenu && view.aladin.contextMenu.show({e}); + }; + var longTouchTimer; + var longTouchDuration = 800; + var xystart; + + Utils.on(view.catalogCanvas, "mousedown touchstart", function (e) { e.preventDefault(); e.stopPropagation(); - const xymouse = Utils.relMouseCoords(view.imageCanvas, 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, + xy: xymouse, + }); + + xystart = xymouse; if (e.which === 3 || e.button === 2) { view.rightClick = true; @@ -501,25 +562,41 @@ export let View = (function () { view.rightclickx = xymouse.x; view.rightclicky = xymouse.y; - const imageLayer = view.imageLayers.get(view.selectedLayer); - if (imageLayer) { - // Take as start cut values what is inside the properties - // If the cuts are not defined in the metadata of the survey - // then we take what has been defined by the user - if (imageLayer.imgFormat === "fits") { - cutMinInit = imageLayer.properties.minCutout || imageLayer.getColorCfg().minCut || 0.0; - cutMaxInit = imageLayer.properties.maxCutout || imageLayer.getColorCfg().maxCut || 1.0; - } else { - cutMinInit = imageLayer.getColorCfg().minCut || 0.0; - cutMaxInit = imageLayer.getColorCfg().maxCut || 1.0; + if (view.selectedLayer) { + const imageLayer = view.imageLayers.get(view.selectedLayer); + if (imageLayer) { + // Take as start cut values what is inside the properties + // If the cuts are not defined in the metadata of the survey + // then we take what has been defined by the user + if (imageLayer.imgFormat === "fits") { + cutMinInit = imageLayer.properties.minCutout || imageLayer.getColorCfg().minCut || 0.0; + cutMaxInit = imageLayer.properties.maxCutout || imageLayer.getColorCfg().maxCut || 1.0; + } else { + cutMinInit = imageLayer.getColorCfg().minCut || 0.0; + cutMaxInit = imageLayer.getColorCfg().maxCut || 1.0; + } } } return; } + // detect long touch + if (e.type === 'touchstart' && e.targetTouches && e.targetTouches.length == 1) { + if (view.aladin.statusBar) { + view.aladin.statusBar.appendMessage({ + id: 'opening-ctxmenu', + message: 'Opening menu...', + duration: 'unlimited', + type: 'loading' + }) + } + + longTouchTimer = setTimeout(() => {onlongtouch(e); view.dragging = false;}, longTouchDuration); + } + // zoom pinching - if (e.type === 'touchstart' && e.originalEvent && e.originalEvent.targetTouches && e.originalEvent.targetTouches.length == 2) { + if (e.type === 'touchstart' && e.targetTouches && e.targetTouches.length == 2) { view.dragging = false; view.pinchZoomParameters.isPinching = true; @@ -527,38 +604,54 @@ export let View = (function () { //view.pinchZoomParameters.initialFov = Math.max(fov[0], fov[1]); var fov = view.wasm.getFieldOfView(); view.pinchZoomParameters.initialFov = fov; - view.pinchZoomParameters.initialDistance = Math.sqrt(Math.pow(e.originalEvent.targetTouches[0].clientX - e.originalEvent.targetTouches[1].clientX, 2) + Math.pow(e.originalEvent.targetTouches[0].clientY - e.originalEvent.targetTouches[1].clientY, 2)); + view.pinchZoomParameters.initialDistance = Math.sqrt(Math.pow(e.targetTouches[0].clientX - e.targetTouches[1].clientX, 2) + Math.pow(e.targetTouches[0].clientY - e.targetTouches[1].clientY, 2)); view.fingersRotationParameters.initialViewAngleFromCenter = view.wasm.getRotationAroundCenter(); - view.fingersRotationParameters.initialFingerAngle = Math.atan2(e.originalEvent.targetTouches[1].clientY - e.originalEvent.targetTouches[0].clientY, e.originalEvent.targetTouches[1].clientX - e.originalEvent.targetTouches[0].clientX) * 180.0 / Math.PI; + view.fingersRotationParameters.initialFingerAngle = Math.atan2(e.targetTouches[1].clientY - e.targetTouches[0].clientY, e.targetTouches[1].clientX - e.targetTouches[0].clientX) * 180.0 / Math.PI; return; } - view.dragx = xymouse.x; - view.dragy = xymouse.y; + view.dragCoo = xymouse; view.dragging = true; - if (view.mode == View.PAN) { + view.aladin.contextMenu && view.aladin.contextMenu._hide() + + if (view.mode === View.PAN) { view.setCursor('move'); } - else if (view.mode == View.SELECT) { - view.selectStartCoo = { x: view.dragx, y: view.dragy }; - } - view.wasm.pressLeftMouseButton(view.dragx, view.dragy); + view.wasm.pressLeftMouseButton(view.dragCoo.x, view.dragCoo.y); + + if (view.mode === View.SELECT) { + view.selector.dispatch('mousedown', {coo: xymouse}) + } // 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 return true; }); - $(view.catalogCanvas).bind("mouseup", function (e) { - if (view.rightClick) { + Utils.on(view.catalogCanvas, "mouseup", 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 + }, + 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; @@ -568,9 +661,41 @@ export let View = (function () { return; } + + if (view.mode === View.SELECT) { + view.selector.dispatch('mouseup', {coo: xymouse}) + } }); + + // reacting on 'click' rather on 'mouseup' is more reliable when panning the view + Utils.on(view.catalogCanvas, "click mouseout touchend touchcancel", 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, + ev: e, + }); + + + if (e.type === 'touchend' || e.type === 'touchcancel') { + if (longTouchTimer) { + if (view.aladin.statusBar) { + view.aladin.statusBar.removeMessage('opening-ctxmenu') + } + clearTimeout(longTouchTimer) + } + } + + xystart = undefined; - $(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; @@ -585,11 +710,13 @@ export let View = (function () { return; } - var wasDragging = view.realDragging === true; - var selectionHasEnded = view.mode === View.SELECT && view.dragging; + var wasDragging = view.realDragging === true; if (view.dragging) { // if we were dragging, reset to default cursor - view.setCursor('default'); + if(view.mode === View.PAN) { + view.setCursor('default'); + } + view.dragging = false; if (wasDragging) { @@ -597,85 +724,32 @@ 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.aladin.fire( - 'selectend', - selectedObjects - ); - - view.requestRedraw(); - - return; - } - view.mustClearCatalog = true; - view.dragx = view.dragy = null; - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + view.dragCoo = null; 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); + if (e.type === "mouseout" || e.type === "touchcancel") { + if (view.mode === View.SELECT) { + view.selector.dispatch('mouseout', {coo: xymouse, e}) } return; } + + if (e.type === "touchend") { + if (view.mode === View.SELECT) { + view.selector.dispatch('mouseup', {coo: xymouse}) + + return; + } + } } 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 } @@ -683,17 +757,17 @@ export let View = (function () { // popup to show ? var objs = view.closestObjects(xymouse.x, xymouse.y, 5); if (!wasDragging && objs) { - view.deselectObjects(); + view.unselectObjects(); var o = objs[0]; // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC if (o.marker) { // could be factorized in Source.actionClicked - view.popup.setTitle(o.popupTitle); - view.popup.setText(o.popupDesc); - view.popup.setSource(o); - view.popup.show(); + view.aladin.popup.setTitle(o.popupTitle); + view.aladin.popup.setText(o.popupDesc); + view.aladin.popup.setSource(o); + view.aladin.popup.show(); } else { if (view.lastClickedObject) { @@ -709,7 +783,6 @@ export let View = (function () { var objClickedFunction = view.aladin.callbacksByEventName['objectClicked']; (typeof objClickedFunction === 'function') && objClickedFunction(o, xymouse); - if (o.isFootprint()) { var footprintClickedFunction = view.aladin.callbacksByEventName['footprintClicked']; if (typeof footprintClickedFunction === 'function' && o != view.lastClickedObject) { @@ -718,29 +791,28 @@ export let View = (function () { } view.lastClickedObject = o; - } else { - if (!wasDragging) { - // Deselect objects if any - view.deselectObjects(); - - // If there is a past clicked object - if (view.lastClickedObject) { - view.aladin.measurementTable.hide(); - view.popup.hide(); - - // Deselect the last clicked object - if (view.lastClickedObject instanceof Ellipse || view.lastClickedObject instanceof Circle || view.lastClickedObject instanceof Polyline) { - view.lastClickedObject.deselect(); - } else { - // Case where lastClickedObject is a Source - view.lastClickedObject.actionOtherObjectClicked(); - } + } else if (!wasDragging) { + // Deselect objects if any + view.unselectObjects(); + + // If there is a past clicked object + if (view.lastClickedObject) { + //view.aladin.measurementTable.hide(); + //view.aladin.sodaForm.hide(); + view.aladin.popup.hide(); + + // Deselect the last clicked object + if (view.lastClickedObject instanceof Ellipse || view.lastClickedObject instanceof Circle || view.lastClickedObject instanceof Polyline) { + view.lastClickedObject.deselect(); + } else { + // Case where lastClickedObject is a Source + view.lastClickedObject.actionOtherObjectClicked(); + } - var objClickedFunction = view.aladin.callbacksByEventName['objectClicked']; - (typeof objClickedFunction === 'function') && objClickedFunction(null, xymouse); + var objClickedFunction = view.aladin.callbacksByEventName['objectClicked']; + (typeof objClickedFunction === 'function') && objClickedFunction(null, xymouse); - view.lastClickedObject = null; - } + view.lastClickedObject = null; } } @@ -759,13 +831,42 @@ export let View = (function () { //view.requestRedraw(); view.wasm.releaseLeftButtonMouse(xymouse.x, xymouse.y); + + if (view.mode === View.SELECT && e.type === "click") { + view.selector.dispatch('click', {coo: xymouse}) + } }); var lastHoveredObject; // save last object hovered by mouse var lastMouseMovePos = null; - $(view.catalogCanvas).bind("mousemove touchmove", function (e) { + Utils.on(view.catalogCanvas, "mousemove touchmove", function (e) { e.preventDefault(); - const xymouse = Utils.relMouseCoords(view.imageCanvas, e); + //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 (e.type === 'touchmove' && xystart) { + let dist = (() => { + return (xymouse.x - xystart.x)*(xymouse.x - xystart.x) + (xymouse.y - xystart.y)*(xymouse.y - xystart.y) + })(); + if (longTouchTimer && dist > 100) { + if (view.aladin.statusBar) { + view.aladin.statusBar.removeMessage('opening-ctxmenu') + } + clearTimeout(longTouchTimer) + xystart = undefined; + } + } if (view.rightClick) { var onRightClickMoveFunction = view.aladin.callbacksByEventName['rightClickMove']; @@ -799,9 +900,9 @@ export let View = (function () { return; } - if (e.type === 'touchmove' && view.pinchZoomParameters.isPinching && e.originalEvent && e.originalEvent.touches && e.originalEvent.touches.length == 2) { + if (e.type === 'touchmove' && view.pinchZoomParameters.isPinching && e.touches && e.touches.length == 2) { // rotation - var currentFingerAngle = Math.atan2(e.originalEvent.targetTouches[1].clientY - e.originalEvent.targetTouches[0].clientY, e.originalEvent.targetTouches[1].clientX - e.originalEvent.targetTouches[0].clientX) * 180.0 / Math.PI; + var currentFingerAngle = Math.atan2(e.targetTouches[1].clientY - e.targetTouches[0].clientY, e.targetTouches[1].clientX - e.targetTouches[0].clientX) * 180.0 / Math.PI; var fingerAngleDiff = view.fingersRotationParameters.initialFingerAngle - currentFingerAngle; // rotation is initiated when angle is equal or greater than 7 degrees if (!view.fingersRotationParameters.rotationInitiated && Math.abs(fingerAngleDiff) >= 7) { @@ -823,8 +924,8 @@ export let View = (function () { } // zoom - const dist = Math.sqrt(Math.pow(e.originalEvent.touches[0].clientX - e.originalEvent.touches[1].clientX, 2) + Math.pow(e.originalEvent.touches[0].clientY - e.originalEvent.touches[1].clientY, 2)); - const fov = Math.min(Math.max(view.pinchZoomParameters.initialFov * view.pinchZoomParameters.initialDistance / dist, 0.00002777777), view.fovLimit); + const dist = Math.sqrt(Math.pow(e.touches[0].clientX - e.touches[1].clientX, 2) + Math.pow(e.touches[0].clientY - e.touches[1].clientY, 2)); + const fov = Math.min(Math.max(view.pinchZoomParameters.initialFov * view.pinchZoomParameters.initialDistance / dist, 0.00002777777), view.projection.fov); view.setZoom(fov); return; @@ -834,12 +935,12 @@ export let View = (function () { view.updateObjectsLookup(); } - if (!view.dragging || hasTouchEvents) { + /*if (!view.dragging || Utils.hasTouchScreen()) { // update location box - view.updateLocation(xymouse.x, xymouse.y, false); - } + view.updateLocation({mouseX: xymouse.x, mouseY: xymouse.y}); + }*/ - if (!view.dragging) { + if (!view.dragging && !view.moving && view.mode === View.PAN) { // call listener of 'mouseMove' event var onMouseMoveFunction = view.aladin.callbacksByEventName['mouseMove']; if (typeof onMouseMoveFunction === 'function') { @@ -854,103 +955,118 @@ export let View = (function () { lastMouseMovePos = pos; } - if (!view.dragging && !view.mode == View.SELECT) { - // closestObjects is very costly, we would like to not do it - // especially if the objectHovered function is not defined. - var closest = view.closestObjects(xymouse.x, xymouse.y, 5); + // closestObjects is very costly, we would like to not do it + // especially if the objectHovered function is not defined. + var closest = view.closestObjects(xymouse.x, xymouse.y, 5); - if (closest) { - let o = closest[0]; - var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered']; - var footprintHoveredFunction = view.aladin.callbacksByEventName['footprintHovered']; + if (closest) { + let o = closest[0]; + var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered']; + var footprintHoveredFunction = view.aladin.callbacksByEventName['footprintHovered']; - view.setCursor('pointer'); - if (typeof objHoveredFunction === 'function' && o != lastHoveredObject) { - var ret = objHoveredFunction(o, xymouse); - } + view.setCursor('pointer'); + if (typeof objHoveredFunction === 'function' && o != lastHoveredObject) { + var ret = objHoveredFunction(o, xymouse); + } - if (o.isFootprint()) { - if (typeof footprintHoveredFunction === 'function' && o != lastHoveredObject) { - var ret = footprintHoveredFunction(o, xymouse); - } + if (o.isFootprint()) { + if (typeof footprintHoveredFunction === 'function' && o != lastHoveredObject) { + var ret = footprintHoveredFunction(o, xymouse); } + } - lastHoveredObject = o; - } else { - view.setCursor('default'); - var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop']; - if (lastHoveredObject) { - // Redraw the scene if the lastHoveredObject is a footprint (e.g. circle or polygon) - if (lastHoveredObject.isFootprint()) { - view.requestRedraw(); - } - - if (typeof objHoveredStopFunction === 'function') { - // call callback function to notify we left the hovered object - var ret = objHoveredStopFunction(lastHoveredObject, xymouse); - } + lastHoveredObject = o; + } else { + view.setCursor('default'); + var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop']; + if (lastHoveredObject) { + // Redraw the scene if the lastHoveredObject is a footprint (e.g. circle or polygon) + //if (lastHoveredObject.isFootprint()) { + // view.requestRedraw(); + //} + + if (typeof objHoveredStopFunction === 'function') { + // call callback function to notify we left the hovered object + var ret = objHoveredStopFunction(lastHoveredObject, xymouse); } - - lastHoveredObject = null; } + + lastHoveredObject = null; } + if (e.type === "mousemove") { return; } } - if (!view.dragging) { - return; + if (view.mode === View.SELECT) { + view.selector.dispatch('mousemove', {coo: xymouse}) } - //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; - - if (view.mode == View.SELECT) { - view.requestRedraw(); + if (!view.dragging) { return; } view.realDragging = true; - 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; + var s1 = view.dragCoo, s2 = xymouse; + // update drag coo with the new position + view.dragCoo = xymouse; - // Apply position changed callback after the move - view.throttledPositionChanged(); + /*if (view.mode == View.SELECT) { + view.requestRedraw(); + return; + }*/ + + if (view.mode === View.PAN) { + view.wasm.moveMouse(s1.x, s1.y, s2.x, s2.y); + view.wasm.goFromTo(s1.x, s1.y, s2.x, s2.y); + + view.updateCenter(); + + ALEvent.POSITION_CHANGED.dispatchedTo(view.aladin.aladinDiv, view.viewCenter); + + // Apply position changed callback after the move + view.throttledPositionChanged(); + } }); //// endof mousemove //// // disable text selection on IE - $(view.aladinDiv).onselectstart = function () { return false; } + Utils.on(view.aladinDiv, "selectstart", function () { return false; }) var eventCount = 0; var eventCountStart; var isTouchPad; - var oldTime = 0; - var newTime = 0; + var scale = 0.0; + Utils.on(view.catalogCanvas, 'wheel', function (e) { + e.preventDefault(); + e.stopPropagation(); - $(view.catalogCanvas).on('wheel', function (event) { - event.preventDefault(); - event.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 || e.detail || (-e.wheelDelta); + + // Limit the minimum and maximum zoom levels + //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.deltaY; + }*/ // See https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript // for detecting the use of a touchpad @@ -975,7 +1091,7 @@ export let View = (function () { // The value of the field of view is determined // inside the backend const triggerZoom = (amount) => { - if (delta > 0.0) { + if (delta < 0.0) { view.increaseZoom(amount); } else { view.decreaseZoom(amount); @@ -983,18 +1099,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.0002; } else { - // mouse - triggerZoom(0.007); + a1 = 0.01; + a0 = 0.0004; } + + const alpha = Math.pow(view.fov / view.projection.fov, 0.5); + + const lerp = a0 * alpha + a1 * (1.0 - alpha); + triggerZoom(lerp); } if (!view.debounceProgCatOnZoom) { @@ -1015,8 +1136,12 @@ export let View = (function () { var init = function (view) { var stats = new Stats(); stats.domElement.style.top = '50px'; - if ($('#aladin-statsDiv').length > 0) { - $('#aladin-statsDiv')[0].appendChild(stats.domElement); + + var statsDiv = document.getElementById('aladin-statsDiv'); + + if (statsDiv) { + // Append stats.domElement to statsDiv + statsDiv.appendChild(stats.domElement); } view.stats = stats; @@ -1025,24 +1150,8 @@ export let View = (function () { view.displayHpxGrid = false; view.displayCatalog = false; - 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; }; @@ -1065,15 +1174,16 @@ 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 // specified fpsInterval not being a multiple of RAF's interval (16.7ms) // Drawing code + try { this.moving = this.wasm.update(elapsedTime); } catch (e) { @@ -1089,66 +1199,64 @@ export let View = (function () { this.then = now; //this.then = now % View.FPS_INTERVAL; - requestAnimFrame(this.redraw.bind(this)); + requestAnimFrame(this.redrawClbk); //} }; View.prototype.drawAllOverlays = function () { - var catalogCtx = this.catalogCtx; - var catalogCanvasCleared = false; + var ctx = this.catalogCtx; + this.catalogCanvasCleared = false; if (this.mustClearCatalog) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; + ctx.clearRect(0, 0, this.width, this.height); + this.catalogCanvasCleared = true; this.mustClearCatalog = false; } if (this.catalogs && this.catalogs.length > 0 && this.displayCatalog && (!this.dragging || View.DRAW_SOURCES_WHILE_DRAGGING)) { // TODO : do not clear every time //// clear canvas //// - if (!catalogCanvasCleared) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; + if (!this.catalogCanvasCleared) { + ctx.clearRect(0, 0, this.width, this.height); + this.catalogCanvasCleared = true; } for (var i = 0; i < this.catalogs.length; i++) { var cat = this.catalogs[i]; - cat.draw(catalogCtx, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor); + cat.draw(ctx, this.cooFrame, this.width, this.height, this.largestDim); } } // draw popup catalog if (this.catalogForPopup.isShowing && this.catalogForPopup.sources.length > 0) { - if (!catalogCanvasCleared) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; + if (!this.catalogCanvasCleared) { + ctx.clearRect(0, 0, this.width, this.height); + this.catalogCanvasCleared = true; } - this.catalogForPopup.draw(catalogCtx, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor); + this.catalogForPopup.draw(ctx, this.cooFrame, this.width, this.height, this.largestDim); // draw popup overlay layer if (this.overlayForPopup.isShowing) { - this.overlayForPopup.draw(catalogCtx, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor); + this.overlayForPopup.draw(ctx, this.cooFrame, this.width, this.height, this.largestDim); } } ////// 3. Draw overlays//////// - var overlayCtx = this.catalogCtx; if (this.overlays && this.overlays.length > 0 && (!this.dragging || View.DRAW_SOURCES_WHILE_DRAGGING)) { - if (!catalogCanvasCleared) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; + if (!this.catalogCanvasCleared) { + ctx.clearRect(0, 0, this.width, this.height); + this.catalogCanvasCleared = true; } for (var i = 0; i < this.overlays.length; i++) { - this.overlays[i].draw(overlayCtx); + this.overlays[i].draw(ctx); } } // Redraw HEALPix grid - var healpixGridCtx = catalogCtx; if (this.displayHpxGrid) { - if (!catalogCanvasCleared) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; + if (!this.catalogCanvasCleared) { + ctx.clearRect(0, 0, this.width, this.height); + this.catalogCanvasCleared = true; } var cornersXYViewMapAllsky = this.getVisibleCells(3); @@ -1162,82 +1270,15 @@ export let View = (function () { } } if (cornersXYViewMapHighres && this.curNorder > 3) { - this.healpixGrid.redraw(healpixGridCtx, cornersXYViewMapHighres, this.fov, this.curNorder); + this.healpixGrid.redraw(ctx, cornersXYViewMapHighres, this.fov, this.curNorder); } else { - this.healpixGrid.redraw(healpixGridCtx, cornersXYViewMapAllsky, this.fov, 3); - } - } - - ////// 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) { - // VIEW mode, we do not want to display the reticle in this - // but draw a selection box - if (this.dragging) { - if (!catalogCanvasCleared) { - reticleCtx.clearRect(0, 0, this.width, this.height); - 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); - } - } else { - // Normal modes - if (this.displayReticle) { - if (!catalogCanvasCleared) { - catalogCtx.clearRect(0, 0, this.width, this.height); - catalogCanvasCleared = true; - } - - if (!this.reticleCache) { - // build reticle image - var c = document.createElement('canvas'); - var s = this.options.reticleSize; - c.width = s; - c.height = s; - var ctx = c.getContext('2d'); - ctx.lineWidth = 2; - ctx.strokeStyle = this.options.reticleColor; - ctx.beginPath(); - ctx.moveTo(s / 2, s / 2 + (s / 2 - 1)); - ctx.lineTo(s / 2, s / 2 + 2); - ctx.moveTo(s / 2, s / 2 - (s / 2 - 1)); - ctx.lineTo(s / 2, s / 2 - 2); - - ctx.moveTo(s / 2 + (s / 2 - 1), s / 2); - ctx.lineTo(s / 2 + 2, s / 2); - ctx.moveTo(s / 2 - (s / 2 - 1), s / 2); - ctx.lineTo(s / 2 - 2, s / 2); - - ctx.stroke(); - - this.reticleCache = c; - } - reticleCtx.drawImage(this.reticleCache, this.width / 2 - this.reticleCache.width / 2, this.height / 2 - this.reticleCache.height / 2); + this.healpixGrid.redraw(ctx, cornersXYViewMapAllsky, this.fov, 3); } } - ////// 5. Draw all-sky ring ///// - 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; - } - - reticleCtx.strokeStyle = this.aladin.options['allskyRingColor']; - var ringWidth = this.aladin.options['allskyRingWidth']; - reticleCtx.lineWidth = ringWidth; - reticleCtx.beginPath(); - const maxCxCy = this.cy; - const radius = (maxCxCy - (ringWidth / 2.0) + 1) / this.zoomFactor; - reticleCtx.arc(this.cx, this.cy, radius, 0, 2 * Math.PI, true); - reticleCtx.stroke(); + if (this.mode === View.SELECT) { + this.selector.dispatch('draw') } }; @@ -1264,14 +1305,83 @@ export let View = (function () { return pixList; }; - View.prototype.deselectObjects = function() { - if (this.selectedObjects) { - this.selectedObjects.forEach((objList) => { + View.prototype.unselectObjects = function() { + this.aladin.measurementTable.hide(); + + if (this.selection) { + this.selection.forEach((objList) => { objList.forEach((o) => o.deselect()) }); - this.aladin.measurementTable.hide(); - this.selectedObjects = null; + this.selection = null; + } + + // reattach the default contextmenu + if (this.aladin.contextMenu) { + this.aladin.contextMenu.attach(DefaultActionsForContextMenu.getDefaultActions(this.aladin)); + } + + this.requestRedraw(); + } + + View.prototype.selectObjects = function(selection) { + // unselect the previous selection + this.unselectObjects(); + + if (Array.isArray(selection)) { + this.selection = [selection]; + } else { + // select the new + this.selection = Selector.getObjects(selection, this); + } + + if (this.selection.length > 0) { + this.selection.forEach((objListPerCatalog) => { + objListPerCatalog.forEach((obj) => obj.select()) + }); + + let tables = this.selection.map((objList) => { + // Get the catalog containing that list of objects + let catalog = objList[0].getCatalog(); + + let source; + let sources = objList.map((o) => { + if (o instanceof Footprint) { + source = o.source; + } else { + source = o; + } + + return source; + }); + let table = { + 'name': catalog.name, + 'color': catalog.color, + 'rows': sources, + 'fields': catalog.fields, + 'showCallback': ObsCore.SHOW_CALLBACKS(this.aladin) + }; + + return table; + }) + + this.aladin.measurementTable.showMeasurement(tables); + let a = this.aladin; + const sampBtn = SAMPActionButton.sendSources(a); + + if (a.contextMenu) { + a.contextMenu.attach([ + { + label: Layout.horizontal([sampBtn, a.samp ? 'Send selection to SAMP' : 'SAMP disabled']), + }, + { + label: 'Remove selection', + action(o) { + a.view.unselectObjects(); + } + } + ]); + } } } @@ -1282,6 +1392,8 @@ export let View = (function () { // Called for touchmove events // initialAccDelta must be consistent with fovDegrees here View.prototype.setZoom = function (fovDegrees) { + fovDegrees = Math.min(fovDegrees, this.projection.fov); + this.wasm.setFieldOfView(fovDegrees); this.updateZoomState(); }; @@ -1313,8 +1425,8 @@ export let View = (function () { let new_fov = si / Math.pow(initialAccDelta, alpha); - if (new_fov >= this.fovLimit) { - new_fov = this.fovLimit; + if (new_fov >= this.projection.fov) { + new_fov = this.projection.fov; } this.pinchZoomParameters.initialAccDelta = initialAccDelta; @@ -1326,29 +1438,30 @@ 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(); let fov = this.wasm.getFieldOfView(); // Update the pinch zoom parameters consequently @@ -1360,31 +1473,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 }); }; /** @@ -1417,7 +1511,7 @@ export let View = (function () { return imageLayer; }; - View.prototype.addLayer = function(imageLayer) { + View.prototype._addLayer = function(imageLayer) { const layerName = imageLayer.layer; // Check whether this layer already exist const idxOverlayLayer = this.overlayLayers.findIndex(overlayLayer => overlayLayer == layerName); @@ -1425,13 +1519,9 @@ export let View = (function () { this.overlayLayers.push(layerName); } - if (this.options.log) { - Logger.log("setImageLayer", imageLayer.url); - } - // Find the toppest layer - const toppestLayer = this.overlayLayers[this.overlayLayers.length - 1]; - this.selectedLayer = toppestLayer; + //const toppestLayer = this.overlayLayers[this.overlayLayers.length - 1]; + //this.selectedLayer = toppestLayer; // Remove the existant layer if there is one let existantImageLayer = this.imageLayers.get(layerName); @@ -1451,6 +1541,10 @@ export let View = (function () { this.promises.push(imageLayerPromise); // All image layer promises must be completed (fullfilled or rejected) + const task = { + message: 'Load layer: ' + imageLayer.name, + id: Utils.uuidv4(), + } Promise.allSettled(this.promises) .then(() => imageLayerPromise) // The promise is resolved and we now have access @@ -1460,7 +1554,8 @@ export let View = (function () { const promise = imageLayer.add(layer); self.loadingState = true; - ALEvent.LOADING_STATE.dispatchedTo(this.aladinDiv, { loading: true }); + + ALEvent.FETCH.dispatchedTo(document, {task}); return promise; }) @@ -1469,10 +1564,10 @@ export let View = (function () { this.empty = false; if (imageLayer.children) { imageLayer.children.forEach((imageLayer) => { - this.addLayer(imageLayer); + this._addLayer(imageLayer); }) } else { - this.addLayer(imageLayer); + this._addLayer(imageLayer); } }) .catch((e) => { @@ -1481,7 +1576,7 @@ export let View = (function () { .finally(() => { // Loading state is over self.loadingState = false; - ALEvent.LOADING_STATE.dispatchedTo(this.aladinDiv, { loading: false }); + ALEvent.RESOURCE_FETCHED.dispatchedTo(document, {task}); self.imageLayersBeingQueried.delete(layer); @@ -1527,12 +1622,12 @@ export let View = (function () { this.imageLayers.set(newLayer, imageLayer); // Change the selected layer if this is the one renamed - if (this.selectedLayer === layer) { + /*if (this.selectedLayer === layer) { this.selectedLayer = newLayer; - } + }*/ // Tell the layer hierarchy has changed - ALEvent.HIPS_LAYER_RENAMED.dispatchedTo(this.aladinDiv, { layer: layer, newLayer: newLayer }); + ALEvent.HIPS_LAYER_RENAMED.dispatchedTo(this.aladinDiv, { layer, newLayer }); } View.prototype.swapLayers = function(firstLayer, secondLayer) { @@ -1581,16 +1676,13 @@ export let View = (function () { if (this.overlayLayers.length === 0) { this.empty = true; - this.selectedLayer = "base"; - } else { + } else if (this.selectedLayer === layer) { // find the toppest layer - if (this.selectedLayer === layer) { - const toppestLayer = this.overlayLayers[this.overlayLayers.length - 1]; - this.selectedLayer = toppestLayer; - } + //const toppestLayer = this.overlayLayers[this.overlayLayers.length - 1]; + this.selectedLayer = 'base'; } - ALEvent.HIPS_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: layer }); + ALEvent.HIPS_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer }); // check if there are no more surveys const noMoreLayersToWaitFor = this.promises.length === 0; @@ -1614,117 +1706,27 @@ export let View = (function () { let imageLayerQueried = this.imageLayersBeingQueried.get(layer); let imageLayer = this.imageLayers.get(layer); - return imageLayerQueried || imageLayer; + return imageLayer || imageLayerQueried; }; View.prototype.requestRedraw = function () { this.needRedraw = true; }; - View.prototype.setProjection = function (projectionName) { - 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) { - // 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; - 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; + View.prototype.setProjection = function (projName) { + if (!ProjectionEnum[projName]) { + console.warn(projName + " is not a valid projection.") + projName = 'SIN' } + + if (this.projection.id === ProjectionEnum[projName].id) { + return; + } + + this.projection = ProjectionEnum[projName]; + // Change the projection here - this.wasm.setProjection(projectionName); + this.wasm.setProjection(projName); this.updateZoomState(); this.requestRedraw(); @@ -1750,14 +1752,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; @@ -1783,16 +1790,6 @@ export let View = (function () { this.requestRedraw(); }; - View.prototype.showReticle = function (show) { - this.displayReticle = show; - - if (!this.displayReticle) { - this.mustClearCatalog = true; - } - - this.requestRedraw(); - }; - /** * * @API Point to a specific location in ICRS @@ -1802,8 +1799,7 @@ export let View = (function () { * @param options * */ - View.prototype.pointTo = function (ra, dec, options) { - options = options || {}; + View.prototype.pointTo = function (ra, dec) { ra = parseFloat(ra); dec = parseFloat(dec); @@ -1812,19 +1808,24 @@ 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(); + + // hide the popup if it is open + this.aladin.hidePopup(); }; + View.prototype.makeUniqLayerName = function (name) { if (!this.layerNameExists(name)) { return name; @@ -1876,6 +1877,7 @@ export let View = (function () { ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: layer }); + this.mustClearCatalog = true; this.requestRedraw(); }; @@ -1903,56 +1905,6 @@ export let View = (function () { moc.setView(this); }; - View.prototype.getObjectsInBBox = function (x, y, w, h) { - if (w < 0) { - x = x + w; - w = -w; - } - if (h < 0) { - y = y + h; - h = -h; - } - var objList = []; - var cat, sources, s; - var footprints, f; - var objListPerCatalog = []; - if (this.catalogs) { - for (var k = 0; k < this.catalogs.length; k++) { - cat = this.catalogs[k]; - if (!cat.isShowing) { - continue; - } - sources = cat.getSources(); - for (var l = 0; l < sources.length; l++) { - s = sources[l]; - if (!s.isShowing || !s.x || !s.y) { - continue; - } - if (s.x >= x && s.x <= x + w && s.y >= y && s.y <= y + h) { - objListPerCatalog.push(s); - } - } - // footprints - footprints = cat.getFootprints(); - if (footprints) { - for (var l = 0; l < footprints.length; l++) { - f = footprints[l]; - if (f.intersectsBBox(x, y, w, h, this)) { - objListPerCatalog.push(f); - } - } - } - - if (objListPerCatalog.length > 0) { - objList.push(objListPerCatalog); - } - objListPerCatalog = []; - } - } - return objList; - - }; - // update objLookup, lookup table View.prototype.updateObjectsLookup = function () { this.objLookup = []; @@ -1995,9 +1947,16 @@ export let View = (function () { footprints.forEach((footprint) => { // Hidden footprints are not considered + let lineWidth = footprint.getLineWidth(); + + footprint.setLineWidth(10.0); if (footprint.isShowing && footprint.isInStroke(ctx, this, x, y)) { closest = footprint; - return; + } + footprint.setLineWidth(lineWidth); + + if (closest) { + return closest; } }) @@ -2011,8 +1970,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++) { @@ -2020,7 +1978,7 @@ export let View = (function () { let closest = this.closestFootprints(overlay.overlayItems, ctx, x, y); if (closest) { - ctx.lineWidth = pastLineWidth; + //ctx.lineWidth = pastLineWidth; return [closest]; } } @@ -2033,18 +1991,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++) { diff --git a/src/js/events/ALEvent.js b/src/js/events/ALEvent.js index f52b19cc6..d38713eb8 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"); @@ -55,7 +60,18 @@ export class ALEvent { static GRAPHIC_OVERLAY_LAYER_CHANGED = new ALEvent("AL:GraphicOverlayLayer.changed"); - static SAMP_AVAILABILITY = new ALEvent("AL:samp.started"); + static SAMP_HUB_RUNNING = new ALEvent("AL:samp.hub"); + static SAMP_CONNECTED = new ALEvent("AL:samp.connected"); + static SAMP_DISCONNECTED = new ALEvent("AL:samp.disconnected"); + + static CANVAS_EVENT = new ALEvent("AL:Event"); + + static RETICLE_CHANGED = new ALEvent("AL:Reticle.changed") + + static RESOURCE_FETCHED = new ALEvent("AL:Resource.fetched") + static FETCH = new ALEvent("AL:fetch") + + static MODE = new ALEvent("AL:mode") constructor(name) { this.name = name; diff --git a/src/js/gui/Box/CatalogQueryBox.js b/src/js/gui/Box/CatalogQueryBox.js new file mode 100644 index 000000000..e3a62a146 --- /dev/null +++ b/src/js/gui/Box/CatalogQueryBox.js @@ -0,0 +1,310 @@ +// 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 { MocServer } from "../../MocServer.js"; + +import { Box } from "../Widgets/Box.js"; +import { Layout } from "../Layout.js"; +import { Input } from "../Widgets/Input.js"; +import A from "../../A.js"; +import { ConeSearchBox } from "./ConeSearchBox.js"; +import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; +/****************************************************************************** + * Aladin Lite project + * + * File gui/HiPSSelector.js + * + * + * Author: Thomas Boch, Matthieu Baumann[CDS] + * + *****************************************************************************/ + + export class CatalogQueryBox extends Box { + static catalogs = {}; + constructor(aladin) { + // Query the mocserver + MocServer.getAllCatalogHiPSes() + .then((catalogs) => { + catalogs.forEach((cat) => { + CatalogQueryBox.catalogs[cat.obs_title] = cat; + }); + + inputText.update({autocomplete: {options: Object.keys(CatalogQueryBox.catalogs)}}) + }) + + const fnIdSelected = function(type, params) { + if (type=='coneSearch') { + let errorCallback = (e) => { + alert(e + '.\nThe table might contain no data for the cone search specified.'); + } + if (params.baseURL.includes('/vizier.')) { + A.catalogFromVizieR( + params.id.replace('CDS/', ''), + params.ra + ' ' + params.dec, + params.radiusDeg, + {limit: params.limit, onClick: 'showTable'}, + (catalog) => { + aladin.addCatalog(catalog) + }, + errorCallback + ); + } + else if (params.baseURL.includes('/simbad.')) { + A.catalogFromSimbad( + params.ra + ' ' + params.dec, + params.radiusDeg, + {limit: params.limit, onClick: 'showTable'}, + (catalog) => { + aladin.addCatalog(catalog) + }, + errorCallback + ); + } + else { + console.log('cone search', params.baseURL) + let url = params.baseURL; + if (! url.endsWith('?')) { + url += '?'; + } + url += 'RA=' + params.ra + '&DEC=' + params.dec + '&SR=' + params.radiusDeg; + A.catalogFromURL( + url, + {limit: params.limit, onClick: 'showTable'}, + (catalog) => { + aladin.addCatalog(catalog) + }, + errorCallback + ); + } + } + else if (type=='hips') { + const hips = A.catalogHiPS(params.hipsURL, {onClick: 'showTable', name: params.id}); + aladin.addCatalog(hips); + } + }; + + let inputText = Input.text({ + //tooltip: {content: 'Search for a VizieR catalogue', position: {direction :'bottom'}}, + name: 'catalogs', + placeholder: "Type ID, title, keyword or URL", + actions: { + change() { + const catalog = CatalogQueryBox.catalogs[this.value]; + inputText.set(catalog.ID); + loadBtn.update({disable: false}); + + self._selectItem(catalog, aladin); + }, + keydown() { + loadBtn.update({disable: true}); + } + } + + /*change(e) { + self._selectItem(undefined, aladin) + //resetCatalogueSelection(); + // Unfocus the keyboard on android devices (maybe it concerns all smartphones) when the user click on enter + //input.element().blur(); + }*/ + }); + let self; + + let loadBtn = new CtxMenuActionButtonOpener({ + openDirection: "left", + content: 'Load', + disable: true, + }, aladin) + + super(aladin, { + content: Layout.horizontal({ + layout: [inputText, loadBtn] + }) + }) + + self = this; + this.loadBtn = loadBtn; + this.inputText = inputText; + this.fnIdSelected = fnIdSelected; + + /*autocomplete({ + input: catNameTextInput.element(), + minLength: 3, + fetch: function(text, update) { + text = text.toLowerCase(); + + const filterCats = function(item) { + const ID = item.ID; + const obsTitle = item.obs_title || ''; + const obsDescription = item.obs_description || ''; + + return ID.toLowerCase().includes(text) || obsTitle.toLowerCase().includes(text) || obsDescription.toLowerCase().includes(text); + } + + // filter suggestions + const suggestions = MocServer.getAllCatalogHiPSes().filter(filterCats); + // sort suggestions + suggestions.sort( function(a , b) { + let scoreForA = 0; + let scoreForB = 0; + + if (a.ID.toLowerCase().includes(text)) { + scoreForA += 100; + } + if (b.ID.toLowerCase().includes(text)) { + scoreForB += 100; + } + + if (a.obs_title.toLowerCase().includes(text)) { + scoreForA += 50; + } + if (b.obs_title.toLowerCase().includes(text)) { + scoreForB += 50; + } + + if (a.obs_description.toLowerCase().includes(text)) { + scoreForA += 10; + } + if (b.obs_description.toLowerCase().includes(text)) { + scoreForB += 10; + } + + // HiPS catalogue available + if (a.hips_service_url) { + scoreForA += 20; + } + if (b.hips_service_url) { + scoreForB += 20; + } + + if (scoreForA > scoreForB) { + return -1; + } + if (scoreForB > scoreForA) { + return 1; + } + + return 0; + }); + + // limit to 50 first suggestions + const returnedSuggestions = suggestions.slice(0, 50); + update(returnedSuggestions); + }, + onSelect: function(item) { + catNameTextInput.set(item.ID); + self._selectItem(item, aladin); + + // enable the load button + //loadBtn.update({disable: false}); + + catNameTextInput.element().blur(); + }, + // attach container to AL div if needed (to prevent it from being hidden in full screen mode) + customize: function(input, inputRect, container, maxHeight) { + // this tests if we are in full screen mode + if (aladin.isInFullscreen) { + aladin.aladinDiv.appendChild(container); + } + }, + render: function(item, currentValue) { + const itemElement = document.createElement("div"); + itemElement.innerHTML = (item.obs_title || '') + ' - ' + '' + item.ID + ''; + + return itemElement; + }, + });*/ + } + + _selectItem(item, aladin) { + this.selectedItem = item; + + if (!item) { + this.loadBtn.update({disable: true}, aladin) + } else { + let self = this; + let layout = []; + + if (item && item.cs_service_url) { + layout.push({ + label: 'Cone search', + disable: !item.cs_service_url, + action(o) { + let box = ConeSearchBox.getInstance(aladin); + box.attach({ + callback: (cs) => { + self.fnIdSelected('coneSearch', { + baseURL: self.selectedItem.cs_service_url, + id: self.selectedItem.ID, + ra: cs.ra, + dec: cs.dec, + radiusDeg: cs.rad, + limit: cs.limit + }) + + self._hide(); + }, + position: { + anchor: 'center center', + } + }) + box._show(); + self.loadBtn.hideMenu() + + } + }) + } + + if (item && item.hips_service_url) { + layout.push({ + label: 'HiPS catalogue', + disable: !item.hips_service_url, + action(o) { + self.fnIdSelected('hips', { + hipsURL: item.hips_service_url, + id: item.ID, + }) + + self._hide(); + } + }) + } + this.loadBtn.update({ctxMenu: layout, disable: false}, aladin) + } + + this.loadBtn.hideMenu() + } + + _hide() { + if (this.loadBtn) { + this.loadBtn.hideMenu() + } + + super._hide() + } + + static layerSelector = undefined; + + static getInstance(aladin) { + if (!CatalogQueryBox.layerSelector) { + CatalogQueryBox.layerSelector = new CatalogQueryBox(aladin); + } + + return CatalogQueryBox.layerSelector; + } +} diff --git a/src/js/gui/Box/ConeSearchBox.js b/src/js/gui/Box/ConeSearchBox.js new file mode 100644 index 000000000..446ccd72c --- /dev/null +++ b/src/js/gui/Box/ConeSearchBox.js @@ -0,0 +1,184 @@ +// 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 { Coo } from "../../libs/astro/coo.js"; +import { Box } from "../Widgets/Box.js"; +import { Layout } from "../Layout.js"; +import { ConeSearchActionButton } from "../Button/ConeSearch.js"; +import { Form } from "../Widgets/Form.js"; +import { Angle } from "../../libs/astro/angle.js"; +/****************************************************************************** + * Aladin Lite project + * + * File gui/HiPSSelector.js + * + * + * Author: Thomas Boch, Matthieu Baumann[CDS] + * + *****************************************************************************/ + + export class ConeSearchBox extends Box { + constructor(aladin) { + let self; + let selectorBtn = new ConeSearchActionButton({ + tooltip: {content: 'Select the area to query the catalogue with', position: {direction: 'left'}}, + onBeforeClick(e) { + self._hide(); + }, + action(circle) { + // convert to ra, dec and radius in deg + try { + let [ra, dec] = aladin.pix2world(circle.x, circle.y); + let radius = aladin.angularDist(circle.x, circle.y, circle.x + circle.r, circle.y); + + //var hlon = this.lon/15.0; + //var strlon = Numbers.toSexagesimal(hlon, this.prec+1, false); + let coo = new Coo(ra, dec, 7); + let [lon, lat] = coo.format('s2'); + + let fov = new Angle(radius, 1).format(); + //selectorBtn.update({tooltip: {content: 'center: ' + ra.toFixed(2) + ', ' + dec.toFixed(2) + 'radius: ' + radius.toFixed(2), position: {direction: 'left'}}}) + form.set('ra', lon) + form.set('dec', lat) + form.set('rad', fov) + } catch (e) { + alert(e, 'Cone search out of projection') + } + + self._show() + } + }, aladin) + + let [ra, dec] = aladin.getRaDec(); + let centerCoo = new Coo(ra, dec, 5); + let [defaultRa, defaultDec] = centerCoo.format('s2'); + + let fov = aladin.getFov(); + let fovAngle = new Angle(Math.min(fov[0], fov[1]) / 2, 1).format() + + let form = new Form({ + submit(values) { + self._hide(); + let coo = new Coo(); + coo.parse(values.ra + ' ' + values.dec) + + let theta = new Angle(); + theta.parse(values.rad) + self.callback && self.callback({ + ra: coo.lon, + dec: coo.lat, + rad: theta.degrees(), + limit: values.limit, + }) + }, + subInputs: [ + { + type: 'group', + header: Layout.horizontal([selectorBtn, 'Cone search']), + subInputs: [ + { + label: "ra:", + name: "ra", + type: "text", + value: defaultRa, + placeholder: 'Right ascension', + actions: { + change(e, input) { + input.addEventListener('blur', (event) => {}); + }, + } + }, + { + label: "dec:", + name: "dec", + type: "text", + value: defaultDec, + placeholder: 'Declination', + actions: { + change(e, input) { + input.addEventListener('blur', (event) => {}); + }, + } + }, + { + label: "Rad:", + name: "rad", + type: 'text', + value: fovAngle, + placeholder: 'Radius', + actions: { + change(e, input) { + input.addEventListener('blur', (event) => {}); + }, + } + } + ] + }, + { + type: 'group', + header: 'Max number of sources', + subInputs: [{ + label: "Limit:", + name: "limit", + step: '1', + value: 1000, + type: "number", + placeholder: 'Limit number of sources', + actions: { + change(e, input) { + input.addEventListener('blur', (event) => {}); + }, + } + }] + }, + ] + }); + + super( + aladin, + { + header: { + draggable: true, + title: 'Cone Search box' + }, + content: form + } + ) + + // hide by default + //console.log("hide cone search") + this._hide(); + + self = this; + } + + attach(options) { + this.callback = options.callback; + super.update(options) + } + + static box = undefined; + + static getInstance(aladin) { + if (!ConeSearchBox.box) { + ConeSearchBox.box = new ConeSearchBox(aladin); + } + + return ConeSearchBox.box; + } +} diff --git a/src/js/gui/Box/GotoBox.js b/src/js/gui/Box/GotoBox.js new file mode 100644 index 000000000..105da4385 --- /dev/null +++ b/src/js/gui/Box/GotoBox.js @@ -0,0 +1,82 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { Box } from "../Widgets/Box.js"; +import { Input } from "../Widgets/Input.js"; +import { Layout } from "../Layout.js"; +import { SearchTextInput } from "../Input/InputTextSearch.js"; + +export class GotoBox extends Box { + // Constructor + constructor(aladin) { + /*let content = Layout.horizontal([ + 'Go to:', + Input.text({ + //tooltip: {content: 'Search for a VizieR catalogue', position: {direction :'bottom'}}, + label: "Go to:", + name: "goto", + type: "text", + placeholder: 'Object name/position', + autocomplete: 'off', + change(e, self) { + self.addEventListener('blur', (event) => {}); + } + }) + ]);*/ + let textField = new SearchTextInput(aladin, { + cssStyle: { + width: '15rem' + } + }); + + super(aladin, {content: textField, cssStyle: {backgroundColor: 'transparent', padding: '0 0 0 0.2rem'}}) + + this.addClass('aladin-box-night'); + this.textField = textField; + } + + _hide() { + if (this.textField) { + this.textField.set('') + } + + super._hide() + } + + static singleton; + + static getInstance(aladin) { + if (!GotoBox.singleton) { + GotoBox.singleton = new GotoBox(aladin); + } + + return GotoBox.singleton; + } +} diff --git a/src/js/gui/Box/GridBox.js b/src/js/gui/Box/GridBox.js new file mode 100644 index 000000000..f7ed32878 --- /dev/null +++ b/src/js/gui/Box/GridBox.js @@ -0,0 +1,205 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { Box } from "../Widgets/Box.js"; +import { Input } from "../Widgets/Input.js"; +import { Layout } from "../Layout.js"; +import { ALEvent } from "../../events/ALEvent.js"; +import { Color } from "../../Color.js"; +import { ContextMenu } from "../Widgets/ContextMenu.js"; +import { ActionButton } from "../Widgets/ActionButton.js"; +import thicknessLineIcon from './../../../../assets/icons/thickness.svg'; +import labelSizeIcon from './../../../../assets/icons/font-size.svg'; + +export class GridBox extends Box { + // Constructor + constructor(aladin) { + let colorInput = new Input({ + layout: { + name: 'gridColor', + type: 'color', + value: (() => { + let c = aladin.getGridOptions().color; + const cHex = Color.rgbToHex(c.r * 255, c.g * 255, c.b * 255) + return cHex; + })(), + change(e) { + aladin.setCooGrid({color: e.target.value}) + } + } + }); + colorInput.addClass("aladin-input-color"); + + let sliderOpacity = new Input({ + layout: { + name: 'opacitySlider', + type: 'range', + min: 0.0, + max: 1.0, + value: aladin.getGridOptions().opacity, + change(e) { + aladin.setCooGrid({opacity: +e.target.value}) + } + } + }); + sliderOpacity.addClass("aladin-input-range") + + const labelSizeBtn = new ActionButton({ + iconURL: labelSizeIcon, + tooltip: {content: 'Change the label size', position: {direction: 'left'}}, + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + cursor: 'pointer', + width: '20px', + height: '20px', + padding: '0', + }, + action(e) { + let ctxMenu = ContextMenu.getInstance(aladin); + ctxMenu._hide(); + + let ctxMenuLayout = []; + const fontSize = 5; // 10px + for (let em = 1; em <= 5; em++) { + let pxSize = fontSize * em; + ctxMenuLayout.push({ + label: em + 'em', + action(o) { + aladin.setCooGrid({labelSize: pxSize}) + } + }) + } + + ctxMenu.attach(ctxMenuLayout); + ctxMenu.show({ + e: e, + position: { + nextTo: labelSizeBtn, + direction: 'bottom', + } + }) + } + }); + + const thicknessLineBtn = new ActionButton({ + iconURL: thicknessLineIcon, + tooltip: {content: 'Grid line thickness', position: {direction: 'left'}}, + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + cursor: 'pointer', + width: '20px', + height: '20px', + padding: '0', + }, + action(e) { + let ctxMenu = ContextMenu.getInstance(aladin); + ctxMenu._hide(); + + let ctxMenuLayout = []; + for (let thickness = 1; thickness <= 5; thickness++) { + ctxMenuLayout.push({ + label: thickness + 'px', + action(o) { + aladin.setCooGrid({thickness: thickness}) + } + }) + } + + ctxMenu.attach(ctxMenuLayout); + ctxMenu.show({ + e: e, + position: { + nextTo: thicknessLineBtn, + direction: 'bottom', + } + }) + } + }); + let enableCheckbox = Input.checkbox({ + name: 'enableGrid', + tooltip: {content: 'Enable/disable the grid', position: {direction: 'left'}}, + type: 'checkbox', + checked: aladin.getGridOptions().enabled, + click(e) { + aladin.setCooGrid({enabled: enableCheckbox.get()}) + } + }); + sliderOpacity.addClass("aladin-input-range") + const layout = Layout.horizontal({ + layout: [ + enableCheckbox, + labelSizeBtn, + thicknessLineBtn, + colorInput, + sliderOpacity + ] + }) + + layout.addClass('aladin-grid-frame'); + + ALEvent.COO_GRID_UPDATED.listenedBy(aladin.aladinDiv, function (e) { + let color = e.detail.color; + + let hexColor = Color.rgbToHex(Math.round(255 * color.r), Math.round(255 * color.g), Math.round(255 * color.b)); + colorInput.set(hexColor) + }); + + super(aladin, + { + content: layout, + } + ) + + this.addClass("aladin-box-night") + + this.aladin = aladin; + + this._hide(); + } + + _hide() { + super._hide() + + let ctxMenu = ContextMenu.getInstance(this.aladin); + ctxMenu._hide(); + } + + static singleton; + + static getInstance(aladin) { + if (!GridBox.singleton) { + GridBox.singleton = new GridBox(aladin); + } + + return GridBox.singleton; + } +} diff --git a/src/js/gui/Box/HiPSSelectorBox.js b/src/js/gui/Box/HiPSSelectorBox.js new file mode 100644 index 000000000..52f80cdce --- /dev/null +++ b/src/js/gui/Box/HiPSSelectorBox.js @@ -0,0 +1,178 @@ +// 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 { MocServer } from "../../MocServer.js"; + +import { Box } from "../Widgets/Box.js"; +import { Layout } from "../Layout.js"; +import { ActionButton } from "../Widgets/ActionButton.js"; +import { Input } from "../Widgets/Input.js"; +/****************************************************************************** + * Aladin Lite project + * + * File gui/HiPSSelector.js + * + * + * Author: Thomas Boch, Matthieu Baumann[CDS] + * + *****************************************************************************/ + + export class HiPSSelectorBox extends Box { + static HiPSList = {}; + + constructor(aladin) { + MocServer.getAllHiPSes() + .then((HiPSes) => { + HiPSes.forEach((h) => { + HiPSSelectorBox.HiPSList[h.obs_title] = h + }); + + inputText.update({autocomplete: {options: Object.keys(HiPSSelectorBox.HiPSList)}}) + }); + + let self; + let loadBtn = new ActionButton({ + content: 'Add', + disable: true, + action(e) { + self.callback && self.callback(inputText.get()); + // reset the field + inputText.set(''); + + self._hide(); + } + }) + + let inputText = Input.text({ + classList: ['search'], + name: 'survey', + placeholder: "Type survey keywords", + actions: { + change() { + const HiPS = HiPSSelectorBox.HiPSList[this.value]; + inputText.set(HiPS.ID); + loadBtn.update({disable: false}); + }, + keydown() { + loadBtn.update({disable: true}); + } + } + }); + + super( + aladin, + { + content: Layout.horizontal({ + layout: [ + inputText, + loadBtn + ] + }) + } + ) + + self = this; + // Query the mocserver + /*MocServer.getAllHiPSes(); + + autocomplete({ + input: inputText.element(), + fetch: function(text, update) { + text = text.toLowerCase(); + // filter suggestions + const suggestions = MocServer.getAllHiPSes().filter(n => n.ID.toLowerCase().includes(text) || n.obs_title.toLowerCase().includes(text)) + + // sort suggestions + suggestions.sort( function(a , b) { + let scoreForA = 0; + let scoreForB = 0; + + if (a.ID.toLowerCase().includes(text)) { + scoreForA += 100; + } + if (b.ID.toLowerCase().includes(text)) { + scoreForB += 100; + } + + if (a.obs_title.toLowerCase().includes(text)) { + scoreForA += 50; + } + if (b.obs_title.toLowerCase().includes(text)) { + scoreForB += 50; + } + + if (a.obs_description && a.obs_description.toLowerCase().includes(text)) { + scoreForA += 10; + } + if (b.obs_description && b.obs_description.toLowerCase().includes(text)) { + scoreForB += 10; + } + + if (scoreForA > scoreForB) { + return -1; + } + if (scoreForB > scoreForA) { + return 1; + } + + return 0; + }); + + // limit to 50 first suggestions + const returnedSuggestions = suggestions.slice(0, 50); + + update(returnedSuggestions); + }, + onSelect: function(item) { + inputText.set(item.ID); + loadBtn.update({disable: false}); + + inputText.element().blur(); + }, + // attach container to AL div if needed (to prevent it from being hidden in full screen mode) + customize: function(input, inputRect, container, maxHeight) { + // this tests if we are in full screen mode + if (aladin.isInFullscreen) { + aladin.aladinDiv.appendChild(container); + } + }, + render: function(item, currentValue) { + const itemElement = document.createElement("div"); + itemElement.innerHTML = item.obs_title + ' - ' + '' + item.ID + ''; + + + return itemElement; + } + });*/ + } + + attach(callback) { + this.callback = callback; + } + + static box = undefined; + + static getInstance(aladin) { + if (!HiPSSelectorBox.box) { + HiPSSelectorBox.box = new HiPSSelectorBox(aladin); + } + + return HiPSSelectorBox.box; + } +} diff --git a/src/js/gui/Box/ServiceQueryBox.js b/src/js/gui/Box/ServiceQueryBox.js new file mode 100644 index 000000000..b2c439285 --- /dev/null +++ b/src/js/gui/Box/ServiceQueryBox.js @@ -0,0 +1,195 @@ +// 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. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/SODAQueryWindow.js + * + * A form window aiming to query a SODA service cutout + * + * + * Author: Matthieu Baumann [CDS] + * + *****************************************************************************/ + +import { Utils } from '../../Utils'; +import { ActionButton } from '../Widgets/ActionButton.js'; +import { Form } from '../Widgets/Form.js'; +import { Layout } from '../Layout.js'; +import { Box } from '../Widgets/Box'; +import { ConeSearchActionButton } from '../Button/ConeSearch'; +import { Coo } from '../../libs/astro/coo'; +import { Angle } from '../../libs/astro/angle'; + +export class ServiceQueryBox extends Box { + constructor(aladin) { + let self; + // Define the form once for all + let form = new Form({ + submit(params) { + // Construct the SODA url + let url = new URL(self.service.baseUrl) + + if (params['ra'] && params['dec'] && params['rad']) { + url.searchParams.append('CIRCLE', params['ra'] + ' ' + params['dec'] + ' ' + params['rad']); + } + + if (params['ramin'] && params['ramax'] && params['decmin'] && params['decmax']) { + url.searchParams.append('RANGE', params['ramin'] + ' ' + params['ramax'] + ' ' + params['decmin'] + ' ' + params['decmax']); + } + + if (params['fmin'] && params['fmax']) { + url.searchParams.append('BAND', params['fmin'] + ' ' + params['fmax']); + } + + if (params['ID']) { + url.searchParams.append('ID', params['ID']); + } + + /*let loadingBtn = ActionButton.create( + ActionButton.DEFAULT_BTN["loading"], + 'Waiting to get the image response...', + submitFormDiv + );*/ + + let name = url.searchParams.toString(); + // Tackle CORS problems + Utils.loadFromUrls([url, Utils.handleCORSNotSameOrigin(url)], {timeout: 30000, dataType: 'blob'}) + .then((blob) => { + const url = URL.createObjectURL(blob); + try { + let image = self.aladin.createImageFITS(url, name); + self.aladin.setOverlayImageLayer(image, Utils.uuidv4()) + } catch(e) { + throw('Fail to interpret ' + url + ' as a fits file') + } + }) + .catch((e) => { + window.alert(e) + }) + .finally(() => { + // set cursor back to the normal mode + //loadingBtn.remove(); + }) + }, + subInputs: [] + }); + + super( + aladin, + { + header: { + draggable: true, + title: 'Service query' + }, + content: form + } + ) + + this.form = form; + this.aladin = aladin; + + self = this; + } + + attach(service) { + this.service = service; + let self = this; + + let subInputs = [] + for (const param in this.service.inputParams) { + + let subInput = this.service.inputParams[param]; + + let header; + switch (param) { + case 'ID': + const listOfInputParams = Object.keys(this.service["inputParams"]).map((name) => name).join(', '); + + header = Layout.horizontal(['ID', new ActionButton({ + size: 'small', + content: '📡', + tooltip: { + content: 'This is the form to request the SODA server located at:
' + + '' + this.service["baseUrl"] + '
' + + 'The list of input params is:
' + listOfInputParams + }, + })]); + break; + case 'Circle': + let csBtn = new ConeSearchActionButton({ + tooltip: {content: 'Cone selection', position: {direction: 'left'}}, + onBeforeClick(e) { + self._hide(); + }, + action(c) { + // convert to ra, dec and radius in deg + try { + let [ra, dec] = self.aladin.pix2world(c.x, c.y); + let radius = self.aladin.angularDist(c.x, c.y, c.x + c.r, c.y); + + //var hlon = this.lon/15.0; + //var strlon = Numbers.toSexagesimal(hlon, this.prec+1, false); + let coo = new Coo(ra, dec, 7); + let [lon, lat] = coo.format('d2'); + + let fov = new Angle(radius, 1).degrees(); + //selectorBtn.update({tooltip: {content: 'center: ' + ra.toFixed(2) + ', ' + dec.toFixed(2) + 'radius: ' + radius.toFixed(2), position: {direction: 'left'}}}) + self.form.set('ra', +lon) + self.form.set('dec', +lat) + self.form.set('rad', fov) + + console.log(lon, lat, fov) + } catch (e) { + alert(e, 'Cone search out of projection') + } + + self._show() + } + }, self.aladin) + + header = Layout.horizontal(['Circle', csBtn]); + break; + default: + header = param; + break; + } + + subInput.header = header; + subInputs.push(subInput) + } + + this.form.update({ + subInputs, + }) + } + + static singleton; + + static getInstance(aladin) { + if (!ServiceQueryBox.singleton) { + ServiceQueryBox.singleton = new ServiceQueryBox(aladin); + } + + return ServiceQueryBox.singleton; + } + +} \ No newline at end of file diff --git a/src/js/gui/Box/ShortLivedBox.js b/src/js/gui/Box/ShortLivedBox.js new file mode 100644 index 000000000..d7d14946c --- /dev/null +++ b/src/js/gui/Box/ShortLivedBox.js @@ -0,0 +1,75 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { Box } from "../Widgets/Box.js"; + +export class ShortLivedBox extends Box { + // Constructor + constructor(aladin) { + super( + aladin, + { + cssStyle: { + color: 'white', + backgroundColor: 'black', + borderRadius: '3px', + padding: 0, + } + } + ) + } + + _show(options) { + let duration = options.duration || 1000; + + // clear the past timeout + if (this.idxTimeout) { + clearTimeout(this.idxTimeout) + } + + if (duration !== 'unlimited') { + this.idxTimeout = setTimeout(() => { + this._hide() + }, duration) + } + + super._show(options); + } + + static singleton; + + static getInstance(aladin) { + if (!ShortLivedBox.singleton) { + ShortLivedBox.singleton = new ShortLivedBox(aladin); + } + + return ShortLivedBox.singleton; + } +} diff --git a/src/js/gui/Box/StatusBarBox.js b/src/js/gui/Box/StatusBarBox.js new file mode 100644 index 000000000..05aba64f8 --- /dev/null +++ b/src/js/gui/Box/StatusBarBox.js @@ -0,0 +1,180 @@ + +// 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 Sesame.js + * + * Author: Thomas Boch[CDS] + * + *****************************************************************************/ +import { Box } from "../Widgets/Box"; +import { ALEvent } from "../../events/ALEvent"; +import { Utils } from "../../Utils"; +import { Layout } from "../Layout"; +import infoIconUrl from '../../../../assets/icons/info.svg'; +import tooltipIconUrl from '../../../../assets/icons/tooltip.svg'; +import { Icon } from "../Widgets/Icon"; + +export class StatusBarBox extends Box { + constructor(aladin, options) { + options.cssStyle = { + color: 'white', + backgroundColor: 'black', + borderRadius: '3px', + padding: 0, + ...options.cssStyle + }; + + super(aladin, { + position: { + anchor: 'center bottom' + }, + ...options, + }) + + this.addClass("aladin-status-bar"); + + this.inProgressTasks = []; + + this._addListeners() + } + + _addListeners() { + ALEvent.FETCH.listenedBy(document, (e) => { + let task = e.detail.task; + this.appendMessage(task); + }); + + ALEvent.RESOURCE_FETCHED.listenedBy(document, (e) => { + let task = e.detail.task; + this.removeMessage(task.id); + }); + } + + appendMessage(task) { + task.id = task.id || Utils.uuidv4(); + task.type = task.type || 'loading'; + + this.inProgressTasks.push(task); + + if (task.duration && task.duration !== "unlimited") { + setTimeout(() => { + this.removeMessage(task.id); + }, task.duration) + } + + // display it + this._displayLastTaskInProgress(); + }; + + removeMessage(id) { + const index = this.inProgressTasks.findIndex((t) => t.id === id); + if (index >= 0) { + // task found + this.inProgressTasks.splice(index, 1); + + // If it was the last element, i.e. the one being displayed + if (index === this.inProgressTasks.length) { + // display the "new" last + this._displayLastTaskInProgress(); + } + } + }; + + _displayLastTaskInProgress() { + this._hide(); + + if (this.inProgressTasks.length === 0) { + // no more task to run + return; + } + + let task = this.inProgressTasks[this.inProgressTasks.length - 1]; + + this.el.title = task.message; + + // create message div + let message = Layout.horizontal({ + layout: task.message, + tooltip: { + content: task.message, + position: { + direction: "top", + }, + cssStyle: { + border: "1px solid white", + fontSize: 'xx-small', + maxWidth: "200px", + "overflow-wrap": "break-word", + "pointer-events": "auto", + } + }, + }); + + message.addClass("aladin-status-bar-message") + + this._show({ + content: new Layout({layout: [StatusBarBox.icons[task.type], message], orientation: 'horizontal'}), + }) + } + + static icons = { + loading: (() => { + let icon = new Icon({ + size: 'medium', + url: "https://raw.githubusercontent.com/cds-astro/aladin-lite/master/assets/aladin-logo.gif", + cssStyle: { + cursor: "help", + }, + tooltip: { + content: "Loading...", + position: { + direction: 'top' + } + }, + }) + + icon.addClass("rotating") + + return icon + })(), + info: new Icon({ + size: 'medium', + monochrome: true, + url: infoIconUrl, + cssStyle: { + cursor: "help", + }, + }), + tooltip: new Icon({ + size: 'medium', + monochrome: true, + url: tooltipIconUrl, + cssStyle: { + cursor: "help", + }, + }) + } +} + \ No newline at end of file diff --git a/src/js/gui/Box/SurveyEditBox.js b/src/js/gui/Box/SurveyEditBox.js new file mode 100644 index 000000000..a7c264e9f --- /dev/null +++ b/src/js/gui/Box/SurveyEditBox.js @@ -0,0 +1,344 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ColorCfg } from "../../ColorCfg.js"; + import { Box } from "../Widgets/Box.js"; + import { ALEvent } from "../../events/ALEvent.js"; + import opacityIconUrl from '../../../../assets/icons/opacity.svg'; + import luminosityIconUrl from '../../../../assets/icons/brightness.svg'; + import colorIconUrl from '../../../../assets/icons/color.svg'; + import pixelHistIconUrl from '../../../../assets/icons/pixel_histogram.svg'; + import { SelectorButton } from "../Widgets/Selector"; + + import { Layout } from "../Layout.js"; + import { Input } from "../Widgets/Input.js"; +import { CmapSelector } from "../Selector/Colormap.js"; + + export class LayerEditBox extends Box { + // Constructor + constructor(aladin, options) { + super( + aladin, + { + cssStyle: { + padding: '4px', + backgroundColor: 'black', + } + } + ) + + this.aladin = aladin; + + let self = this; + this.selector = new SelectorButton({ + luminosity: { + icon: { + size: 'small', + monochrome: true, + url: luminosityIconUrl + }, + tooltip: {content: 'Luminosity sliders', position: {direction: 'right'}}, + change(e) { + const content = Layout.horizontal({ + layout: [self.selector, self.luminositySettingsContent] + }); + self.update({content}) + } + }, + opacity: { + icon: { + size: 'small', + monochrome: true, + url: opacityIconUrl + }, + tooltip: {content: 'Opacity slider', position: {direction: 'right'}}, + change(e) { + const content = Layout.horizontal({layout: [self.selector, self.opacitySettingsContent]}); + self.update({content}) + } + }, + colors: { + icon: { + size: 'small', + url: colorIconUrl + }, + tooltip: {content: 'Colormap', position: {direction: 'right'}}, + change(e) { + const content = Layout.horizontal({layout: [self.selector, self.colorSettingsContent]}); + self.update({content}) + } + }, + pixel: { + icon: { + size: 'small', + monochrome: true, + url: pixelHistIconUrl + }, + tooltip: {content: 'Pixel cutouts', position: {direction: 'right'}}, + change(e) { + const content = Layout.horizontal({layout: [self.selector, self.pixelSettingsContent]}); + self.update({content}) + } + }, + selected: 'opacity' + }, aladin); + + + + // content + this.minCutInput = Input.number({ + cssStyle: { + padding: '0', + width: '8ex', + 'font-family': 'monospace', + }, + tooltip: {content: 'Min cut', position: {direction: 'bottom'}}, + name: 'mincut', + change(e) { + let layer = self.options.layer; + layer.setCuts(+e.target.value, layer.getColorCfg().getCuts()[1]) + } + }) + + this.maxCutInput = Input.number({ + cssStyle: { + padding: '0', + width: '8ex', + 'font-family': 'monospace', + }, + tooltip: {content: 'Max cut', position: {direction: 'bottom'}}, + name: 'maxcut', + change(e) { + let layer = self.options.layer; + layer.setCuts(layer.getColorCfg().getCuts()[0], +e.target.value) + } + }) + self.stretchSelector = Input.select({ + name: 'stretch', + value: self.options.layer && self.options.layer.getColorCfg().stretch || 'linear', + options: ['sqrt', 'linear', 'asinh', 'pow2', 'log'], + change() { + let layer = self.options.layer; + layer.setColormap(layer.getColorCfg().getColormap(), {stretch: this.value}); + }, + tooltip: {content: 'stretch function', position: {direction: 'right'}} + }); + + self._addListeners() + } + + update(options) { + let self = this; + if (options && options.layer) { + let layer = options.layer; + // Define the contents + + let layerOpacity = layer.getOpacity() + + self.opacitySettingsContent = Layout.horizontal([ + Input.slider({ + tooltip: {content: layerOpacity, position: {direction: 'bottom'}}, + name: 'opacitySlider', + type: 'range', + min: 0.0, + max: 1.0, + value: layerOpacity, + change(e, slider) { + const opacity = +e.target.value; + layer.setOpacity(opacity) + slider.update({value: opacity, tooltip: {content: opacity.toFixed(2), position: {direction: 'bottom'}}}) + } + }), + ]); + + let brightness = layer.getColorCfg().getBrightness() + let saturation = layer.getColorCfg().getSaturation() + let contrast = layer.getColorCfg().getContrast() + + self.luminositySettingsContent = Layout.vertical({ + layout: [ + Input.slider({ + cssStyle: { + marginBottom: '7px', + }, + tooltip: {content: 'brightness', position: {direction: 'right'}}, + name: 'brightness', + type: 'range', + min: -1, + max: 1, + ticks: [0.0], + value: brightness, + change(e, slider) { + const brightness = +e.target.value + layer.setBrightness(brightness) + } + }), + Input.slider({ + cssStyle: { + marginBottom: '7px', + }, + tooltip: {content: 'saturation', position: {direction: 'right'}}, + name: 'saturation', + type: 'range', + min: -1, + max: 1, + ticks: [0.0], + value: saturation, + change(e, slider) { + const saturation = +e.target.value + layer.setSaturation(saturation) + } + }), + Input.slider({ + tooltip: {content: 'contrast', position: {direction: 'right'}}, + name: 'contrast', + type: 'range', + min: -1, + max: 1, + ticks: [0.0], + value: contrast, + change(e, slider) { + const contrast = +e.target.value + layer.setContrast(contrast) + } + }), + ] + }); + const [minCut, maxCut] = layer.getColorCfg().getCuts(); + self.minCutInput.set(minCut); + self.maxCutInput.set(maxCut) + self.stretchSelector.update({value: layer.getColorCfg().stretch}) + + self.pixelSettingsContent = Layout.horizontal({ + layout: [ + self.stretchSelector, + self.minCutInput, + self.maxCutInput + ] + }); + + let cmap = layer.getColorCfg().getColormap(); + + this.colorSettingsContent = Input.select({ + name: 'colormap', + value: cmap, + options: ColorCfg.COLORMAPS, + change() { + let colormap = this.value; + layer.setColormap(colormap) + }, + }); + + //this.colorSettingsContent = new CmapSelector(optionsCmapSelector, this.aladin); + let content = (() => { + let selected = self.selector.options.selected; + switch (selected) { + case 'colors': + return self.colorSettingsContent; + case 'pixel': + return self.pixelSettingsContent; + case 'opacity': + return self.opacitySettingsContent; + case 'luminosity': + return self.luminositySettingsContent; + default: + return self.opacitySettingsContent; + } + })(); + options.content = Layout.horizontal({layout: [self.selector, content]}); + } + + super.update(options) + } + + _show(options) { + this._hide(); + + if (this.selector) { + this.selector._show(); + } + + if (this.stretchSelector) { + this.stretchSelector._show(); + } + + if (this.colorSettingsContent) { + this.colorSettingsContent._show(); + } + + super._show(options) + } + + _hide() { + if (this.colorSettingsContent) { + this.colorSettingsContent._hide(); + } + + if (this.stretchSelector) { + this.stretchSelector._hide(); + } + + if (this.selector) { + this.selector._hide(); + } + + super._hide() + } + + _addListeners() { + ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, (e) => { + const layerChanged = e.detail.layer; + let selectedLayer = this.options.layer; + if (selectedLayer && layerChanged.layer === selectedLayer.layer) { + let colorCfg = layerChanged.getColorCfg(); + + let cmap = colorCfg.getColormap(); + let reversed = colorCfg.getReversed(); + let stretch = colorCfg.stretch; + + let [minCut, maxCut] = colorCfg.getCuts(); + this.minCutInput.set(+minCut.toFixed(2)); + this.maxCutInput.set(+maxCut.toFixed(2)); + this.stretchSelector.update({value: stretch}) + } + }); + } + + static singleton; + + static getInstance(aladin) { + if (!LayerEditBox.singleton) { + LayerEditBox.singleton = new LayerEditBox(aladin); + } + + return LayerEditBox.singleton; + } +} + \ No newline at end of file diff --git a/src/js/gui/Button/ConeSearch.js b/src/js/gui/Button/ConeSearch.js new file mode 100644 index 000000000..f7de009d5 --- /dev/null +++ b/src/js/gui/Button/ConeSearch.js @@ -0,0 +1,71 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + + import { ActionButton } from "../Widgets/ActionButton.js"; + import targetIconUrl from '../../../../assets/icons/target.svg'; + + /* + options = { + action: (connector) => { + + } + tooltip + } + */ +export class ConeSearchActionButton extends ActionButton { + // Constructor + constructor(options, aladin) { + super({ + icon: { + size: 'medium', + monochrome: true, + url: targetIconUrl + }, + tooltip: options.tooltip, + disable: options.disable, + cssStyle: { + backgroundPosition: 'center center', + cursor: 'pointer', + ...options.cssStyle + }, + action(e) { + if (options.onBeforeClick) { + options.onBeforeClick(e); + } + + aladin.select('circle', c => { + options.action(c) + }) + } + }) + + this.addClass('medium-sized-icon') + } +} \ No newline at end of file diff --git a/src/js/gui/Button/CtxMenuOpener.js b/src/js/gui/Button/CtxMenuOpener.js new file mode 100644 index 000000000..5620f6818 --- /dev/null +++ b/src/js/gui/Button/CtxMenuOpener.js @@ -0,0 +1,121 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; +import { ContextMenu } from "../Widgets/ContextMenu.js"; + + /* + options = { + action: (connector) => { + + } + tooltip + } + */ +export class CtxMenuActionButtonOpener extends ActionButton { + //static BUTTONS = []; + + // Constructor + constructor(options, aladin) { + let self; + + let ctxMenu = new ContextMenu(aladin, {hideOnClick: true, hideOnResize: true}) + super({ + ...options, + cssStyle: { + backgroundPosition: 'center center', + cursor: 'pointer', + ...options.cssStyle + }, + action(e) { + + //self.ctxMenu._hide(); + + if (self.ctxMenu.isHidden === true) { + if (options.action) { + options.action(e) + } + + self.ctxMenu.attach(self.layout) + self.ctxMenu.show({ + position: { + nextTo: self, + direction: options.openDirection || 'bottom', + }, + cssStyle: options.ctxMenu && options.ctxMenu.cssStyle + }); + + //CtxMenuActionButtonOpener.BUTTONS.forEach(b => {b.hidden = true}) + } else { + self.hideMenu(); + } + + //self.hidden = !self.hidden; + } + }) + + //this.hidden = true; + + this.layout = options.ctxMenu; + + self = this; + self.ctxMenu = ctxMenu; + + //CtxMenuActionButtonOpener.BUTTONS.push(this); + } + + hideMenu() { + this.ctxMenu._hide(); + } + + _hide() { + this.hideMenu(); + super._hide(); + } + + update(options) { + if(options.ctxMenu) { + this.layout = options.ctxMenu; + } + + super.update(options) + + if (!this.ctxMenu.isHidden) { + this.ctxMenu.attach(this.layout) + this.ctxMenu.show({ + position: { + nextTo: this, + direction: options.openDirection || 'bottom', + }, + cssStyle: options.ctxMenu && options.ctxMenu.cssStyle + }); + } + } +} \ No newline at end of file diff --git a/src/js/gui/Button/FileLoader.js b/src/js/gui/Button/FileLoader.js new file mode 100644 index 000000000..d995685cb --- /dev/null +++ b/src/js/gui/Button/FileLoader.js @@ -0,0 +1,75 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + + import { ActionButton } from "../Widgets/ActionButton.js"; + import uploadIconUrl from '../../../../assets/icons/upload.svg'; + + /* + options = { + action: (connector) => { + + } + tooltip + } + */ +export class FileLoaderActionButton extends ActionButton { + // Constructor + constructor(options) { + super({ + icon: { + size: 'medium', + monochrome: true, + url: uploadIconUrl + }, + tooltip: options.tooltip, + cssStyle: { + backgroundPosition: 'center center', + cursor: 'help', + ...options.cssStyle + }, + action(e) { + let fileLoader = document.createElement('input'); + fileLoader.type = 'file'; + fileLoader.accept = options.accept || '*'; + // Case: The user is loading a FITS file + + fileLoader.addEventListener("change", (e) => { + let file = e.target.files[0]; + + if (options.action) { + options.action(file) + } + }); + + fileLoader.click(); + } + }) + } +} \ No newline at end of file diff --git a/src/js/gui/Button/FullScreen.js b/src/js/gui/Button/FullScreen.js new file mode 100644 index 000000000..eec0f49ed --- /dev/null +++ b/src/js/gui/Button/FullScreen.js @@ -0,0 +1,100 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; + +import restoreIcon from './../../../../assets/icons/restore.svg'; +import maximizeIcon from './../../../../assets/icons/maximize.svg'; + +export class FullScreenActionButton extends ActionButton { + // Constructor + constructor(aladin, options) { + let self; + super({ + icon: { + size: 'medium', + monochrome: true, + url: aladin.isInFullscreen ? restoreIcon : maximizeIcon + }, + ...options, + tooltip: { + content: aladin.isInFullscreen ? 'Restore original size' : 'Full-screen', + position: { + direction: 'left' + } + }, + action(e) { + if (aladin.statusBar) { + aladin.statusBar.removeMessage('tooltip') + } + + aladin.toggleFullscreen(aladin.options.realFullscreen); + + if (aladin.isInFullscreen) { + // make that div above other aladin lite divs (if there are...) + aladin.aladinDiv.style.zIndex = 1 + self.update({ + icon: { + size: 'medium', + monochrome: true, + url: restoreIcon + }, + tooltip: { + content: 'Restore original size', + position: { + direction: 'left' + } + } + }); + } else { + aladin.aladinDiv.style.removeProperty('z-index') + + self.update({ + icon: { + size: 'medium', + monochrome: true, + url: maximizeIcon + }, + tooltip: { + content: 'Fullscreen', + position: { + direction: 'left' + } + } + }); + } + } + }) + + self = this; + + this.addClass('medium-sized-icon') + } +} \ No newline at end of file diff --git a/src/js/gui/Button/GridEnabler.js b/src/js/gui/Button/GridEnabler.js new file mode 100644 index 000000000..8a3bd47b5 --- /dev/null +++ b/src/js/gui/Button/GridEnabler.js @@ -0,0 +1,80 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; +import gridIcon from './../../../../assets/icons/grid.svg'; + +export class GridEnabler extends ActionButton { + // Constructor + constructor(aladin) { + const computeTooltip = (enabled) => { + const content = enabled ? 'Hide the coordinate grid' : 'Display the coordinate grid' + return { + content, + position: { + direction: 'top right' + } + } + } + + let gridEnabled = aladin.getGridOptions().enabled; + let self; + super({ + icon: { + size: 'medium', + monochrome: true, + url: gridIcon + }, + tooltip: computeTooltip(gridEnabled), + toggled: gridEnabled, + action(o) { + const isGridEnabled = aladin.getGridOptions().enabled; + const enabled = !isGridEnabled; + aladin.setCooGrid({enabled}) + + self.update({toggled: enabled, tooltip: computeTooltip(enabled)}) + + if (aladin.statusBar) { + aladin.statusBar.removeMessage('grid') + + if (enabled) { + aladin.statusBar.appendMessage({ + id: 'grid', + message: 'Grid enabled!', + duration: 2000, + type: 'info' + }) + } + } + } + }) + self = this; + } +} \ No newline at end of file diff --git a/src/js/gui/Button/MainSurvey.js b/src/js/gui/Button/MainSurvey.js new file mode 100644 index 000000000..c262af1ff --- /dev/null +++ b/src/js/gui/Button/MainSurvey.js @@ -0,0 +1,70 @@ +// 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 { ALEvent } from "../../events/ALEvent"; + +import { ActionButton } from "../Widgets/ActionButton"; +import mapIconUrl from '../../../../assets/icons/map.svg'; + +/****************************************************************************** + * Aladin Lite project + * + * File gui/ActionButton.js + * + * A context menu that shows when the user right clicks, or long touch on touch device + * + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ + + export class MainSurveyActionButton extends ActionButton { + /** + * UI responsible for displaying the viewport infos + * @param {Aladin} aladin - The aladin instance. + */ + constructor(aladin, options) { + super({ + ...options, + tooltip: {content: 'Survey name
Click to change it!', position: { direction: 'bottom' }}, + iconURL: mapIconUrl, + }) + + this.addClass('medium-sized-icon') + + this._addListeners(aladin) + } + + _addListeners(aladin) { + ALEvent.HIPS_LAYER_ADDED.listenedBy(aladin.aladinDiv, (e) => { + const layer = e.detail.layer; + if (layer.layer === 'base') { + let name = (layer.properties && layer.properties.obsTitle) || layer.name; + this.update({ + tooltip: { + content: 'Survey: ' + name, + position: { + direction: 'left' + } + } + }) + } + }); + } +} diff --git a/src/js/gui/Button/Projection.js b/src/js/gui/Button/Projection.js new file mode 100644 index 000000000..3a885fde3 --- /dev/null +++ b/src/js/gui/Button/Projection.js @@ -0,0 +1,97 @@ +// 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 { CtxMenuActionButtonOpener } from "./CtxMenuOpener"; +import { ProjectionEnum } from "../../ProjectionEnum"; +import projectionIconUrl from '../../../../assets/icons/projection.svg'; +import { ALEvent } from "../../events/ALEvent"; +/****************************************************************************** + * Aladin Lite project + * + * File gui/ActionButton.js + * + * A context menu that shows when the user right clicks, or long touch on touch device + * + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ +/** + * Class representing a Tabs layout + * @extends CtxMenuActionButtonOpener + */ + export class ProjectionActionButton extends CtxMenuActionButtonOpener { + /** + * UI responsible for displaying the viewport infos + * @param {Aladin} aladin - The aladin instance. + */ + constructor(aladin, options) { + super({ + icon: { + monochrome: true, + size: 'small', + url: projectionIconUrl, + }, + content: [ProjectionEnum[aladin.getProjectionName()].label], + tooltip: {content: 'Change the view projection', position: {direction: 'bottom left'}}, + cssStyle: { + cursor: 'pointer', + }, + ...options + }, aladin); + + let ctxMenu = this._buildLayout(aladin); + this.update({ctxMenu}) + + this._addEventListeners(aladin) + } + + _addEventListeners(aladin) { + let self = this; + ALEvent.PROJECTION_CHANGED.listenedBy(aladin.aladinDiv, function (e) { + self.update({content: [ProjectionEnum[aladin.getProjectionName()].label]}) + }); + } + + _buildLayout(aladin) { + let layout = []; + let self = this; + + let aladinProj = aladin.getProjectionName(); + for (const key in ProjectionEnum) { + let proj = ProjectionEnum[key]; + layout.push({ + label: proj.label, + selected: aladinProj === key, + action(o) { + aladin.setProjection(key) + + let ctxMenu = self._buildLayout(aladin); + self.update({ctxMenu, content: proj.label}); + } + }) + } + + return layout; + } + + _show() { + super._show() + } +} diff --git a/src/js/gui/Button/SAMP.js b/src/js/gui/Button/SAMP.js new file mode 100644 index 000000000..519a75733 --- /dev/null +++ b/src/js/gui/Button/SAMP.js @@ -0,0 +1,157 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; +import { ALEvent } from "../../events/ALEvent.js"; +import waveOnIconUrl from '../../../../assets/icons/wave-on.svg'; +import waveOffIconUrl from '../../../../assets/icons/wave-off.svg'; + +/* +options = { + action: (connector) => { + + } + tooltip +} +*/ + export class SAMPActionButton extends ActionButton { + // Constructor + constructor(options, aladin) { + if (!aladin.samp) { + options = { + ...options, + icon: { + monochrome: true, + url: waveOffIconUrl + }, + tooltip: {content: 'SAMP disabled in Aladin Lite options', position: {direction: 'top'}}, + disable: true, + } + } else { + //let isHubRunning = aladin.samp.isHubCurrentlyRunning(); + let tooltip = options && options.tooltip || {content: 'Connect to SAMP Hub', position: {direction: 'top'}} + let action = options && options.action + if (!action) { + // default action, just connect and ping + action = (connector) => { + connector.register(); + } + } + + options = { + ...options, + icon: { + monochrome: true, + url: aladin.samp.isConnected() ? waveOnIconUrl : waveOffIconUrl + }, + tooltip, + action(o) { + action(aladin.samp) + } + } + } + + super(options) + + this._addListeners(aladin); + } + + _addListeners(aladin) { + let self = this; + ALEvent.SAMP_CONNECTED.listenedBy(aladin.aladinDiv, function (e) { + const icon = { + monochrome: true, + url: waveOnIconUrl + } + self.update({icon}) + }); + + ALEvent.SAMP_DISCONNECTED.listenedBy(aladin.aladinDiv, function (e) { + const icon = { + monochrome: true, + url: waveOffIconUrl + } + self.update({icon}) + }); + + /*ALEvent.SAMP_HUB_RUNNING.listenedBy(aladin.aladinDiv, function (e) { + const isHubRunning = e.detail.isHubRunning; + + if (hubRunning !== isHubRunning) { + let newOptions = { + disable: !isHubRunning, + tooltip: isHubRunning ? {content: 'Connect to SAMP hub'} : {content: 'No hub running found'} + }; + + self.update(newOptions) + if (isHubRunning === false) { + self.update({iconURL: waveOffIconUrl}) + } + hubRunning = isHubRunning; + } + });*/ + } + + static sendSources(aladin) { + return new SAMPActionButton({ + size: 'small', + tooltip: {content: 'Send a table through SAMP Hub'}, + action(conn) { + // hide the menu + aladin.contextMenu._hide() + + let getSource = (o) => { + let s = o; + if (o.source) { + s = o.source + } + + return s; + }; + + for (const objects of aladin.view.selection) { + let s0 = getSource(objects[0]); + const cat = s0.catalog; + const {url, name} = cat; + conn.loadVOTable(url, name, url); + + let rowList = []; + for (const obj of objects) { + // select the source + let s = getSource(obj) + rowList.push('' + s.rowIdx); + }; + conn.tableSelectRowList(name, url, rowList) + } + } + }, aladin) + } +} + \ No newline at end of file diff --git a/src/js/gui/Button/ShareView.js b/src/js/gui/Button/ShareView.js new file mode 100644 index 000000000..5d65c7d9a --- /dev/null +++ b/src/js/gui/Button/ShareView.js @@ -0,0 +1,142 @@ +// 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 { CtxMenuActionButtonOpener } from "./CtxMenuOpener"; +import shareIconUrl from '../../../../assets/icons/share.svg'; +import cameraIconUrl from '../../../../assets/icons/camera.svg'; + +/****************************************************************************** + * Aladin Lite project + * + * File gui/ActionButton.js + * + * A context menu that shows when the user right clicks, or long touch on touch device + * + * + * Author: Matthieu Baumann[CDS] + * + *****************************************************************************/ +/** + * Class representing a Tabs layout + * @extends CtxMenuActionButtonOpener + */ + export class ShareActionButton extends CtxMenuActionButtonOpener { + /** + * UI responsible for displaying the viewport infos + * @param {Aladin} aladin - The aladin instance. + */ + constructor(aladin, options) { + let layout = [ + { + label: { + content: 'Save the view', + tooltip: { + content: 'View URL will be saved into your clipboard', + position: { + direction: 'bottom' + } + } + }, + action(o) { + var url = aladin.getShareURL(); + navigator.clipboard.writeText(url); + + if (aladin.statusBar) { + aladin.statusBar.appendMessage({ + message: 'View URL saved into your clipboard!', + duration: 2000, + type: 'info' + }) + } + } + }, + { + label: 'HiPS2FITS cutout', + action(o) { + let hips2fitsUrl = 'https://alasky.cds.unistra.fr/hips-image-services/hips2fits#'; + let radec = aladin.getRaDec(); + let fov = Math.max.apply(null, aladin.getFov()); + let hipsId = aladin.getBaseImageLayer().id; + let proj = aladin.getProjectionName(); + hips2fitsUrl += 'ra=' + radec[0] + '&dec=' + radec[1] + '&fov=' + fov + '&projection=' + proj + '&hips=' + encodeURIComponent(hipsId); + window.open(hips2fitsUrl, '_blank'); + } + }, + { + label: 'Export to notebook', + disabled: true, + }, + { + label: { + content: 'Save the WCS', + tooltip: { + content: 'World Coordinate System of the view', + position: { + direction: 'right' + } + } + }, + action(o) { + let wcs = aladin.getViewWCS() + navigator.clipboard.writeText(JSON.stringify(wcs)); + + if (aladin.statusBar) { + aladin.statusBar.appendMessage({ + message: 'WCS saved into your clipboard!', + duration: 2000, + type: 'info' + }) + } + } + }, + { + label: { + icon: { + tooltip: {content: 'Download a PNG image file of the view', position: {direction: 'top'}}, + monochrome: true, + url: cameraIconUrl, + size: 'small', + }, + content: 'Export as image file' + }, + action(o) { + aladin.exportAsPNG() + } + }, + ]; + + let self; + super({ + ctxMenu: layout, + ...options, + icon: { + url: shareIconUrl, + monochrome: true, + }, + openDirection: 'top', + tooltip: {content: 'You can share/export your view into many ways', position: {direction: 'top'}}, + }, aladin); + self = this; + this.addClass('medium-sized-icon') + } + + _show() { + super._show() + } +} diff --git a/src/js/gui/Button/SimbadPointer.js b/src/js/gui/Button/SimbadPointer.js new file mode 100644 index 000000000..3f3228647 --- /dev/null +++ b/src/js/gui/Button/SimbadPointer.js @@ -0,0 +1,93 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; +import targetIcon from '../../../../assets/icons/target.svg'; +import { ALEvent } from "../../events/ALEvent.js"; +import { View } from "../../View.js"; + +export class SimbadPointer extends ActionButton { + // Constructor + constructor(aladin) { + let self; + super({ + icon: { + url: targetIcon, + monochrome: true, + }, + size: 'medium', + tooltip: { + content: 'Want to know what is a specific object ?
Use the Simbad pointer tool!', + position: { direction: 'top right' }, + }, + action(o) { + if (self.mode !== View.TOOL_SIMBAD_POINTER) { + aladin.fire('simbad'); + } else { + aladin.fire('default'); + } + } + }) + self = this; + + this.aladin = aladin; + this.mode = aladin.view.mode; + + this.addListeners() + } + + updateStatus() { + if (this.mode === View.TOOL_SIMBAD_POINTER) { + if (this.aladin.statusBar) { + this.aladin.statusBar.appendMessage({ + id: 'simbad', + message: 'Simbad pointer mode, click on the icon to exit', + type: 'info' + }) + } + } else { + if (this.aladin.statusBar) { + this.aladin.statusBar.removeMessage('simbad') + } + } + + this.update({toggled: this.mode === View.TOOL_SIMBAD_POINTER}) + } + + addListeners() { + ALEvent.MODE.listenedBy(this.aladin.aladinDiv, e => { + let mode = e.detail.mode; + this.mode = mode; + + this.updateStatus(); + }); + } +} + \ No newline at end of file diff --git a/src/js/Location.js b/src/js/gui/Button/Snapshot.js similarity index 52% rename from src/js/Location.js rename to src/js/gui/Button/Snapshot.js index cf5589455..a81779647 100644 --- a/src/js/Location.js +++ b/src/js/gui/Button/Snapshot.js @@ -18,43 +18,30 @@ // - /****************************************************************************** * Aladin Lite project - * - * File Location.js - * - * Author: Thomas Boch[CDS] - * + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * *****************************************************************************/ - -import { Coo } from "./libs/astro/coo.js"; -import { CooFrameEnum } from "./CooFrameEnum.js"; - -import $ from 'jquery'; - -export let Location = (function () { - // constructor - function Location(locationDiv) { - this.$div = $(locationDiv); - }; - - Location.prototype.update = function (lon, lat, cooFrame, isViewCenterPosition) { - var coo = new Coo(lon, lat, 7); - if (cooFrame == CooFrameEnum.J2000) { - this.$div.html(coo.format('s/')); - } - else if (cooFrame == CooFrameEnum.J2000d) { - this.$div.html(coo.format('d/')); - } - else { - this.$div.html(coo.format('d/')); - } - - this.$div.toggleClass('aladin-reticleColor', isViewCenterPosition); - }; - - return Location; -})(); - + import { ActionButton } from "../Widgets/ActionButton.js"; + import cameraIconUrl from '../../../../assets/icons/camera.svg'; + +export class SnapshotActionButton extends ActionButton { + // Constructor + constructor(options, aladin) { + super({ + iconURL: cameraIconUrl, + ...options, + action(e) { + aladin.exportAsPNG() + } + }) + + this.addClass('medium-sized-icon') + } +} \ No newline at end of file diff --git a/src/js/gui/Button/Toggler.js b/src/js/gui/Button/Toggler.js new file mode 100644 index 000000000..e878a8f03 --- /dev/null +++ b/src/js/gui/Button/Toggler.js @@ -0,0 +1,62 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { ActionButton } from "../Widgets/ActionButton.js"; + +export class TogglerActionButton extends ActionButton { + // Constructor + constructor(options) { + let self; + let toggled = false; + if (options.toggled !== undefined) { + toggled = options.toggled; + } + + super({ + ...options, + toggled, + action(o) { + toggled = !toggled; + + self.update({toggled, tooltip: toggled ? options.tooltipOn : options.tooltipOff}) + if (toggled && options.actionOn) { + options.actionOn(o) + } + + if (!toggled && options.actionOff) { + options.actionOff(o) + } + + options.action && options.action(o) + } + }) + self = this; + } +} \ No newline at end of file diff --git a/src/js/gui/Button/WidgetOpener.js b/src/js/gui/Button/WidgetOpener.js new file mode 100644 index 000000000..1ffa4d464 --- /dev/null +++ b/src/js/gui/Button/WidgetOpener.js @@ -0,0 +1,58 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + + import { ActionButton } from "../Widgets/ActionButton.js"; + + /* + options = { + action: (connector) => { + + } + tooltip + } + */ +export class WidgetOpenerActionButton extends ActionButton { + // Constructor + constructor(options) { + let widget = options.widget; + super({ + ...options, + action(e) { + if (widget.isHidden) { + widget._show(); + } else { + widget._hide(); + } + } + }) + + this.addClass('medium-sized-icon') + } +} \ No newline at end of file diff --git a/src/js/gui/CatalogSelector.js b/src/js/gui/CatalogSelector.js deleted file mode 100644 index 7e6fed6f7..000000000 --- a/src/js/gui/CatalogSelector.js +++ /dev/null @@ -1,274 +0,0 @@ -// 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 { MocServer } from "../MocServer.js"; -import { Utils } from "../Utils"; -import autocomplete from 'autocompleter'; - -import $ from 'jquery'; - -/****************************************************************************** - * Aladin Lite project - * - * File gui/CatalogSelector.js - * - * - * Author: Thomas Boch[CDS] - * - *****************************************************************************/ - - export class CatalogSelector { - - constructor(parentDiv, aladin, fnIdSelected) { - this.parentDiv = parentDiv; - this.aladin = aladin; - - this.fnIdSelected = fnIdSelected; - - this._createComponent(); - } - - _createComponent() { - const self = this; - - this.mainDiv = document.createElement('div'); - this.mainDiv.classList.add('aladin-dialog', 'aladin-cb-list'); - this.mainDiv.style.display = 'block'; - - const autocompleteId = 'autocomplete-' + Utils.uuidv4(); - this.mainDiv.insertAdjacentHTML('afterbegin', - '×' + - '
Select Catalogue:
' + - '
' + - '
By ID, title, keyword
' + - '' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
By VOTable URL
' + - '' + - '
' + - '
' - ); - - this.parentDiv.appendChild(this.mainDiv); - - this.idInput = self.mainDiv.querySelectorAll('input')[0]; - this.votInput = self.mainDiv.querySelectorAll('input')[3]; - - // Unfocus the keyboard on android devices (maybe it concerns all smartphones) when the user click on enter - $(this.idInput).on("change", function () { - self.idInput.blur(); - }); - - $(this.votInput).on("change", function () { - self.votInput.blur(); - }); - - let [loadCSBtn, loadHiPSBtn, loadVOTableBtn] = this.mainDiv.querySelectorAll('.aladin-btn'); - this.divCS = this.mainDiv.querySelector('.cone-search'); - this.divLoadHiPS = this.mainDiv.querySelector('.hips'); - this.divLoadHiPS.style.display = "none"; - - - // retrieve cone search div and load HiPS div - //this.divCS = this.mainDiv.querySelector('div > div:nth-child(5) > div:nth-child(1) > div > div.col-start-1'); - //this.divLoadHiPS = this.mainDiv.querySelector('div > div:nth-child(5) > div:nth-child(1) > div > div.col-start-9'); - //$(this.divCS).hide(); - //$(this.divLoadHiPS).hide(); - - // listener to load CS data - //const loadCSBtn = this.divCS.querySelector('div:nth-child(1) > button'); - - $(loadCSBtn).click(function() { - const radius = parseFloat(self.divCS.querySelector('div:nth-child(1) > input').value); - const radiusUnit = self.divCS.querySelector('div:nth-child(1) > select').value; - let radiusDeg = radius; - if (radiusUnit=='arcmin') { - radiusDeg /= 60.0; - } - else if (radiusUnit=='arcsec') { - radiusDeg /= 3600.0; - } - const maxNbSources = parseInt(self.divCS.querySelector('div:nth-child(2) > input').value); - const baseURL = self.selectedItem.cs_service_url; - - const [ra, dec] = self.aladin.getRaDec(); - - self.fnIdSelected && self.fnIdSelected('coneSearch', {id: self.idInput.value, baseURL: baseURL, limit: maxNbSources, radiusDeg: radiusDeg, ra: ra, dec: dec}); - // Reset value of the input - self.idInput.value = null; - }); - - // listener to load HiPS catalogue - //const loadHiPSBtn = this.divLoadHiPS.querySelector('button'); - $(loadHiPSBtn).click(function() { - self.fnIdSelected && self.fnIdSelected('hips', {id: self.idInput.value, hipsURL: self.selectedItem.hips_service_url}); - // Reset value of the input - self.idInput.value = null; - }); - - // listener to load catalogue from VOTable URL - //const loadVOTableBtn = document.querySelector('div > div:nth-child(5) > div:nth-child(2) > div > button'); - $(loadVOTableBtn).click(function() { - self.fnIdSelected && self.fnIdSelected('votable', {url: self.votInput.value}); - // Reset value of the input - self.votInput.value = null; - }); - - // setup autocomplete - let input = document.getElementById(autocompleteId); - - // Query the mocserver - MocServer.getAllCatalogHiPSes(); - autocomplete({ - input: input, - minLength: 3, - fetch: function(text, update) { - text = text.toLowerCase(); - const filterCats = function(item) { - const ID = item.ID; - const obsTitle = item.obs_title || ''; - const obsDescription = item.obs_description || ''; - - return ID.toLowerCase().includes(text) || obsTitle.toLowerCase().includes(text) || obsDescription.toLowerCase().includes(text); - } - // filter suggestions - const suggestions = MocServer.getAllCatalogHiPSes().filter(filterCats); - // sort suggestions - suggestions.sort( function(a , b) { - let scoreForA = 0; - let scoreForB = 0; - - if (a.ID.toLowerCase().includes(text)) { - scoreForA += 100; - } - if (b.ID.toLowerCase().includes(text)) { - scoreForB += 100; - } - - if (a.obs_title.toLowerCase().includes(text)) { - scoreForA += 50; - } - if (b.obs_title.toLowerCase().includes(text)) { - scoreForB += 50; - } - - if (a.obs_description.toLowerCase().includes(text)) { - scoreForA += 10; - } - if (b.obs_description.toLowerCase().includes(text)) { - scoreForB += 10; - } - - // HiPS catalogue available - if (a.hips_service_url) { - scoreForA += 20; - } - if (b.hips_service_url) { - scoreForB += 20; - } - - if (scoreForA > scoreForB) { - return -1; - } - if (scoreForB > scoreForA) { - return 1; - } - - return 0; - }); - - // limit to 50 first suggestions - const returnedSuggestions = suggestions.slice(0, 50); - - update(returnedSuggestions); - }, - onSelect: function(item) { - // adapt UI to selected catalogue - if (item.cs_service_url) { - $(self.divCS).show(); - } - else { - $(self.divCS).hide(); - } - if (item.hips_service_url) { - $(self.divLoadHiPS).show(); - } - else { - $(self.divLoadHiPS).hide(); - } - - input.value = item.ID; - self.selectedItem = item; - - input.blur(); - }, - // attach container to AL div if needed (to prevent it from being hidden in full screen mode) - customize: function(input, inputRect, container, maxHeight) { - // this tests if we are in full screen mode - if (self.aladin.fullScreenBtn.hasClass('aladin-restore')) { - self.parentDiv.appendChild(container); - } - }, - render: function(item, currentValue) { - const itemElement = document.createElement("div"); - itemElement.innerHTML = (item.obs_title || '') + ' - ' + '' + item.ID + ''; - - - return itemElement; - } - }); - - // this modal is closed when clicking on the cross at the top right, or on the Cancel button - let [closeBtn] = this.mainDiv.querySelectorAll('.aladin-closeBtn'); - - $(closeBtn).click(function() { - self.hide(); - }); - } - - show() { - this.mainDiv.style.display = 'block'; - /* - // focus on text field - let byIdSelected = $(this.mainDiv.querySelectorAll('div div a')[0]).hasClass('tab-active'); - if (byIdSelected) { - let idInput = this.mainDiv.querySelectorAll('div div .p-4')[0].querySelector('input'); - idInput.focus(); - } - else { - let urlInput = this.mainDiv.querySelectorAll('div div .p-4')[1].querySelector('input'); - urlInput.focus(); - }*/ - } - - hide() { - this.mainDiv.style.display = 'none'; - } - -} diff --git a/src/js/gui/ContextMenu.js b/src/js/gui/ContextMenu.js deleted file mode 100644 index e96986e76..000000000 --- a/src/js/gui/ContextMenu.js +++ /dev/null @@ -1,175 +0,0 @@ -// 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. -// - - -/****************************************************************************** - * Aladin Lite project - * - * File gui/ContextMenu.js - * - * A context menu that shows when the user right clicks, or long touch on touch device - * - * - * Author: Thomas Boch[CDS] - * - *****************************************************************************/ - -import { Coo } from '../libs/astro/coo.js'; -import { CooFrameEnum } from '../CooFrameEnum.js'; -import { Utils } from '../Utils'; - -export class ContextMenu { - - constructor(aladin) { - this.aladin = aladin; - this.isShowing = false; - } - - _hideMenu(e) { - //if (e === true || !this.contextMenuUl.contains(e.target)) { - this.contextMenuUl.remove(); - document.removeEventListener('click', this._hideMenu); - window.removeEventListener('resize', this._hideOnResize); - - this.isShowing = false; - //} - } - - _hideOnResize() { - this._hideMenu(true); - } - - _attachOption(target, opt, xymouse) { - const item = document.createElement('li'); - item.className = 'aladin-context-menu-item'; - if (opt.label == 'Copy position') { - try { - const pos = this.aladin.pix2world(xymouse.x, xymouse.y); - const coo = new Coo(pos[0], pos[1], 6); - let posStr; - if (this.aladin.view.cooFrame == CooFrameEnum.J2000) { - posStr = coo.format('s/'); - } else if (this.aladin.view.cooFrame == CooFrameEnum.J2000d) { - posStr = coo.format('d/'); - } else { - posStr = coo.format('d/'); - } - item.innerHTML = '' + posStr + ''; - } catch (e) { - item.innerHTML = ''; - } - } else { - item.innerHTML = '' + opt.label + ''; - } - - if (opt.subMenu && opt.subMenu.length > 0) { - item.innerHTML += ''; - } - - const self = this; - item.addEventListener('click', e => { - e.stopPropagation(); - if (!opt.subMenu || opt.subMenu.length === 0) { - if (opt.label == 'Copy position') { - opt.action(e); - } else { - opt.action(this.event); - } - self._hideMenu(true); - } - }); - - target.appendChild(item); - - if (opt.subMenu && opt.subMenu.length) { - const subMenu = document.createElement('ul'); - subMenu.className = 'aladin-context-sub-menu'; - item.appendChild(subMenu); - opt.subMenu.forEach(subOpt => this._attachOption(subMenu, subOpt)); - } - } - - _showMenu(e) { - this.contextMenuUl.className = 'aladin-context-menu'; - this.contextMenuUl.innerHTML = ''; - - const xymouse = Utils.relMouseCoords(this.aladin.view.imageCanvas, e); - - this.menuOptions.forEach(opt => this._attachOption(this.contextMenuUl, opt, xymouse)); - document.body.appendChild(this.contextMenuUl); - - const { innerWidth, innerHeight } = window; - const { offsetWidth, offsetHeight } = this.contextMenuUl; - let x = 0; - let y = 0; - - this.event = e; - - - if (e.clientX >= (innerWidth / 2)) { - this.contextMenuUl.classList.add('left'); - } - - if (e.clientY >= (innerHeight / 2)) { - this.contextMenuUl.classList.add('top'); - } - - if (e.clientX >= (innerWidth - offsetWidth)) { - x = '-100%'; - } - - if (e.clientY >= (innerHeight - offsetHeight)) { - y = '-100%'; - } - - this.contextMenuUl.style.left = e.clientX + 'px'; - this.contextMenuUl.style.top = e.clientY + 'px'; - this.contextMenuUl.style.transform = `translate(${x}, ${y})`; - document.addEventListener('click', () => this._hideMenu(true)); - window.addEventListener('resize', this._hideOnResize); - - this.isShowing = true; - } - - - attachTo(el, options) { - this.contextMenuUl = document.createElement('ul'); - this.menuOptions = options; - - const self = this; - /* - el.addEventListener('contextmenu', function (e) { - e.preventDefault(); - self._showMenu(e, options, el); - - e.stopPropagation(); - }); - */ - - } - -} - - - - - - - - diff --git a/src/js/gui/CooGrid.js b/src/js/gui/CooGrid.js deleted file mode 100644 index 247a4948c..000000000 --- a/src/js/gui/CooGrid.js +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2013 - UDS/CNRS -// The Aladin Lite program is distributed under the terms -// of the GNU General Public License version 3. -// -// This file is part of Aladin Lite. -// -// Aladin Lite is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 3 of the License. -// -// Aladin Lite is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// The GNU General Public License is available in COPYING file -// along with Aladin Lite. -// - - -/****************************************************************************** - * Aladin Lite project - * - * File gui/Stack.js - * - * - * Author: Thomas Boch[CDS] - * - *****************************************************************************/ - - import { Color } from "../Color.js"; - import { ALEvent } from "../events/ALEvent.js"; - - import $ from 'jquery'; - - export class CooGrid { - - // Constructor - constructor(parentDiv, aladin, view) { - this.aladin = aladin; - this.view = view; - this.isChecked = false; - - this.mainDiv = document.createElement('div'); - this.mainDiv.style.display = 'none'; - this.mainDiv.classList.add('aladin-box', 'aladin-layerBox', 'aladin-cb-list'); - - this.aladinDiv = parentDiv; - parentDiv.appendChild(this.mainDiv); - - this._createComponent(); - this._addListeners(); - } - - _createComponent() { - let self = this; - - // first, update - let layerBox = $(this.mainDiv); - layerBox.empty(); - - layerBox.append( - '×' - ) - - // Coordinates grid plot - let labelCoordinatesGridCb = $('
Coo grid options
'); - let cooGridOptions = $('
Color
Opacity
Thickness
Label size
'); - layerBox.append(labelCoordinatesGridCb).append(cooGridOptions); - - let gridColorInput = cooGridOptions.find('input[type="color"]'); - let gridOpacityInput = cooGridOptions.find('.opacity'); - let gridThicknessInput = cooGridOptions.find('.thickness'); - gridThicknessInput.on('input', () => { - const thickness = +gridThicknessInput.val(); - self.view.setGridConfig({ - thickness: thickness - }); - }); - - let updateGridcolor = function () { - let rgb = Color.hexToRgb(gridColorInput.val()); - let opacity = gridOpacityInput.val(); - self.view.setGridConfig({ - color: { r: rgb.r / 255.0, g: rgb.g / 255.0, b: rgb.b / 255.0 }, - opacity: parseFloat(opacity) - }); - }; - gridColorInput.on('input', updateGridcolor); - gridOpacityInput.on('input', updateGridcolor); - let gridLabelSizeInput = cooGridOptions.find('.label-size'); - gridLabelSizeInput.on('input', function () { - const size = +gridLabelSizeInput.val(); - self.view.setGridConfig({ - labelSize: size - }); - }); - - // coordinates grid - add event listeners - ALEvent.COO_GRID_ENABLED.listenedBy(self.aladinDiv, function () { - self.isChecked = !self.isChecked; - }); - - ALEvent.COO_GRID_DISABLED.listenedBy(self.aladinDiv, function () { - self.isChecked = !self.isChecked; - }); - - ALEvent.COO_GRID_UPDATED.listenedBy(self.aladinDiv, function (e) { - let opacity = e.detail.opacity; - - if (gridOpacityInput.val() != opacity) { - gridOpacityInput.val(opacity); - } - - let color = e.detail.color; - let hexColor = Color.rgbToHex(Math.round(255 * color.r), Math.round(255 * color.g), Math.round(255 * color.b)); - if (gridColorInput.val() != hexColor) { - gridColorInput.val(hexColor); - } - }); - - layerBox.find('.aladin-closeBtn').click(function () { self.aladin.hideBoxes(); return false; }); - } - - _addListeners() { - } - - show() { - this.mainDiv.style.display = 'block'; - } - - hide() { - this.mainDiv.style.display = 'none'; - } - } - \ No newline at end of file diff --git a/src/js/gui/CtxMenu/GridSettings.js b/src/js/gui/CtxMenu/GridSettings.js new file mode 100644 index 000000000..aa5343863 --- /dev/null +++ b/src/js/gui/CtxMenu/GridSettings.js @@ -0,0 +1,160 @@ +// 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. +// + + + +/****************************************************************************** + * Aladin Lite project + * + * File GridSettingsCtxMenu + * + * Author: Matthieu Baumann [CDS] + * + *****************************************************************************/ + +import { ALEvent } from "../../events/ALEvent.js"; +import { Input } from "../Widgets/Input.js"; +import { ActionButton } from "../Widgets/ActionButton.js"; +import { Color } from "../../Color.js"; +import thicknessLineIcon from './../../../../assets/icons/thickness.svg'; + +export let GridSettingsCtxMenu = (function () { + + let GridSettingsCtxMenu = {}; + + GridSettingsCtxMenu.getLayout = function (aladin) { + let colorInput = Input.color({ + name: 'gridColor', + type: 'color', + value: (() => { + let c = aladin.getGridOptions().color; + const cHex = Color.rgbToHex(c.r * 255, c.g * 255, c.b * 255) + return cHex; + })(), + change(e) { + aladin.setCooGrid({color: e.target.value}) + } + }); + + let opacitySlider = Input.slider({ + name: 'opacity', + type: 'range', + min: 0.0, + max: 1.0, + value: aladin.getGridOptions().opacity, + change(e) { + aladin.setCooGrid({opacity: +e.target.value}) + } + }); + + const labelSizeSlider = Input.slider({ + name: 'labelSize', + type: 'range', + tooltip: { + content: 'size' + }, + min: 0.0, + max: 1.0, + value: 0.5, + change(e) { + aladin.setCooGrid({labelSize: Math.round(+e.target.value * 20)}) + } + }); + + const thicknessLineBtn = ActionButton.createSmallSizedIconBtn({ + icon: { + url: thicknessLineIcon, + monochrome: true, + }, + tooltip: {content: 'Grid line thickness', position: {direction: 'left'}}, + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + cursor: 'pointer', + width: '20px', + height: '20px', + padding: '0', + }, + action(e) { + let ctxMenu = ContextMenu.getInstance(aladin); + ctxMenu._hide(); + + let ctxMenuLayout = []; + for (let thickness = 1; thickness <= 5; thickness++) { + ctxMenuLayout.push({ + label: thickness + 'px', + action(o) { + aladin.setCooGrid({thickness: thickness}) + } + }) + } + + ctxMenu.attach(ctxMenuLayout); + ctxMenu.show({ + e: e, + position: { + nextTo: thicknessLineBtn, + direction: 'bottom', + } + }) + } + }); + + let enableCheckbox = Input.checkbox({ + name: 'enableGrid', + tooltip: {content: 'Enable/disable the grid', position: {direction: 'left'}}, + type: 'checkbox', + checked: aladin.getGridOptions().enabled, + click(e) { + aladin.setCooGrid({enabled: enableCheckbox.get()}) + } + }); + + ALEvent.COO_GRID_UPDATED.listenedBy(aladin.aladinDiv, function (e) { + let color = e.detail.color; + + let hexColor = Color.rgbToHex(Math.round(255 * color.r), Math.round(255 * color.g), Math.round(255 * color.b)); + colorInput.set(hexColor) + }); + + return { + label: 'Grid', + subMenu: [ + { + label: { + content: [colorInput, 'Color '], + }, + }, + { + label: { + content: ['Opacity ', opacitySlider], + }, + }, + { + label: { + content: ['Label', labelSizeSlider] + }, + } + ] + } + } + + return GridSettingsCtxMenu; + +})(); diff --git a/src/js/gui/CtxMenu/OverlayStack.js b/src/js/gui/CtxMenu/OverlayStack.js new file mode 100644 index 000000000..41931c9cb --- /dev/null +++ b/src/js/gui/CtxMenu/OverlayStack.js @@ -0,0 +1,906 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ +import { CatalogQueryBox } from "../Box/CatalogQueryBox.js"; + import { ALEvent } from "../../events/ALEvent.js"; + import { Layout } from "../Layout.js"; + import { ContextMenu } from "../Widgets/ContextMenu.js"; + import { ActionButton } from "../Widgets/ActionButton.js"; + import { AladinUtils } from "../../AladinUtils.js"; +import A from "../../A.js"; +import { Utils } from "../../../js/Utils"; +import { View } from "../../View.js"; +import { LayerEditBox } from "../Box/SurveyEditBox.js"; +import { HiPSSelectorBox } from "../Box/HiPSSelectorBox.js"; +import searchIconUrl from '../../../../assets/icons/search.svg'; +import showIconUrl from '../../../../assets/icons/show.svg'; +import hideIconUrl from '../../../../assets/icons/hide.svg'; +import removeIconUrl from '../../../../assets/icons/remove.svg'; +import editIconUrl from '../../../../assets/icons/edit.svg'; +import { ImageFITS } from "../../ImageFITS.js"; +import { ImageLayer } from "../../ImageLayer.js"; +import searchIconImg from '../../../../assets/icons/search.svg'; +import { TogglerActionButton } from "../Button/Toggler.js"; +import { Icon } from "../Widgets/Icon.js"; + +export class OverlayStack extends ContextMenu { + static previewImagesUrl = { + 'AllWISE color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_allWISE_color.jpg', + 'DSS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_DSS2_color.jpg', + 'DSS2 Red (F+R)': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_DSS2_red.jpg', + 'Fermi color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Fermi_color.jpg', + 'GALEXGR6_7 NUV': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_GALEXGR6_7_color.jpg', + 'GLIMPSE360': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_GLIMPSE360.jpg', + 'Halpha': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_VTSS_Ha.jpg', + 'IRAC color I1,I2,I4 - (GLIMPSE, SAGE, SAGE-SMC, SINGS)': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SPITZER_color.jpg', + 'IRIS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_IRIS_color.jpg', + 'Mellinger colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Mellinger_color.jpg', + 'PanSTARRS DR1 color': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_PanSTARRS_DR1_color-z-zg-g.jpg', + '2MASS colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_2MASS_color.jpg', + 'AKARI colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_AKARI_FIS_Color.jpg', + 'SWIFT': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SWIFT_BAT_FLUX.jpg', + 'VTSS-Ha': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_Finkbeiner.jpg', + 'XMM PN colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_XMM_PN_color.jpg', + 'SDSS9 colored': 'https://aladin.cds.unistra.fr/AladinLite/survey-previews/P_SDSS9_color.jpg', + }; + static predefinedCats = { + simbad: {url: 'https://axel.u-strasbg.fr/HiPSCatService/SIMBAD', options: {id: 'simbad', name: 'SIMBAD', shape: 'circle', sourceSize: 8, color: '#318d80', onClick: 'showTable'}}, + gaia: {url: 'https://axel.u-strasbg.fr/HiPSCatService/I/355/gaiadr3', options: {id: 'gaia-dr3', name: 'Gaia DR3', shape: 'square', sourceSize: 8, color: '#6baed6', onClick: 'showTable'}}, + twomass: {url: 'https://axel.u-strasbg.fr/HiPSCatService/II/246/out', options: {id: '2mass', name: '2MASS', shape: 'plus', sourceSize: 8, color: '#dd2233', onClick: 'showTable'}} + }; + // Constructor + constructor(aladin) { + let self; + super(aladin, {hideOnClick: (e) => { + if (self.mode === 'stack') { + self._hide(); + } + }}); + self = this; + this.aladin = aladin; + + this.mode = 'stack'; + + this._addListeners(); + + this.mocHiPSUrls = {} + } + + _addListeners() { + let self = this; + + let updateOverlayList = () => { + // If it is shown, update it + if (!self.isHidden) { + // show will update the content of the stack + self._show(); + } + }; + + ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_ADDED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_RENAMED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_SWAP.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + ALEvent.HIPS_LAYER_REMOVED.listenedBy(this.aladin.aladinDiv, function (e) { + updateOverlayList(); + }); + + updateOverlayList(); + } + + attach() { + let self = this; + + const overlays = Array.from(this.aladin.getOverlays()).reverse().map((overlay) => { + return overlay; + }); + const layers = Array.from(self.aladin.getImageOverlays()).reverse().map((name) => { + let overlay = self.aladin.getOverlayImageLayer(name); + return overlay; + }); + + + let layout = [{ + label: 'Add overlay', + subMenu: [ + { + label: 'Catalogue', + subMenu: [ + { + label: { + icon: { + url: 'https://aladin.cds.unistra.fr/AladinLite/logos/SIMBAD.svg', + cssStyle: { + width: '3rem', + height: '3rem', + cursor: 'help', + }, + action(o) { + window.open('https://simbad.cds.unistra.fr/simbad/') + } + }, + content: 'database', + tooltip: {content: 'Click to go to the SIMBAD database', position: {direction: 'bottom'}}, + + }, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStack.predefinedCats.simbad.url, OverlayStack.predefinedCats.simbad.options); + self.aladin.addCatalog(simbadHiPS); + + self.mode = 'stack'; + } + }, + { + label: 'Gaia DR3', + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStack.predefinedCats.gaia.url, OverlayStack.predefinedCats.gaia.options); + self.aladin.addCatalog(simbadHiPS); + + self.mode = 'stack'; + } + }, + { + label: '2MASS', + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + const simbadHiPS = A.catalogHiPS(OverlayStack.predefinedCats.twomass.url, OverlayStack.predefinedCats.twomass.options); + self.aladin.addCatalog(simbadHiPS); + + self.mode = 'stack'; + } + }, + ContextMenu.fileLoaderItem({ + label: 'From a VOTable File', + accept: '.xml,.vot', + action(file) { + let url = URL.createObjectURL(file); + + A.catalogFromURL( + url, + {onClick: 'showTable'}, + (catalog) => { + self.aladin.addCatalog(catalog) + }, + e => alert(e) + ); + } + }), + { + label: { + icon: { + url: searchIconImg, + monochrome: true, + tooltip: {content: 'Find a specific catalogue
in our database...', position: { direction: 'top' }}, + cssStyle: { + cursor: 'help', + }, + }, + content: 'More...' + }, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + let catBox = CatalogQueryBox.getInstance(self.aladin); + catBox._show({position: self.position}); + + self.mode = 'search'; + } + }, + ] + }, + { + label: { + icon: { + url: Icon.dataURLFromSVG({svg: Icon.SVG_ICONS.MOC}), + size: 'small', + tooltip: {content: 'Define a selection coverage', position: {direction: 'bottom'}}, + monochrome: true, + cssStyle: { + cursor: 'pointer', + }, + }, + content: 'MOC' + }, + subMenu: [ + ContextMenu.fileLoaderItem({ + label: 'FITS File', + accept: '.fits', + action(file) { + let url = URL.createObjectURL(file); + + let moc = A.MOCFromURL( + url, + {name: file.name, lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } + }), + { + label: 'From selection', + subMenu: [ + { + label: '◌ Circle', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.preventDefault(); + o.stopPropagation(); + + self._hide(); + + self.aladin.select('circle', c => { + let [ra, dec] = self.aladin.pix2world(c.x, c.y); + let radius = self.aladin.angularDist(c.x, c.y, c.x + c.r, c.y); + + if (ra && dec && radius) { + let moc = A.MOCFromCircle( + {ra, dec, radius}, + {name: 'cone', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } else { + alert('The circle selection might be invalid. ra: ' + ra + 'deg, dec: ' + dec + 'deg, radius: ' + radius + 'deg') + } + }) + } + }, + { + label: '⬚ Rect', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + self.aladin.select('rect', r => { + try { + let [ra1, dec1] = self.aladin.pix2world(r.x, r.y); + let [ra2, dec2] = self.aladin.pix2world(r.x + r.w, r.y); + let [ra3, dec3] = self.aladin.pix2world(r.x + r.w, r.y + r.h); + let [ra4, dec4] = self.aladin.pix2world(r.x, r.y + r.h); + + let moc = A.MOCFromPolygon( + { + ra: [ra1, ra2, ra3, ra4], + dec: [dec1, dec2, dec3, dec4] + }, + {name: 'rect', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + } catch(_) { + alert('Selection covers a region out of the projection definition domain.'); + } + }) + } + }, + { + label: '⛉ Polygon', + disabled: self.aladin.view.mode !== View.PAN ? { + reason: 'Exit your current mode
(e.g. disable the SIMBAD pointer mode)' + } : false, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + self.aladin.select('poly', p => { + try { + let ra = [] + let dec = [] + for (const v of p.vertices) { + let [lon, lat] = self.aladin.pix2world(v.x, v.y); + ra.push(lon) + dec.push(lat) + } + + let moc = A.MOCFromPolygon( + {ra, dec}, + {name: 'poly', lineWidth: 3.0}, + ); + self.aladin.addMOC(moc) + + } catch(_) { + alert('Selection covers a region out of the projection definition domain.'); + } + }) + } + }, + ] + } + ] + } + ] + }]; + + for(const overlay of overlays) { + const name = overlay.name; + let cssStyle = { + height: 'fit-content', + }; + let showBtn = new ActionButton({ + size: 'small', + icon: { + url: overlay.isShowing ? showIconUrl : hideIconUrl, + monochrome: true, + }, + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + tooltip: {content: overlay.isShowing ? 'Hide' : 'Show', position: {direction: 'bottom'}}, + action(e, btn) { + if (overlay.isShowing) { + overlay.hide() + btn.update({icon: {monochrome: true, url: hideIconUrl}, tooltip: {content: 'Show'}}); + } else { + overlay.show() + btn.update({icon: {monochrome: true, url: showIconUrl}, tooltip: {content: 'Hide'}}); + } + } + }); + + let deleteBtn = new ActionButton({ + icon: { + url: removeIconUrl, + monochrome: true, + }, + size: 'small', + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + tooltip: { + content: 'Remove', + position: {direction: 'bottom'} + }, + action(e) { + self.aladin.removeLayer(overlay) + } + }); + + let item = Layout.horizontal({ + layout: [ + this._addOverlayIcon(overlay), + '
' + name + '
', + Layout.horizontal({layout: [showBtn, deleteBtn]}) + ], + cssStyle: { + textAlign: 'center', + } + }); + + if(!Utils.hasTouchScreen()) { + layout.push({ + label: item, + cssStyle, + hover(e) { + showBtn.el.style.visibility = 'visible' + deleteBtn.el.style.visibility = 'visible' + }, + unhover(e) { + showBtn.el.style.visibility = 'hidden' + deleteBtn.el.style.visibility = 'hidden' + }, + }) + } else { + layout.push({ + label: item, + cssStyle + }) + } + } + + layout.push({ + label: 'Add survey', + subMenu: [ + { + label: { + icon: { + url: searchIconUrl, + monochrome: true, + tooltip: {content: 'From our database...', position: { direction: 'right' }}, + cssStyle: { + cursor: 'help', + }, + }, + content: 'Search for a survey' + }, + action: (e) => { + e.stopPropagation(); + e.preventDefault(); + + self._hide(); + + let hipsSelectorBox = HiPSSelectorBox.getInstance(self.aladin); + // attach a callback + hipsSelectorBox.attach( + (HiPSId) => { + let name = Utils.uuidv4() + self.aladin.setOverlayImageLayer(HiPSId, name) + + self.mode = 'stack'; + self._show(); + } + ); + + hipsSelectorBox._show({ + position: self.position, + }); + + self.mode = 'hips'; + } + }, + ContextMenu.fileLoaderItem({ + label: 'FITS image file', + accept: '.fits', + action(file) { + let url = URL.createObjectURL(file); + + const image = self.aladin.createImageFITS( + url, + file.name, + undefined, + (ra, dec, fov, _) => { + // Center the view around the new fits object + self.aladin.gotoRaDec(ra, dec); + self.aladin.setFoV(fov * 1.1); + //self.aladin.selectLayer(image.layer); + }, + undefined + ); + + self.aladin.setOverlayImageLayer(image, Utils.uuidv4()) + } + }), + ] + }) + + // survey list + let selectedLayer = self.aladin.getSelectedLayer(); + + /*if (!layers) { + super.attach(layout); + return; + }*/ + + const defaultLayers = ImageLayer.LAYERS.sort(function (a, b) { + if (!a.order) { + return a.name > b.name ? 1 : -1; + } + return a.maxOrder && a.maxOrder > b.maxOrder ? 1 : -1; + }); + + for(const layer of layers) { + let backgroundUrl = layer.properties.url + '/preview.jpg'; + let cssStyle = { + height: 'fit-content', + }; + + if (backgroundUrl) { + cssStyle = { + backgroundSize: '100%', + backgroundImage: 'url(' + backgroundUrl + ')', + ...cssStyle + } + } + + let showBtn = ActionButton.createSmallSizedIconBtn({ + icon: { + url: layer.getOpacity() === 0.0 ? hideIconUrl : showIconUrl, + monochrome: true, + }, + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + tooltip: {content: layer.getOpacity() === 0.0 ? 'Show' : 'Hide', position: {direction: 'bottom'}}, + action(e, btn) { + e.preventDefault(); + e.stopPropagation(); + + let opacity = layer.getOpacity(); + if (opacity === 0.0) { + layer.setOpacity(1.0); + btn.update({icon: {monochrome: true, url: showIconUrl}, tooltip: {content: 'Hide'}}); + } else { + layer.setOpacity(0.0); + btn.update({icon: {monochrome: true, url: hideIconUrl}, tooltip: {content: 'Show'}}); + } + } + }); + + let deleteBtn = ActionButton.createSmallSizedIconBtn({ + icon: {url: removeIconUrl, monochrome: true}, + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + disable: layer.layer === 'base', + tooltip: {content: 'Remove', position: {direction: 'bottom'}}, + action(e) { + self.aladin.removeImageLayer(layer.layer); + } + }); + + let editBtn = ActionButton.createSmallSizedIconBtn({ + icon: {url: editIconUrl, monochrome: true}, + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + tooltip: {content: 'Edit', position: {direction: 'bottom'}}, + action: (e) => { + e.stopPropagation(); + e.preventDefault(); + + self._hide(); + + self.aladin.selectLayer(layer.layer); + self.attach() + + let editBox = LayerEditBox.getInstance(self.aladin); + editBox.update({layer}) + editBox._show({position: self.position}); + + self.mode = 'edit'; + } + }); + + let loadMOCBtn = new TogglerActionButton({ + size: 'small', + cssStyle: { + visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', + }, + icon: {url: Icon.dataURLFromSVG({svg: Icon.SVG_ICONS.MOC}), monochrome: true}, + tooltip: {content: 'Add coverage', position: {direction: 'bottom'}}, + toggled: (() => { + let overlays = self.aladin.getOverlays(); + let found = overlays.find((o) => o.type === "moc" && o.name === layer.properties.obsTitle); + return found !== undefined; + })(), + actionOn: (e) => { + let moc = A.MOCFromURL(layer.properties.url + '/Moc.fits', {lineWidth: 3, name: layer.properties.obsTitle}); + self.aladin.addMOC(moc); + + self.mocHiPSUrls[layer.properties.url] = moc; + loadMOCBtn.update({tooltip: {content: 'Remove coverage', position: {direction: 'bottom'}}}) + + if (self.aladin.statusBar) { + self.aladin.statusBar.appendMessage({ + message: 'Coverage of ' + layer.properties.obsTitle + ' loaded', + duration: 2000, + type: 'info' + }) + } + }, + actionOff: (e) => { + let moc = self.mocHiPSUrls[layer.properties.url]; + self.aladin.removeLayer(moc) + + delete self.mocHiPSUrls[layer.properties.url]; + + loadMOCBtn.update({tooltip: {content: 'Add coverage', position: {direction: 'bottom'}}}) + + if (self.aladin.statusBar) { + self.aladin.statusBar.appendMessage({ + message: 'Coverage of ' + layer.properties.obsTitle + ' removed', + duration: 2000, + type: 'info' + }) + } + } + }); + + let layerClassName = 'a' + layer.layer.replace(/[.\/ ]/g, '') + + let btns = [showBtn, editBtn]; + + if (layer.subtype !== 'fits') { + btns.push(loadMOCBtn) + } + btns.push(deleteBtn) + + let item = Layout.horizontal({ + layout: [ + '
' + (layer.name) + '
', + Layout.horizontal({layout: btns}) + ], + cssStyle: { + display: 'flex', + alignItems: 'center', + listStyle: 'none', + justifyContent: 'space-between', + width: '100%', + } + }); + + let l = { + label: item, + classList: 'surveyItem', + cssStyle, + hover(e) { + showBtn.el.style.visibility = 'visible' + editBtn.el.style.visibility = 'visible' + deleteBtn.el.style.visibility = 'visible' + loadMOCBtn.el.style.visibility = 'visible' + }, + unhover(e) { + showBtn.el.style.visibility = 'hidden' + editBtn.el.style.visibility = 'hidden' + deleteBtn.el.style.visibility = 'hidden' + loadMOCBtn.el.style.visibility = 'hidden' + } + }; + + l.subMenu = []; + + for(let ll of defaultLayers) { + backgroundUrl = OverlayStack.previewImagesUrl[ll.name]; + if (!backgroundUrl) { + backgroundUrl = ll.url + '/preview.jpg' + } + let cssStyle = { + height: '2.5em', + }; + if (backgroundUrl) { + cssStyle = { + backgroundSize: '100%', + backgroundImage: 'url(' + backgroundUrl + ')', + ...cssStyle + } + } + + l.subMenu.push({ + //selected: layer.name === aladin.getBaseImageLayer().name, + label: '
' + ll.name + '
', + cssStyle: cssStyle, + action(e) { + let cfg = ImageLayer.LAYERS.find((l) => l.name === ll.name); + let newLayer; + + // Max order is specific for surveys + if (cfg.subtype === "fits") { + // FITS + newLayer = self.aladin.createImageFITS( + cfg.url, + cfg.name, + cfg.options, + ); + } else { + // HiPS + newLayer = self.aladin.createImageSurvey( + cfg.id, + cfg.name, + cfg.url, + undefined, + cfg.maxOrder, + cfg.options + ); + } + + self.aladin.setOverlayImageLayer(newLayer, layer.layer); + //self._hide(); + }, + hover(e, item) { + item.style.filter = 'brightness(1.5)'; + }, + unhover(e, item) { + item.style.filter = 'brightness(1.0)'; + } + }) + } + + l.subMenu.push({ + label: { + icon: { + url: searchIconImg, + monochrome: true, + tooltip: {content: 'Find a specific survey
in our database...', position: { direction: 'top' }}, + cssStyle: { + cursor: 'help', + }, + }, + content: 'More...' + }, + action(o) { + o.stopPropagation(); + o.preventDefault(); + + self._hide(); + + let hipsBox = HiPSSelectorBox.getInstance(self.aladin) + + hipsBox.attach( + (HiPSId) => { + self.aladin.setOverlayImageLayer(HiPSId, layer.layer); + self.mode = 'stack'; + self._show(); + } + ); + + hipsBox._show({ + position: self.position, + }) + + self.mode = 'hips'; + } + }) + + l.action = (o) => { + let oldLayerClassName = 'a' + self.aladin.getSelectedLayer().replace(/[.\/ ]/g, '') + self.el.querySelector('.' + oldLayerClassName).style.removeProperty('border') + self.aladin.selectLayer(layer.layer); + self.el.querySelector('.' + layerClassName).style.border = '1px solid white'; + } + + layout.push(l); + } + + super.attach(layout); + } + + _findPreviewImageUrl(layer) { + if (layer instanceof ImageFITS) { + return; + } + + if (!layer.properties || !layer.properties.creatorDid) { + return; + } + + const creatorDid = layer.properties.creatorDid; + + for (const key in Stack.previewImagesUrl) { + if (creatorDid.includes(key)) { + return Stack.previewImagesUrl[key]; + } + } + // if not found + return layer.properties.url + '/preview.jpg' + } + + _addOverlayIcon(overlay) { + var tooltipText; + var svg = ''; + if (overlay.type == 'catalog' || overlay.type == 'progressivecat') { + var nbSources = overlay.getSources().length; + tooltipText = nbSources + ' source' + (nbSources > 1 ? 's' : ''); + + svg = Icon.SVG_ICONS.CATALOG; + } + else if (overlay.type == 'moc') { + tooltipText = 'Coverage: ' + (100 * overlay.skyFraction()).toFixed(2) + ' % of sky'; + + svg = Icon.SVG_ICONS.MOC; + } + else if (overlay.type == 'overlay') { + svg = Icon.SVG_ICONS.OVERLAY; + } + + let tooltip; + if (tooltipText) { + tooltip = { content: tooltipText, position: {direction: 'bottom'} } + } + + // retrieve SVG icon, and apply the layer color + return new Icon({ + size: 'small', + url: Icon.dataURLFromSVG({svg, color: overlay.color}), + tooltip + }); + } + + _show(options) { + this.attach(); + + this.position = (options && options.position) || this.position || { anchor: 'center center'}; + super.show({ + ...options, + ...{position: this.position}, + cssStyle: { + backgroundColor: 'black', + maxWidth: '20em', + //border: '1px solid white', + } + }) + + this.element().querySelectorAll(".surveyItem") + .forEach((surveyItem) => { + surveyItem.querySelectorAll(".aladin-context-sub-menu") + // skip the first menu + .forEach((subMenu) => { + subMenu.style.maxHeight = '50vh'; + subMenu.style.overflowY = 'scroll'; + }) + }) + + } + + _hide() { + let catBox = CatalogQueryBox.getInstance(this.aladin); + catBox._hide(); + + let editBox = LayerEditBox.getInstance(this.aladin); + editBox._hide(); + + let hipsSelectorBox = HiPSSelectorBox.getInstance(this.aladin); + hipsSelectorBox._hide(); + + this.mode = 'stack'; + + super._hide(); + } + + static singleton; + + static getInstance(aladin) { + if (!OverlayStack.singleton) { + OverlayStack.singleton = new OverlayStack(aladin); + } + + return OverlayStack.singleton; + } +} diff --git a/src/js/gui/CtxMenu/Projection.js b/src/js/gui/CtxMenu/Projection.js new file mode 100644 index 000000000..6d7c878c6 --- /dev/null +++ b/src/js/gui/CtxMenu/Projection.js @@ -0,0 +1,70 @@ +// 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. +// + + + +/****************************************************************************** + * Aladin Lite project + * + * File GridSettingsCtxMenu + * + * Author: Matthieu Baumann [CDS] + * + *****************************************************************************/ + +import { ProjectionEnum } from "../../ProjectionEnum"; + +export let ProjectionCtxMenu = (function () { + + let ProjectionCtxMenu = {}; + + ProjectionCtxMenu.getLayout = function (aladin) { + + /*ALEvent.COO_GRID_UPDATED.listenedBy(aladin.aladinDiv, function (e) { + let color = e.detail.color; + + let hexColor = Color.rgbToHex(Math.round(255 * color.r), Math.round(255 * color.g), Math.round(255 * color.b)); + colorInput.set(hexColor) + });*/ + let layout = []; + + let aladinProj = aladin.getProjectionName(); + for (const key in ProjectionEnum) { + let proj = ProjectionEnum[key]; + layout.push({ + label: proj.label, + selected: aladinProj === key, + action(o) { + aladin.setProjection(key) + } + }) + } + + return { + label: { + content: 'Projection', + tooltip: { content: ProjectionEnum[aladinProj].label + ' selected', position: { direction: 'bottom'} } + }, + subMenu: layout + }; + } + + return ProjectionCtxMenu; + +})(); diff --git a/src/js/gui/CtxMenu/Settings.js b/src/js/gui/CtxMenu/Settings.js new file mode 100644 index 000000000..db42a5d16 --- /dev/null +++ b/src/js/gui/CtxMenu/Settings.js @@ -0,0 +1,314 @@ +// Copyright 2013 - UDS/CNRS +// The Aladin Lite program is distributed under the terms +// of the GNU General Public License version 3. +// +// This file is part of Aladin Lite. +// +// Aladin Lite is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Aladin Lite is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// The GNU General Public License is available in COPYING file +// along with Aladin Lite. +// + + +/****************************************************************************** + * Aladin Lite project + * + * File gui/Stack/Menu.js + * + * + * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr] + * + *****************************************************************************/ + +import { Layout } from "../Layout.js"; +import { ContextMenu } from "../Widgets/ContextMenu.js"; +import { Input } from "../Widgets/Input.js"; +import { Color } from "../../Color.js"; +import { ALEvent } from "../../events/ALEvent.js"; +import { SAMPActionButton } from "../Button/SAMP.js"; +import helpIconBtn from '../../../../assets/icons/help.svg'; +import { Utils } from "../../Utils"; +import { GridSettingsCtxMenu } from "./GridSettings.js"; + +export class SettingsCtxMenu extends ContextMenu { + // Constructor + constructor(aladin, menu) { + super(aladin, {hideOnClick: true}); + let self = this; + self.backgroundColorInput = Input.color({ + name: 'color', + value: (() => { + let {r, g, b} = aladin.getBackgroundColor(); + return Color.rgbToHex(r, g, b); + })(), + change(e) { + let hex = e.target.value; + aladin.setBackgroundColor(hex) + } + }); + + let reticleColor = new Color(aladin.getReticle().getColor()) + self.reticleColorInput = Input.color({ + value: reticleColor.toHex(), + name: 'reticleColor', + change(e) { + let hex = e.target.value; + let reticle = aladin.getReticle(); + reticle.update({color: hex}) + } + }); + + // Event received from aladin + ALEvent.BACKGROUND_COLOR_CHANGED.listenedBy(aladin.aladinDiv, function (e) { + const {r, g, b} = e.detail.color; + + let hex = Color.rgbToHex(r, g, b); + self.backgroundColorInput.set(hex) + }); + ALEvent.RETICLE_CHANGED.listenedBy(aladin.aladinDiv, function (e) { + const color = e.detail.color; + let hex = new Color(color).toHex(); + + self.reticleColorInput.set(hex) + }); + + this.toggleCheckbox = (checkbox) => { + const pastVal = checkbox.get(); + const curVal = !pastVal; + + checkbox.set(curVal) + + return curVal; + }; + + self.hpxGridCheckbox = Input.checkbox({ + name: 'hpxgrid', checked: this.aladin.healpixGrid(), + click(e) { + let newVal = self.toggleCheckbox(self.hpxGridCheckbox); + self.aladin.showHealpixGrid(newVal) + } + }) + self.reticleCheckbox = Input.checkbox({ + name: 'reticle', + checked: this.aladin.isReticleDisplayed(), + click(e) { + let newVal = self.toggleCheckbox(self.reticleCheckbox); + self.aladin.showReticle(newVal) + } + }) + + this.menu = menu; + + let sampBtn = new SAMPActionButton({ + size: 'small', + action(conn) { + if (conn.isConnected()) { + conn.unregister(); + } else { + conn.register(); + } + + self._hide() + } + }, aladin); + this.sampBtn = sampBtn; + + this._attach(); + } + + _attach() { + const toggleWindow = (window) => { + let windowShown = self.menu.isShown(window); + if(windowShown) { + self.menu.disable(window) + } else { + self.menu.enable(window) + } + } + + let self = this; + + let reticle = self.aladin.getReticle(); + + let sliderReticleSize = Input.slider({ + name: 'reticleSize', + type: 'range', + min: 0.0, + max: 50, + value: reticle.getSize(), + change(e) { + reticle.update({size: e.target.value}) + } + }); + + this.attach([ + //ProjectionCtxMenu.getLayout(self.aladin), + GridSettingsCtxMenu.getLayout(self.aladin), + { + label: 'Reticle', + subMenu: [ + { + label: { + content: [self.reticleCheckbox, 'Show/Hide'] + }, + mustHide: false, + action(o) { + let newVal = self.toggleCheckbox(self.reticleCheckbox); + self.aladin.showReticle(newVal) + + self._attach(); + } + }, + { + label: { + content: [self.reticleColorInput, 'Color'] + } + }, + { + label: Layout.horizontal(['Size', sliderReticleSize]), + } + ] + }, + { + label: { + content: [self.backgroundColorInput, 'Back color'] + }, + }, + { + label: { + content: [self.hpxGridCheckbox, 'HEALPix grid'] + }, + mustHide: false, + action(o) { + let newVal = self.toggleCheckbox(self.hpxGridCheckbox); + self.aladin.showHealpixGrid(newVal) + + self._attach(); + } + }, + { + label: { + content: [self.sampBtn, 'SAMP'] + }, + }, + { + label: 'Features', + subMenu: [ + { + label: 'Stack', + selected: self.menu.isShown('overlay'), + action(o) { + toggleWindow('overlay') + } + }, + { + label: 'Simbad', + selected: self.menu.isShown('simbad'), + action(o) { + toggleWindow('simbad'); + } + }, + { + label: 'Grid', + selected: self.menu.isShown('grid'), + action(o) { + toggleWindow('grid'); + } + } + ] + }, + { + label: { + icon: { + monochrome: true, + tooltip: {content: 'Documentation about Aladin Lite', position: {direction: 'top'}}, + url: helpIconBtn, + size: 'small', + cssStyle: { + cursor: 'help', + } + }, + content: 'Help' + }, + subMenu: [ + { + label: 'Aladin Lite API', + action(o) { + Utils.openNewTab('https://aladin.cds.unistra.fr/AladinLite/doc/API/') + } + }, + { + label: { + content: 'Contact us', + tooltip: { content: 'For bug reports, discussions, feature ideas...', position: {direction: 'bottom'} } + }, + subMenu: [ + { + label: 'GitHub', + action(o) { + Utils.openNewTab('https://github.com/cds-astro/aladin-lite/issues') + } + }, + { + label: 'by email', + action(o) { + Utils.openNewTab('mailto:matthieu.baumann@astro.unistra.fr,thomas.boch@astro.unistra.fr?subject=Aladin Lite issue&body=message%20goes%20here') + } + } + ], + }, + { + label: 'General documentation', + + action(o) { + Utils.openNewTab('https://aladin.cds.unistra.fr/AladinLite/doc/') + } + }, + { + label: Layout.horizontal({layout: ['Examples'], tooltip: { content: 'How to embed Aladin Lite
into your own webpages!', position: {direction: 'bottom'}}}), + action(o) { + Utils.openNewTab('https://aladin.cds.unistra.fr/AladinLite/doc/API/examples/') + } + } + ] + } + ]); + } + + _hide() { + this._attach() + + super._hide(); + } + + _show(options) { + this.position = (options && options.position) || this.position || { anchor: 'center center'}; + + super.show({ + position: this.position, + cssStyle: { + backgroundColor: 'black', + maxWidth: '20em', + } + }) + } + + static singleton; + + static getInstance(aladin, menu) { + if (!Settings.singleton) { + Settings.singleton = new Settings(aladin, menu); + } + + return Settings.singleton; + } +} + \ No newline at end of file diff --git a/src/js/gui/FoV.js b/src/js/gui/FoV.js new file mode 100644 index 000000000..b5a444c30 --- /dev/null +++ b/src/js/gui/FoV.js @@ -0,0 +1,127 @@ +// 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"; +import { ActionButton } from "./Widgets/ActionButton.js"; + +import plusIconUrl from "../../../assets/icons/plus.svg" +import minusIconUrl from "../../../assets/icons/minus.svg" + +export class FoV extends DOMElement { + // constructor + constructor(aladin) { + let el = Layout.horizontal({ + layout: [ + new ActionButton({ + size: 'small', + icon: { + monochrome: true, + size: 'small', + url: plusIconUrl, + }, + cssStyle: { + marginRight: 0, + borderRight: 'none', + borderRadius: '5px 0px 0px 5px' + }, + action(o) { + aladin.increaseZoom(); + } + }), + new ActionButton({ + size: 'small', + cssStyle: { + borderRadius: '0px 5px 5px 0px' + }, + icon: { + monochrome: true, + size: 'small', + url: minusIconUrl, + }, + action(o) { + aladin.decreaseZoom(); + } + }), + '
', + '
×
', + '
', + ] + }); + el.addClass('aladin-fov'); + el.addClass('aladin-dark-theme') + + 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 deleted file mode 100644 index 4fa139b2e..000000000 --- a/src/js/gui/HiPSLayer.js +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright 2013 - UDS/CNRS -// The Aladin Lite program is distributed under the terms -// of the GNU General Public License version 3. -// -// This file is part of Aladin Lite. -// -// Aladin Lite is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 3 of the License. -// -// Aladin Lite is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// The GNU General Public License is available in COPYING file -// along with Aladin Lite. -// - - -/****************************************************************************** - * Aladin Lite project - * - * File gui/Stack.js - * - * - * Author: Matthieu Baumann[CDS] - * - *****************************************************************************/ - -import { ImageLayer } from "../ImageLayer.js"; -import { ALEvent } from "../events/ALEvent.js"; -import { HiPSSelector } from "./HiPSSelector.js"; - -import $ from 'jquery'; - -export class HiPSLayer { - - // Constructor - constructor(aladin, layer) { - this.aladin = aladin; - this.layer = layer; - this.hidden = false; - this.lastOpacity = 1.0; - - // HiPS header div - this.headerDiv = $( - '
' + - '
' + - '' + - '' + - '' + - '' + - '' + - '
' + - '
' - ); - - // Add a centered on button for images - if (this.layer.subtype === "fits") { - let layerSelector = this.headerDiv[0].querySelector(".aladin-layerSelection"); - layerSelector.after($('')[0]); - } - - 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'; - } - - // HiPS main options div - let cmListStr = ''; - for (const cm of this.aladin.wasm.getAvailableColormapList()) { - cmListStr += ''; - } - - this.cmap = "native"; - this.color = "#ff0000"; - - this.mainDiv = $(''); - - this._addListeners(); - this._updateHiPSLayerOptions(); - - let self = this; - this.layerChangedListener = function(e) { - const layer = e.detail.layer; - if (layer.layer === self.layer.layer) { - // Update the survey to the new one - self.layer = layer; - self._updateHiPSLayerOptions(); - } - self._updateLayersDropdownList(); - }; - ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, this.layerChangedListener); - } - - destroy() { - ALEvent.HIPS_LAYER_CHANGED.remove(this.aladin.aladinDiv, this.layerChangedListener); - } - - _addListeners() { - const self = this; - // HEADER DIV listeners - // Click opener - const clickOpener = this.headerDiv.find('.aladin-indicatorBtn'); - clickOpener.off("click"); - clickOpener.on("click", function () { - if (clickOpener.hasClass('right-triangle')) { - clickOpener.removeClass('right-triangle'); - clickOpener.addClass('down-triangle'); - self.mainDiv.slideDown(300); - } - else { - clickOpener.removeClass('down-triangle'); - clickOpener.addClass('right-triangle'); - self.mainDiv.slideUp(300); - } - }); - - 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); - }); - - // Search HiPS button - const hipsSelector = this.headerDiv.find('.aladin-HiPSSelector'); - hipsSelector.off("click"); - hipsSelector.on("click", function () { - 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(); - }); - - // Delete HiPS button - const deleteLayer = this.headerDiv.find('.aladin-delete-layer'); - deleteLayer.off("click"); - deleteLayer.on("click", function () { - const removeLayerEvent = new CustomEvent('remove-layer', { - detail: self.layer.layer - }); - self.aladin.aladinDiv.dispatchEvent(removeLayerEvent); - }); - - // Hide HiPS button - const hideLayer = this.headerDiv.find('.aladin-layer-hide'); - hideLayer.off("click"); - hideLayer.on("click", function () { - self.hidden = !self.hidden; - let opacitySlider = self.mainDiv.find('.opacity').eq(0); - - let newOpacity = 0.0; - if (self.hidden) { - self.lastOpacity = self.layer.getOpacity(); - hideLayer.text(''); - } else { - newOpacity = self.lastOpacity; - hideLayer.text('👁️'); - } - // Update the opacity slider - opacitySlider.val(newOpacity); - opacitySlider.get(0).disabled = self.hidden; - - self.layer.setOpacity(newOpacity); - }); - - // Hide HiPS button - const focusOnLayer = this.headerDiv.find('.aladin-layer-focuson'); - if (focusOnLayer) { - focusOnLayer.off("click"); - focusOnLayer.on("click", function () { - self.layer.focusOn(); - }); - } - - // 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; - } - 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); - } - }); - - // saturation - const sat4ImgLayer = self.mainDiv.find('.saturation').eq(0); - sat4ImgLayer.off("input"); - sat4ImgLayer.on('input', function (e) { - const saturation = parseFloat(sat4ImgLayer.val()) || 0.0; - - self.layer.setSaturation(saturation); - - const trueSaturation = self.layer.getColorCfg().getSaturation(); - if (saturation !== trueSaturation) { - sat4ImgLayer.val(trueSaturation); - } - }); - - // 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); - } - }); - - // 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); - } - }); - } - - _updateHiPSLayerOptions() { - const colorMapTr = this.mainDiv.find('.row').eq(0); - const reverseTr = this.mainDiv.find('.row').eq(1); - const stretchTr = this.mainDiv.find('.row').eq(2); - const formatTr = this.mainDiv.find('.row').eq(3); - const minCutTr = this.mainDiv.find('.row').eq(4); - const maxCutTr = this.mainDiv.find('.row').eq(5); - - 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($('