From 2aaf917e046d0abb789b0a94e51e8ce1d042c3a8 Mon Sep 17 00:00:00 2001 From: Matthieu Baumann Date: Wed, 29 Nov 2023 09:28:54 +0100 Subject: [PATCH] enhance samp connexion --- assets/icons/telescope.svg | 12 + assets/icons/wave-off.svg | 12 + assets/icons/wave-on.svg | 12 + examples/al-easy-access-simbad-ned.html | 6 +- examples/al-ellipse.html | 4 +- examples/al-obscore.html | 6 +- examples/al-soda-ska.html | 9 +- src/core/al-api/src/catalog.rs | 37 ++ src/core/src/app.rs | 2 + src/core/src/lib.rs | 30 +- src/core/src/renderable/catalog/manager.rs | 87 ++-- src/css/aladin.css | 14 +- src/js/A.js | 1 + src/js/Aladin.js | 38 +- src/js/Catalog.js | 50 ++- src/js/Color.js | 58 +-- src/js/DefaultActionsForContextMenu.js | 48 ++- src/js/FiniteStateMachine.js | 21 + src/js/Footprint.js | 4 +- src/js/MeasurementTable.js | 36 -- src/js/Overlay.js | 8 + src/js/Reticle.js | 35 +- src/js/Selector.js | 281 +++++++++--- src/js/Source.js | 25 +- src/js/View.js | 406 +++++++++--------- src/js/events/ALEvent.js | 7 +- src/js/gui/Button/SAMP.js | 114 +++++ src/js/gui/FoV.js | 2 +- src/js/gui/Layout.js | 3 - src/js/gui/SODAQueryWindow.js | 4 +- src/js/gui/Toolbar/Controls/GotoBox.js | 2 +- src/js/gui/Toolbar/Controls/Overlays/Stack.js | 221 ++++++++++ src/js/gui/Toolbar/Controls/Settings.js | 25 +- .../Toolbar/Controls/StackLayer/EditBox.js | 6 +- .../gui/Toolbar/Controls/StackLayer/Menu.js | 2 +- .../gui/Toolbar/Controls/StackLayer/Stack.js | 2 +- src/js/gui/Toolbar/Menu.js | 28 +- src/js/gui/Utils.js | 26 +- src/js/gui/widgets/ActionButton.js | 5 +- src/js/gui/widgets/Form.js | 4 +- src/js/gui/widgets/Selector.js | 4 +- src/js/gui/widgets/Tab.js | 53 ++- src/js/gui/widgets/Table.js | 47 +- src/js/gui/widgets/Tooltip.js | 7 +- src/js/libs/samp.js | 8 +- src/js/vo/Datalink.js | 15 +- src/js/vo/ObsCore.js | 6 +- src/js/vo/samp.js | 286 +++++++++++- 48 files changed, 1507 insertions(+), 612 deletions(-) create mode 100644 assets/icons/telescope.svg create mode 100644 assets/icons/wave-off.svg create mode 100644 assets/icons/wave-on.svg create mode 100644 src/core/al-api/src/catalog.rs create mode 100644 src/js/FiniteStateMachine.js create mode 100644 src/js/gui/Button/SAMP.js create mode 100644 src/js/gui/Toolbar/Controls/Overlays/Stack.js diff --git a/assets/icons/telescope.svg b/assets/icons/telescope.svg new file mode 100644 index 00000000..9d3ce131 --- /dev/null +++ b/assets/icons/telescope.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ 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 00000000..76d8038d --- /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 00000000..17b3b8b2 --- /dev/null +++ b/assets/icons/wave-on.svg @@ -0,0 +1,12 @@ + + + + radio-waves + + + + + + + + \ No newline at end of file diff --git a/examples/al-easy-access-simbad-ned.html b/examples/al-easy-access-simbad-ned.html index 7f507eef..aec8c4ce 100644 --- a/examples/al-easy-access-simbad-ned.html +++ b/examples/al-easy-access-simbad-ned.html @@ -11,10 +11,10 @@ let aladin; A.init.then(() => { // Start up Aladin Lite - aladin = A.aladin('#aladin-lite-div', {target: 'M 82', fov: 0.25, showContextMenu: true, fullScreen: true}); + aladin = A.aladin('#aladin-lite-div', {target: 'orion', projection: 'CAR', fov: 0.25, showContextMenu: true, fullScreen: true}); - aladin.addCatalog(A.catalogFromSimbad('M 82', 0.1, {onClick: 'showTable'})); - aladin.addCatalog(A.catalogFromNED('09 55 52.4 +69 40 47', 0.1, {onClick: 'showPopup', shape: 'plus'})); + aladin.addCatalog(A.catalogFromSimbad('orion', 10, {onClick: 'showTable', limit: 100000})); + //aladin.addCatalog(A.catalogFromNED('09 55 52.4 +69 40 47', 0.1, {onClick: 'showPopup', shape: 'plus'})); }); diff --git a/examples/al-ellipse.html b/examples/al-ellipse.html index 3281d7d8..398ad324 100644 --- a/examples/al-ellipse.html +++ b/examples/al-ellipse.html @@ -11,7 +11,9 @@ let aladin; A.init.then(() => { // Start up Aladin Lite - aladin = A.aladin('#aladin-lite-div', {survey: "CDS/P/DSS2/color", target: 'M 31', fov: 3}); + aladin = A.aladin('#aladin-lite-div', {vr: {renderer: , animate: }, 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-obscore.html b/examples/al-obscore.html index 635fe042..b3d59d15 100644 --- a/examples/al-obscore.html +++ b/examples/al-obscore.html @@ -10,11 +10,11 @@ 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: '14 18 16.868 +56 44 29.37', 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) => { + /*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); diff --git a/examples/al-soda-ska.html b/examples/al-soda-ska.html index 6d5b6917..1dd98877 100644 --- a/examples/al-soda-ska.html +++ b/examples/al-soda-ska.html @@ -10,12 +10,11 @@ import A from '../src/js/A.js'; let aladin; A.init.then(() => { - aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "m51", fov: 180, projection: 'SIN', showContextMenu: true}); + aladin = A.aladin('#aladin-lite-div', {fullScreen: true, target: "Abell 194", fov: 180, projection: 'SIN', showContextMenu: true}); - const c1 = A.catalogFromSKAORucio("200 50", 90, {onClick: 'showTable'}); - //const c1 = A.catalogFromURL('./ObsCoreRucioScs.xml', {onClick: 'showTable', limit: 1000}); - - aladin.addCatalog(c1); + A.catalogFromSKAORucio("Abell 194", 90, {onClick: 'showTable'}, (cat) => { + aladin.addCatalog(cat); + }); }); diff --git a/src/core/al-api/src/catalog.rs b/src/core/al-api/src/catalog.rs new file mode 100644 index 00000000..ade5c7a6 --- /dev/null +++ b/src/core/al-api/src/catalog.rs @@ -0,0 +1,37 @@ +use wasm_bindgen::prelude::*; + +use serde::{Deserialize, Serialize}; + +use crate::angle_fmt::AngleSerializeFmt; + +use super::color::ColorRGB; + +#[wasm_bindgen] +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Catalog { + #[serde(default = "default_color")] + pub color: Option, + #[serde(default = "default_opacity")] + pub opacity: Option, + #[serde(default = "default_shape")] + pub shape: Option<&'static str>, + #[serde(default = "default_size")] + pub size: Option, +} + +fn default_color() -> Option { + None +} + +fn default_opacity() -> Option { + None +} + +fn default_shape() -> Option<&'static str> { + None +} + +fn default_size() -> Option { + None +} diff --git a/src/core/src/app.rs b/src/core/src/app.rs index bd00de6c..7fd70777 100644 --- a/src/core/src/app.rs +++ b/src/core/src/app.rs @@ -553,6 +553,8 @@ impl App { &JsValue::from_f64(center.lat().to_degrees()), )?; js_sys::Reflect::set(&args, &"dragging".into(), &JsValue::from_bool(false))?; + js_sys::Reflect::set(&args, &"inertia".into(), &JsValue::from_bool(true))?; + // Position has changed, we call the callback self.callback_position_changed .call1(&JsValue::null(), &args)?; diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index ba85f8b7..1b648248 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -16,6 +16,7 @@ //extern crate itertools_num; //extern crate num; //extern crate num_traits; +use crate::time::Time; use std::panic; pub trait Abort { @@ -629,20 +630,23 @@ impl WebClient { #[wasm_bindgen(js_name = worldToScreenVec)] pub fn world_to_screen_vec(&self, lon: &[f64], lat: &[f64]) -> Box<[f64]> { - let vertices = lon - .iter() - .zip(lat.iter()) - .map(|(&lon, &lat)| { - let xy = self - .app - .world_to_screen(lon, lat) - .map(|v| [v.x, v.y]) - .unwrap_or([0.0, 0.0]); + let vertices = Time::measure_perf("projection rust side", || { + Ok(lon + .iter() + .zip(lat.iter()) + .map(|(&lon, &lat)| { + let xy = self + .app + .world_to_screen(lon, lat) + .map(|v| [v.x, v.y]) + .unwrap_or([0.0, 0.0]); - xy - }) - .flatten() - .collect::>(); + xy + }) + .flatten() + .collect::>()) + }) + .unwrap(); vertices.into_boxed_slice() } diff --git a/src/core/src/renderable/catalog/manager.rs b/src/core/src/renderable/catalog/manager.rs index 94248a8b..cd3cb1d3 100644 --- a/src/core/src/renderable/catalog/manager.rs +++ b/src/core/src/renderable/catalog/manager.rs @@ -1,6 +1,6 @@ +use crate::survey::texture::Texture; use crate::ShaderManager; - use al_api::coo_system::CooSystem; use al_api::resources::Resources; @@ -9,6 +9,8 @@ use al_core::Colormaps; use al_core::FrameBufferObject; use al_core::{Texture2D, VecData, VertexArrayObject, WebGlContext}; +use al_core::image::format::{R8UI, RGBA8U}; + use crate::ProjectionType; use std::collections::HashMap; @@ -27,9 +29,10 @@ impl From for JsValue { } } +const NUM_SHAPES: usize = 5; pub struct Manager { gl: WebGlContext, - kernel_texture: Texture2D, + kernels: HashMap<&'static str, Texture2D>, fbo: FrameBufferObject, @@ -49,31 +52,53 @@ impl Manager { ) -> Result { // Load the texture of the gaussian kernel let kernel_filename = resources.get_filename("kernel").unwrap_abort(); - let kernel_texture = Texture2D::create_from_path::<_, al_core::image::format::RGBA8U>( - gl, - "kernel", - &kernel_filename, - &[ - ( - WebGl2RenderingContext::TEXTURE_MIN_FILTER, - WebGl2RenderingContext::LINEAR, - ), - ( - WebGl2RenderingContext::TEXTURE_MAG_FILTER, - WebGl2RenderingContext::LINEAR, - ), - // Prevents s-coordinate wrapping (repeating) - ( - WebGl2RenderingContext::TEXTURE_WRAP_S, - WebGl2RenderingContext::CLAMP_TO_EDGE, - ), - // Prevents t-coordinate wrapping (repeating) - ( - WebGl2RenderingContext::TEXTURE_WRAP_T, - WebGl2RenderingContext::CLAMP_TO_EDGE, - ), - ], - )?; + let params = &[ + ( + WebGl2RenderingContext::TEXTURE_MIN_FILTER, + WebGl2RenderingContext::LINEAR, + ), + ( + WebGl2RenderingContext::TEXTURE_MAG_FILTER, + WebGl2RenderingContext::LINEAR, + ), + // Prevents s-coordinate wrapping (repeating) + ( + WebGl2RenderingContext::TEXTURE_WRAP_S, + WebGl2RenderingContext::CLAMP_TO_EDGE, + ), + // Prevents t-coordinate wrapping (repeating) + ( + WebGl2RenderingContext::TEXTURE_WRAP_T, + WebGl2RenderingContext::CLAMP_TO_EDGE, + ), + ]; + let kernels = [ + ( + "gaussian", + Texture2D::create_from_path::<_, RGBA8U>(gl, "kernel", &kernel_filename, params)?, + ), + ( + "plus", + Texture2D::create_from_raw_pixels::( + gl, + 3, + 3, + params, + Some(&[0, 0xff, 0, 0xff, 0xff, 0xff, 0, 0xff, 0]), + )?, + ), + ( + "square", + Texture2D::create_from_raw_pixels::( + gl, + 3, + 3, + params, + Some(&[0xff, 0xff, 0xff, 0xff, 0, 0xff, 0xff, 0xff, 0xff]), + )?, + ), + ] + .into(); // Create the VAO for the screen let vertex_array_object_screen = { @@ -81,10 +106,10 @@ impl Manager { -1.0_f32, -1.0_f32, 0.0_f32, 0.0_f32, 1.0_f32, -1.0_f32, 1.0_f32, 0.0_f32, 1.0_f32, 1.0_f32, 1.0_f32, 1.0_f32, -1.0_f32, 1.0_f32, 0.0_f32, 1.0_f32, ]; - let _position = [ + let position = [ -1.0_f32, -1.0_f32, 1.0_f32, -1.0_f32, 1.0_f32, 1.0_f32, -1.0_f32, 1.0_f32, ]; - let _uv = [ + let uv = [ 0.0_f32, 0.0_f32, 1.0_f32, 0.0_f32, 1.0_f32, 1.0_f32, 0.0_f32, 1.0_f32, ]; @@ -143,7 +168,7 @@ impl Manager { let gl = gl.clone(); let mut manager = Manager { gl, - kernel_texture, + kernels, fbo, @@ -489,7 +514,7 @@ impl Catalog { shader_bound .attach_uniforms_from(camera) // Attach catalog specialized uniforms - .attach_uniform("kernel_texture", &manager.kernel_texture) // Gaussian kernel texture + .attach_uniform("kernel_texture", &manager.kernels["gaussian"]) // Gaussian kernel texture .attach_uniform("strength", &self.strength) // Strengh of the kernel .attach_uniform("current_time", &utils::get_current_time()) .attach_uniform("kernel_size", &manager.kernel_size) diff --git a/src/css/aladin.css b/src/css/aladin.css index 447e0630..5d2d1806 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -108,7 +108,7 @@ cursor: pointer; font-size: 13px; - font-family: Verdana, Geneva, Tahoma, sans-serif; + font-family: monospace; text-overflow: ellipsis; text-align: left; @@ -636,6 +636,14 @@ canvas { .aladin-btn.toggled { border: 2px solid greenyellow; } +.aladin-btn.disabled { + cursor: not-allowed; + filter: brightness(70%); +} + +.aladin-btn:not(.disabled):hover { + filter: brightness(105%); +} .aladin-toolbar { display: flex; @@ -693,10 +701,6 @@ canvas { vertical-align: middle; } -.aladin-btn:hover { - filter: brightness(105%); -} - .aladin-cancelBtn { background-color: #ca4242; border-color: #bd3935; diff --git a/src/js/A.js b/src/js/A.js index 7dcaafdf..1476b9d2 100644 --- a/src/js/A.js +++ b/src/js/A.js @@ -178,6 +178,7 @@ A.MOCFromJSON = function (jsonMOC, options) { A.catalogFromURL = function (url, options, successCallback, errorCallback, useProxy) { + options.url = url; var catalog = A.catalog(options); const processVOTable = function (table) { diff --git a/src/js/Aladin.js b/src/js/Aladin.js index f0c7341b..41b39a9b 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -125,6 +125,9 @@ export let Aladin = (function () { //var location = new Location(locationDiv.find('.aladin-location-text')); // set different options + // Reticle + this.reticle = new Reticle(this.options, this); + this.view = new View(this); this.cacheSurveys = new Map(); @@ -317,20 +320,20 @@ export let Aladin = (function () { var aladin = this; var zoomPlus = $(aladinDiv).find('.zoomPlus'); - zoomPlus.click(function () { + zoomPlus.on('click', function () { aladin.increaseZoom(); return false; }); - zoomPlus.bind('mousedown', function (e) { + zoomPlus.on('mousedown', function (e) { e.preventDefault(); // to prevent text selection }); var zoomMinus = $(aladinDiv).find('.zoomMinus'); - zoomMinus.click(function () { + zoomMinus.on('click', function () { aladin.decreaseZoom(); return false; }); - zoomMinus.bind('mousedown', function (e) { + zoomMinus.on('mousedown', function (e) { e.preventDefault(); // to prevent text selection }); @@ -366,12 +369,7 @@ export let Aladin = (function () { if (options.samp) { this.samp = new SAMPConnector(this); - ALEvent.SAMP_AVAILABILITY.listenedBy(this.aladinDiv, function (e) { - console.log('is hub running samp', e.detail.isHubRunning) - }); } - // Reticle - this.reticle = new Reticle(this.options, this); }; /**** CONSTANTS ****/ @@ -926,8 +924,7 @@ export let Aladin = (function () { this.view.showCatalog(show); }; Aladin.prototype.showReticle = function (show) { - console.log("show", show, this.reticle) - this.reticle.show(show) + this.reticle.update({show}) //$('#displayReticle').attr('checked', show); }; @@ -1224,24 +1221,19 @@ export let Aladin = (function () { new ALEvent(alEventName).listenedBy(this.aladinDiv, customFn); }; + Aladin.prototype.selectObjects = function(objects) { + this.view.selectObjects(objects) + }; // Possible values are 'rect' and 'circle' // TODO: add a 'polygon' selection mode - Aladin.prototype.select = function (mode = 'rect', callbackFn) { - this.fire('selectstart', {mode: mode, callbackFn: callbackFn}); + Aladin.prototype.select = function (mode = 'rect', callback) { + this.fire('selectstart', {mode, callback}); }; Aladin.prototype.fire = function (what, params) { if (what === 'selectstart') { - this.view.startSelection(params["mode"], params["callbackFn"]); - } - else if (what === 'selectend') { - this.view.finishSelection(); - - var callbackFn = this.callbacksByEventName['select']; - if (typeof callbackFn === "function") { - this.view.showSelectedObjects(); - callbackFn(this.view.selectedObjects); - } + const {mode, callback} = params; + this.view.startSelection(mode, callback); } else if (what === 'simbad') { this.view.setMode(View.TOOL_SIMBAD_POINTER); diff --git a/src/js/Catalog.js b/src/js/Catalog.js index 290faa34..af40c8de 100644 --- a/src/js/Catalog.js +++ b/src/js/Catalog.js @@ -48,6 +48,7 @@ export let Catalog = (function() { let self = this; options = options || {}; + this.url = options.url; this.uuid = Utils.uuidv4(); this.type = 'catalog'; this.name = options.name || "catalog"; @@ -64,7 +65,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.filterFn = options.filter || undefined; // TODO: do the same for catalog this.showFieldCallback = {}; // callbacks when the user clicks on a cell in the measurement table associated this.fields = undefined; @@ -301,6 +302,7 @@ 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) { + let rowIdx = 0; VOTable.parse( url, (rsc) => { @@ -348,6 +350,7 @@ export let Catalog = (function() { } source = new Source(ra, dec, mesures); + source.rowIdx = rowIdx; } let footprint = null; @@ -368,6 +371,7 @@ export let Catalog = (function() { } } + rowIdx++; return true; }) @@ -642,8 +646,16 @@ export let Catalog = (function() { }) } + let measureTime = (msg, f) => { + let a = performance.now() + let res = f() + console.log(msg, performance.now() - a); + + return res; + }; + // Draw the footprints - this.drawFootprints(ctx); + measureTime(this.name + ' draw footprints', () => {this.drawFootprints(ctx)}); }; Catalog.prototype.drawSources = function(ctx, width, height) { @@ -651,20 +663,32 @@ export let Catalog = (function() { return; } + let measureTime = (msg, f) => { + let a = performance.now() + let res = f() + console.log(msg, performance.now() - a); + + return res; + }; + let sourcesInsideView = []; - let xy = this.view.wasm.worldToScreenVec(this.ra, this.dec); + let xy = measureTime(this.name + ' projection sources', () => { + return this.view.wasm.worldToScreenVec(this.ra, this.dec); + }); - let self = this; - this.sources.forEach(function(s, idx) { - if (xy[2*idx] && xy[2*idx + 1]) { - if (!self.filterFn || self.filterFn(s)) { - s.x = xy[2*idx]; - s.y = xy[2*idx + 1]; - - self.drawSource(s, ctx, width, height) - sourcesInsideView.push(s); + measureTime(this.name + ' draw sources', () => { + let self = this; + this.sources.forEach(function(s, idx) { + if (xy[2*idx] && xy[2*idx + 1]) { + if (!self.filterFn || self.filterFn(s)) { + s.x = xy[2*idx]; + s.y = xy[2*idx + 1]; + + self.drawSource(s, ctx, width, height) + sourcesInsideView.push(s); + } } - } + }); }); return sourcesInsideView; diff --git a/src/js/Color.js b/src/js/Color.js index a310067a..06ef144e 100644 --- a/src/js/Color.js +++ b/src/js/Color.js @@ -29,38 +29,44 @@ *****************************************************************************/ export let Color = (function() { let Color = function(label) { + let color; if (label.r && label.g && label.b) { - return label; - } - - // hex string form - if (label.match(/^#?[0-9A-Fa-f]{6}$/) || label.match(/^#?[0-9A-Fa-f]{3}$/)) { + 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 - return Color.hexToRgb(label); + 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]) + } } - // rgb string form - var rgb = label.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); - - if (rgb) { - var r = parseInt(rgb[1]); - var g = parseInt(rgb[2]); - var b = parseInt(rgb[3]); - return {r: r, g: g, b: b} - } - - // fill style label - if (!Color.standardizedColors[label]) { - var ctx = document.createElement('canvas').getContext('2d'); - ctx.fillStyle = label; - const colorHexa = ctx.fillStyle; - - Color.standardizedColors[label] = colorHexa; - } - - return Color.standardizedColors[label]; + 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) + } + Color.curIdx = 0; Color.colors = ['#ff0000', '#0000ff', '#99cc00', '#ffff00','#000066', '#00ffff', '#9900cc', '#0099cc', '#cc9900', '#cc0099', '#00cc99', '#663333', '#ffcc9a', '#ff9acc', '#ccff33', '#660000', '#ffcc33', '#ff00ff', '#00ff00', '#ffffff']; Color.standardizedColors = {}; diff --git a/src/js/DefaultActionsForContextMenu.js b/src/js/DefaultActionsForContextMenu.js index 6a0c3b9d..fff18d0b 100644 --- a/src/js/DefaultActionsForContextMenu.js +++ b/src/js/DefaultActionsForContextMenu.js @@ -36,6 +36,11 @@ 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) { @@ -54,19 +59,19 @@ export let DefaultActionsForContextMenu = (function () { } }, { - 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(); } }, { label: 'New catalogue layer', action(o) { - aladinInstance.stack._onAddCatalogue(); + a.stack._onAddCatalogue(); } }, ] @@ -86,19 +91,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 +119,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 +135,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 +146,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,13 +161,21 @@ export let DefaultActionsForContextMenu = (function () { } }, { - label: "Select sources", action(o) { - const a = aladinInstance; - - a.select('rect', (_) => { - a.view.showSelectedObjects(); - }) - } + 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/FiniteStateMachine.js b/src/js/FiniteStateMachine.js new file mode 100644 index 00000000..52cfd18d --- /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) { + if (params) { + action(params); + } else { + action() + } + + this.state = to; + } + } +} \ No newline at end of file diff --git a/src/js/Footprint.js b/src/js/Footprint.js index 24317970..14f84729 100644 --- a/src/js/Footprint.js +++ b/src/js/Footprint.js @@ -110,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() { diff --git a/src/js/MeasurementTable.js b/src/js/MeasurementTable.js index 061c0970..00a8fe0e 100644 --- a/src/js/MeasurementTable.js +++ b/src/js/MeasurementTable.js @@ -43,42 +43,6 @@ export let MeasurementTable = (function() { this.target = target; } - /*MeasurementTable.prototype.updateTableBody = function() { - let tbody = this.element.querySelector('tbody'); - tbody.innerHTML = ''; - - let table = this.tables[this.curTableIdx]; - - table["rows"].forEach((row) => { - let trEl = document.createElement('tr'); - - for (let key in row.data) { - // check the type here - - let tdEl = document.createElement('td'); - tdEl.classList.add(key); - - if (table.showCallback && table.showCallback[key]) { - let showFieldCallback = table.showCallback[key]; - - let el = showFieldCallback(row.data); - if (el instanceof Element) { - tdEl.appendChild(el); - } else { - tdEl.innerHTML = el; - } - } else { - let val = row.data[key] || '--'; - tdEl.innerText = val; - } - - trEl.appendChild(tdEl); - } - - tbody.appendChild(trEl); - }); - }*/ - // show measurement associated with a given source MeasurementTable.prototype.showMeasurement = function(tables) { if (tables.length === 0) { diff --git a/src/js/Overlay.js b/src/js/Overlay.js index 7f12798a..701e4173 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/Reticle.js b/src/js/Reticle.js index 9d01ea92..6fee0851 100644 --- a/src/js/Reticle.js +++ b/src/js/Reticle.js @@ -30,6 +30,7 @@ import { Color } from './Color.js'; *****************************************************************************/ import { Aladin } from "./Aladin"; +import { ALEvent } from './events/ALEvent'; export let Reticle = (function() { // constructor @@ -38,6 +39,7 @@ export let Reticle = (function() { this.el.className = 'aladin-reticle'; this.el.type = "image/svg+xml"; this.el.data = iconUrl; + this.aladin = aladin; aladin.aladinDiv.appendChild(this.el); @@ -69,12 +71,33 @@ export let Reticle = (function() { self.el.addEventListener('error', handleError); }); - this.setColor(reticleColor) - this.setSize(reticleSize); - this.show(reticleShow); + this._setColor(reticleColor) + this._setSize(reticleSize); + this._show(reticleShow); }; - Reticle.prototype.setColor = async function(color) { + Reticle.prototype.update = 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; } @@ -92,7 +115,7 @@ export let Reticle = (function() { } - Reticle.prototype.setSize = async function(size) { + Reticle.prototype._setSize = async function(size) { if (!size) { return; } @@ -104,7 +127,7 @@ export let Reticle = (function() { this.el.style.height = this.size + 'px'; } - Reticle.prototype.show = async function(show) { + Reticle.prototype._show = async function(show) { if (show === undefined) { return; } diff --git a/src/js/Selector.js b/src/js/Selector.js index 9a56ad28..1d69f08d 100644 --- a/src/js/Selector.js +++ b/src/js/Selector.js @@ -17,6 +17,10 @@ // along with Aladin Lite. // +import { Aladin } from "./Aladin"; +import { Color } from "./Color"; +import { FSM } from "./FiniteStateMachine"; +import { View } from "./View"; /****************************************************************************** * Aladin Lite project * @@ -28,81 +32,238 @@ * *****************************************************************************/ -export let Selector = (function() { +export class Selector extends FSM { // constructor - let Selector = function() {}; + constructor(view, options) { + let start = (params) => { + const {mode, callback} = params; + this.view.setCursor('crosshair'); + this.view.aladin.showReticle(false) - Selector.prototype.start = function(startCoo, mode, callbackFn) { - this.startCoo = { x: startCoo.x, y: startCoo.y}; - this.mode = mode; - this.callbackFn = callbackFn; + this.mode = mode; + this.callback = callback; + this.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; + + this.view.requestRedraw(); + }; + + let draw = () => { + let ctx = this.view.catalogCtx; + + if (!this.view.catalogCanvasCleared) { + ctx.clearRect(0, 0, this.view.width, this.view.height); + this.view.catalogCanvasCleared = true; + } + // draw the selection + ctx.fillStyle = this.color + '7f'; + ctx.strokeStyle = this.color; + ctx.lineWidth = this.lineWidth; + switch (this.mode) { + case 'rect': + 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); + break; + case 'circle': + 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(); + break; + default: + break; + } + } + + let mouseup = (params) => { + var x, y; + const {coo} = params; + this.coo = coo; + // finish the selection + switch (this.mode) { + case 'rect': + 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} + } + }); + break; + case 'circle': + 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 + } + } + }); + break; + default: + break; + } + + // TODO: remove these modes in the future + this.view.aladin.showReticle(true) + this.view.setCursor('default'); + + // execute general callback + if (this.view.callbacksByEventName) { + var callback = this.view.callbacksByEventName['select']; + if (typeof callback === "function") { + // !todo + let selectedObjects = this.view.selectObjects(this); + callback(selectedObjects); + } + } + this.view.setMode(View.PAN) + this.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, + } + } + }) + + this.view = view; + this.options = options; + + let color = (options && options.color) || Aladin.DEFAULT_OPTIONS.reticleColor; + this.color = new Color(color).toHex(); + this.lineWidth = (options && options.lineWidth) || 2; }; - Selector.prototype.draw = function(ctx, dragCoo) { - ctx.fillStyle = "rgba(100, 240, 110, 0.20)"; - ctx.strokeStyle = "rgb(100, 240, 110)"; - ctx.lineWidth = 2; - switch (this.mode) { - case 'rect': - var w = dragCoo.x - this.startCoo.x; - var h = dragCoo.y - this.startCoo.y; - - ctx.fillRect(this.startCoo.x, this.startCoo.y, w, h); - ctx.strokeRect(this.startCoo.x, this.startCoo.y, w, h); - break; - case 'circle': - var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); - var r = Math.sqrt(r2); - - ctx.beginPath(); - ctx.arc(this.startCoo.x, this.startCoo.y, r, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - break; - default: - break; + static getObjects(selection, view) { + if (!selection) { + return; } - }; - Selector.prototype.finish = function(dragCoo) { - switch (this.mode) { - case 'rect': - var w = dragCoo.x - this.startCoo.x; - var h = dragCoo.y - this.startCoo.y; + 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); + } + } + } - (typeof this.callbackFn === 'function') && this.callbackFn({ - x: this.startCoo.x, - y: this.startCoo.y, - w: w, - h: h - }); - break; - case 'circle': - var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); - var r = Math.sqrt(r2); - - (typeof this.callbackFn === 'function') && this.callbackFn({ - x: this.startCoo.x, - y: this.startCoo.y, - r: r, - }); - break; - default: - break; + if (objListPerCatalog.length > 0) { + objList.push(objListPerCatalog); + } + objListPerCatalog = []; + } } + return objList; } - Selector.prototype.getBBox = function(dragCoo) { + _getBBox() { let x, y, w, h; switch (this.mode) { case 'rect': - w = dragCoo.x - this.startCoo.x; - h = dragCoo.y - this.startCoo.y; + w = this.coo.x - this.startCoo.x; + h = this.coo.y - this.startCoo.y; x = this.startCoo.x; y = this.startCoo.y; break; case 'circle': - var r2 = (dragCoo.x - this.startCoo.x) * (dragCoo.x - this.startCoo.x) + (dragCoo.y - this.startCoo.y) * (dragCoo.y - this.startCoo.y); + var 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 - r; @@ -116,6 +277,4 @@ export let Selector = (function() { return {x: x, y: y, w: w, h: h}; }; - - return Selector; -})(); \ No newline at end of file +} \ No newline at end of file diff --git a/src/js/Source.js b/src/js/Source.js index 1e7023f4..b28461ef 100644 --- a/src/js/Source.js +++ b/src/js/Source.js @@ -28,10 +28,9 @@ * *****************************************************************************/ -import { ActionButton } from "./gui/Widgets/ActionButton"; -import { Datalink } from "./vo/Datalink"; -import { Utils } from "./Utils"; import { ObsCore } from "./vo/ObsCore"; +import { SAMPActionButton } from "./gui/Button/SAMP"; +import { Layout } from "./gui/Layout"; export let Source = (function() { // constructor @@ -102,27 +101,17 @@ 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(); - - view.aladin.measurementTable.hide(); - - let singleSourceTable = { - 'rows': [this], - 'fields': this.catalog.fields, - 'showCallback': ObsCore.SHOW_CALLBACKS(view.aladin), - 'name': this.catalog.name, - 'color': this.catalog.color - }; - - view.aladin.measurementTable.showMeasurement([singleSourceTable]); + if (!obj) { + obj = this; + } + view.selectObjects([obj]); } else if (this.catalog.onClick == 'showPopup') { - view.popup.setTitle('

'); var m = '
'; m += ''; diff --git a/src/js/View.js b/src/js/View.js index e318d304..1247bf3e 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -50,6 +50,9 @@ import { Footprint } from "./Footprint.js"; import { Selector } from "./Selector.js"; import $ from 'jquery'; import { ObsCore } from "./vo/ObsCore.js"; +import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js"; +import { Layout } from "./gui/Layout.js"; +import { SAMPActionButton } from "./gui/Button/SAMP.js"; export let View = (function () { @@ -147,9 +150,7 @@ export let View = (function () { // Frame setting this.changeFrame(this.cooFrame); - this.selector = new Selector(); - this.selectMode = 'rect'; - this.selectCallbackFn = undefined; + this.selector = new Selector(this); // Zoom starting setting const si = 500000.0; @@ -230,7 +231,7 @@ export let View = (function () { this.resizeTimer = null; let resizeObserver = new ResizeObserver(() => { self.fixLayoutDimensions(); - self.requestRedraw(); + //self.requestRedraw(); }); this.throttledPositionChanged = Utils.throttle( @@ -269,6 +270,7 @@ export let View = (function () { //$(window).resize(() => { self.fixLayoutDimensions(); self.redraw() + //self.requestRedraw(); //}); // in some contexts (Jupyter notebook for instance), the parent div changes little time after Aladin Lite creation @@ -383,28 +385,18 @@ export let View = (function () { ctx.oImageSmoothingEnabled = enableSmoothing; } - View.prototype.startSelection = function(mode, callbackFn) { - this.setMode(View.SELECT); - - this.selectMode = mode; - this.selectCallbackFn = callbackFn; - } - - View.prototype.finishSelection = function() { - this.setMode(View.PAN); + View.prototype.startSelection = function(mode, callback) { + this.selector.dispatch('start', {mode, 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) { + if (this.mode == View.TOOL_SIMBAD_POINTER) { this.popup.hide(); this.catalogCanvas.style.cursor = ''; $(this.catalogCanvas).addClass('aladin-sp-cursor'); } - else { + else if (this.mode == View.PAN) { this.setCursor('default'); } }; @@ -475,7 +467,7 @@ export let View = (function () { const xymouse = Utils.relMouseCoords(e); // deselect all the selected sources with Select panel - view.hideSelectedObjects() + view.unselectObjects() try { const lonlat = view.wasm.screenToWorld(xymouse.x, xymouse.y); @@ -492,7 +484,7 @@ export let View = (function () { } // prevent default context menu from appearing (potential clash with right-click cuts control) - $(view.catalogCanvas).bind("contextmenu", function (e) { + $(view.catalogCanvas).on("contextmenu", function (e) { // do something here... e.preventDefault(); }, false); @@ -501,7 +493,7 @@ export let View = (function () { let cutMinInit = null let cutMaxInit = null; - $(view.catalogCanvas).bind("mousedown touchstart", function (e) { + $(view.catalogCanvas).on("mousedown touchstart", function (e) { e.preventDefault(); e.stopPropagation(); @@ -564,18 +556,19 @@ export let View = (function () { if (view.mode == View.PAN) { view.setCursor('move'); } - else if (view.mode == View.SELECT) { - view.selector.start(view.dragCoo, view.selectMode, view.selectCallbackFn); - } 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) { + $(view.catalogCanvas).on("mouseup", function (e) { e.preventDefault(); e.stopPropagation(); @@ -604,10 +597,14 @@ 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 - $(view.catalogCanvas).bind("click mouseout touchend touchcancel", function (e) { + $(view.catalogCanvas).on("click mouseout touchend touchcancel", function (e) { const xymouse = Utils.relMouseCoords(e); ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, { @@ -634,8 +631,7 @@ 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'); @@ -646,18 +642,6 @@ export let View = (function () { } } // end of "if (view.dragging) ... " - if (selectionHasEnded) { - view.selector.finish(view.dragCoo); - - view.aladin.fire( - 'selectend' - ); - - view.requestRedraw(); - - return; - } - view.mustClearCatalog = true; view.dragCoo = null; @@ -683,7 +667,7 @@ export let View = (function () { // popup to show ? var objs = view.closestObjects(xymouse.x, xymouse.y, 5); if (!wasDragging && objs) { - view.hideSelectedObjects(); + view.unselectObjects(); var o = objs[0]; @@ -717,30 +701,28 @@ export let View = (function () { } view.lastClickedObject = o; - } else { - if (!wasDragging) { - // Deselect objects if any - view.hideSelectedObjects(); + } 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.popup.hide(); + // If there is a past clicked object + if (view.lastClickedObject) { + //view.aladin.measurementTable.hide(); + //view.aladin.sodaForm.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(); - } - - var objClickedFunction = view.aladin.callbacksByEventName['objectClicked']; - (typeof objClickedFunction === 'function') && objClickedFunction(null, xymouse); - - view.lastClickedObject = null; + // 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); + + view.lastClickedObject = null; } } @@ -759,11 +741,15 @@ export let View = (function () { //view.requestRedraw(); view.wasm.releaseLeftButtonMouse(xymouse.x, xymouse.y); + + if (view.mode === View.SELECT) { + view.selector.dispatch('mouseout', {coo: view.dragCoo}) + } }); var lastHoveredObject; // save last object hovered by mouse var lastMouseMovePos = null; - $(view.catalogCanvas).bind("mousemove touchmove", function (e) { + $(view.catalogCanvas).on("mousemove touchmove", function (e) { e.preventDefault(); const xymouse = Utils.relMouseCoords(e); @@ -841,7 +827,7 @@ export let View = (function () { return; } - if (!view.dragging /*&& !view.moving*/) { + if (!view.dragging && !view.moving) { view.updateObjectsLookup(); } @@ -850,7 +836,7 @@ export let View = (function () { 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') { @@ -865,46 +851,45 @@ 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); - } - - 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 = null; + 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); + } + } + + 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; } + if (e.type === "mousemove") { return; } @@ -914,26 +899,31 @@ export let View = (function () { return; } + view.realDragging = true; + + var s1 = view.dragCoo, s2 = xymouse; // update drag coo with the new position view.dragCoo = xymouse; - if (view.mode == View.SELECT) { + /*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(); + } else if (view.mode === View.SELECT) { + view.selector.dispatch('mousemove', {coo: xymouse}) } - - 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); - - 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 @@ -1101,61 +1091,59 @@ export let View = (function () { }; 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, this.zoomFactor); } } // 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, this.zoomFactor); // 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, this.zoomFactor); } } ////// 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); @@ -1169,13 +1157,17 @@ 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); + this.healpixGrid.redraw(ctx, cornersXYViewMapAllsky, this.fov, 3); } } + if (this.mode === View.SELECT) { + this.selector.dispatch('draw') + } + ////// 4. Draw reticle /////// // TODO: reticle should be placed in a static DIV, no need to waste a canvas //var reticleCtx = catalogCtx; @@ -1267,35 +1259,45 @@ export let View = (function () { return pixList; }; - View.prototype.hideSelectedObjects = function() { + View.prototype.unselectObjects = function() { this.aladin.measurementTable.hide(); - if (this.selectedObjects) { - this.selectedObjects.forEach((objList) => { + if (this.selection) { + this.selection.forEach((objList) => { objList.forEach((o) => o.deselect()) }); - 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.showSelectedObjects = function() { - this.hideSelectedObjects(); + View.prototype.selectObjects = function(selection) { + // unselect the previous selection + this.unselectObjects(); - this.selectedObjects = this.getSelectedObjects(); + if (Array.isArray(selection)) { + this.selection = [selection]; + } else { + // select the new + this.selection = Selector.getObjects(selection, this); + } - if (this.selectedObjects.length > 0) { - this.selectedObjects.forEach((objListPerCatalog) => { + if (this.selection.length > 0) { + this.selection.forEach((objListPerCatalog) => { objListPerCatalog.forEach((obj) => obj.select()) }); - let tables = this.selectedObjects.map((objList) => { + 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) { @@ -1318,6 +1320,50 @@ export let View = (function () { }) this.aladin.measurementTable.showMeasurement(tables); + let a = this.aladin; + const sampBtn = new SAMPActionButton({ + tooltip: {content: 'Send a table through SAMP Hub'}, + action(conn) { + // hide the menu + a.contextMenu._hide() + + let getSource = (o) => { + let s = o; + if (o.source) { + s = o.source + } + + return s; + }; + + for (const objects of objList) { + 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) + } + } + }, a); + + a.contextMenu.attach([ + { + label: Layout.horizontal([sampBtn, a.samp ? 'Send selection to SAMP' : 'SAMP disabled']), + }, + { + label: 'Remove selection', + action(o) { + a.view.unselectObjects(); + } + } + ]); } } @@ -1584,14 +1630,12 @@ export let View = (function () { } View.prototype.removeImageLayer = function (layer) { - console.log("remove image layer", layer) // Get the survey to remove to dissociate it from the view let imageLayer = this.imageLayers.get(layer); if (imageLayer === undefined) { // there is nothing to remove return; } - console.log("remove image layer", layer) // Update the backend if (imageLayer.added) { @@ -1606,7 +1650,6 @@ export let View = (function () { // layer not found return; } - console.log("remove image layer", layer) // Delete it this.imageLayers.delete(layer); @@ -1625,7 +1668,6 @@ export let View = (function () { } } - console.log("removed image layer", layer) ALEvent.HIPS_LAYER_REMOVED.dispatchedTo(this.aladinDiv, { layer: layer }); // check if there are no more surveys @@ -1749,7 +1791,7 @@ export let View = (function () { this.requestRedraw(); }; - View.prototype.showReticle = function (show) { + /*View.prototype.showReticle = function (show) { this.displayReticle = show; if (!this.displayReticle) { @@ -1757,7 +1799,7 @@ export let View = (function () { } this.requestRedraw(); - }; + };*/ /** * @@ -1870,58 +1912,6 @@ export let View = (function () { moc.setView(this); }; - View.prototype.getSelectedObjects = function () { - let {x, y, w, h} = this.selector.getBBox(this.dragCoo); - - 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 = []; diff --git a/src/js/events/ALEvent.js b/src/js/events/ALEvent.js index fac0eb82..232984ac 100644 --- a/src/js/events/ALEvent.js +++ b/src/js/events/ALEvent.js @@ -60,9 +60,14 @@ 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") + constructor(name) { this.name = name; } diff --git a/src/js/gui/Button/SAMP.js b/src/js/gui/Button/SAMP.js new file mode 100644 index 00000000..1548bfc1 --- /dev/null +++ b/src/js/gui/Button/SAMP.js @@ -0,0 +1,114 @@ +// 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 = { + iconURL: waveOffIconUrl, + tooltip: {content: 'SAMP disabled in Aladin Lite options', position: {direction: 'left'}}, + disable: true, + } + } else { + let isHubRunning = aladin.samp.isHubCurrentlyRunning(); + let tooltip = options && options.tooltip || {content: isHubRunning ? 'Connect to SAMP Hub' : 'No hub running found', position: {direction: 'left'}} + let action = options && options.action + if (!action) { + // default action, just connect and ping + action = (connector) => { + connector.register(); + } + } + let disable = !isHubRunning; + + options = { + iconURL: aladin.samp.isConnected() ? waveOnIconUrl : waveOffIconUrl, + tooltip, + disable, + action(o) { + action(aladin.samp) + } + } + } + + super(options) + + this.addClass('aladin-24px-icon') + this._addListeners(aladin); + } + + _addListeners(aladin) { + let self = this; + let hubRunning; + ALEvent.SAMP_CONNECTED.listenedBy(aladin.aladinDiv, function (e) { + const iconURL = waveOnIconUrl + self.update({iconURL}) + }); + + ALEvent.SAMP_DISCONNECTED.listenedBy(aladin.aladinDiv, function (e) { + const iconURL = waveOffIconUrl + self.update({iconURL}) + }); + + ALEvent.SAMP_HUB_RUNNING.listenedBy(aladin.aladinDiv, function (e) { + const isHubRunning = e.detail.isHubRunning; + + if (hubRunning !== isHubRunning) { + let newOptions = { + disable: !isHubRunning + }; + if (!isHubRunning) { + newOptions['tooltip'] = {content: 'No hub running found'}; + } + + self.update(newOptions) + if (isHubRunning === false) { + self.update({iconURL: waveOffIconUrl}) + } + hubRunning = isHubRunning; + } + }); + } + } + \ No newline at end of file diff --git a/src/js/gui/FoV.js b/src/js/gui/FoV.js index b495550e..8312a9ee 100644 --- a/src/js/gui/FoV.js +++ b/src/js/gui/FoV.js @@ -32,7 +32,7 @@ import { Numbers } from "../libs/astro/coo.js"; import { Layout } from "./Layout.js"; -import { DOMElement } from "./widgets/Widget.js"; +import { DOMElement } from "./Widgets/Widget.js"; import { ALEvent } from "../events/ALEvent.js"; diff --git a/src/js/gui/Layout.js b/src/js/gui/Layout.js index eee6290b..16a4742e 100644 --- a/src/js/gui/Layout.js +++ b/src/js/gui/Layout.js @@ -16,9 +16,6 @@ // The GNU General Public License is available in COPYING file // along with Aladin Lite. // - -import { Utils } from "./Utils"; - import { DOMElement } from "./Widgets/Widget"; import { Tooltip } from "./Widgets/Tooltip"; diff --git a/src/js/gui/SODAQueryWindow.js b/src/js/gui/SODAQueryWindow.js index f9e3cfc0..5b7650f2 100644 --- a/src/js/gui/SODAQueryWindow.js +++ b/src/js/gui/SODAQueryWindow.js @@ -73,7 +73,7 @@ export class SODAQueryWindow { backgroundColor: '#bababa', borderColor: '#484848', }, - info: 'Circular selection\nClick, drag and release to define the circle', + tooltip: {content: 'Circular selection
Click, drag and release to define the circle'}, action(e) { self.aladin.select('circle', (s) => { const {x, y, r} = s; @@ -105,7 +105,7 @@ export class SODAQueryWindow { backgroundColor: '#bababa', borderColor: '#484848', }, - info: 'This is the form to request the SODA server located at: ' + this.formParams["baseUrl"] + '\nThe list of input params is:\n' + listOfInputParams, + tooltip: {content: 'This is the form to request the SODA server located at:
' + this.formParams["baseUrl"] + '
The list of input params is:
' + listOfInputParams}, action(e) {} }); let layoutForm = { diff --git a/src/js/gui/Toolbar/Controls/GotoBox.js b/src/js/gui/Toolbar/Controls/GotoBox.js index ee35a657..5624cae9 100644 --- a/src/js/gui/Toolbar/Controls/GotoBox.js +++ b/src/js/gui/Toolbar/Controls/GotoBox.js @@ -29,7 +29,7 @@ *****************************************************************************/ import { Box } from "../../Widgets/Box.js"; -import { Form } from "../../widgets/Form.js"; +import { Form } from "../../Widgets/Form.js"; export class GotoBox extends Box { // Constructor diff --git a/src/js/gui/Toolbar/Controls/Overlays/Stack.js b/src/js/gui/Toolbar/Controls/Overlays/Stack.js new file mode 100644 index 00000000..db543708 --- /dev/null +++ b/src/js/gui/Toolbar/Controls/Overlays/Stack.js @@ -0,0 +1,221 @@ +// 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 { ALEvent } from "../../../../events/ALEvent.js"; + import { Layout } from "../../../Layout.js"; + import { ContextMenu } from "../../../Widgets/ContextMenu.js"; + import { ActionButton } from "../../../Widgets/ActionButton.js"; + import showIconUrl from '../../../../../../assets/icons/show.svg'; + import hideIconUrl from '../../../../../../assets/icons/hide.svg'; + import removeIconUrl from '../../../../../../assets/icons/remove.svg'; + + export class OverlayStack extends ContextMenu { + // Constructor + constructor(aladin, menu) { + super(aladin); + this.aladin = aladin; + this.anchor = menu.controls["OverlayStack"]; + + this._addListeners(); + } + + _addListeners() { + let self = this; + + let updateImageList = () => { + const overlays = Array.from(self.aladin.getOverlays()).reverse().map((overlay) => { + return overlay; + }); + + + self.attach({overlays}); + // If it is shown, update it + if (!self.isHidden) { + self.show(); + } + }; + + updateImageList(); + + ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.listenedBy(this.aladin.aladinDiv, function (e) { + updateImageList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_REMOVED.listenedBy(this.aladin.aladinDiv, function (e) { + updateImageList(); + }); + + ALEvent.GRAPHIC_OVERLAY_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, function (e) { + updateImageList(); + }); + } + + attach(options) { + const overlays = options && options.overlays || []; + + /*let layout = [{ + label: Layout.horizontal({ + layout: [ + ActionButton.createIconBtn({ + iconURL: searchIconUrl, + tooltip: {content: 'Add a survey
from our database...', position: { direction: 'bottom' }}, + cssStyle: { + backgroundPosition: 'center center', + backgroundColor: '#bababa', + border: '1px solid rgb(72, 72, 72)', + cursor: 'help', + }, + }), + 'Add a survey/FITS image' + ] + }), + action(o) { + const hipsSelector = HiPSSelectorBox.getInstance(self.aladin); + hipsSelector._hide(); + hipsSelector._show(); + + self.fsm.dispatch('hide'); + } + }];*/ + let layout = []; + + let self = this; + for(const overlay of overlays) { + console.log(overlay) + const name = overlay.name; + let cssStyle = { + height: 'fit-content', + }; + let showBtn = ActionButton.createIconBtn({ + iconURL: showIconUrl, + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + visibility: 'hidden', + width: '18px', + height: '18px', + verticalAlign: 'middle', + marginRight: '2px', + }, + tooltip: {content: 'Hide', position: {direction: 'bottom'}}, + action(e, btn) { + if (overlay.isShowing) { + overlay.hide() + btn.update({iconURL: showIconUrl, tooltip: {content: 'Hide'}}); + } else { + overlay.show() + + btn.update({iconURL: hideIconUrl, tooltip: {content: 'Show'}}); + } + } + }); + + let deleteBtn = ActionButton.createIconBtn({ + iconURL: removeIconUrl, + cssStyle: { + backgroundColor: '#bababa', + borderColor: '#484848', + color: 'black', + visibility: 'hidden', + width: '18px', + height: '18px', + verticalAlign: 'middle' + }, + tooltip: {content: 'Remove', position: {direction: 'left'}}, + action(e) { + self.aladin.removeLayer(overlay) + } + }); + + let item = Layout.horizontal({ + layout: [ + '
' + name + '
', + Layout.horizontal({layout: [showBtn, deleteBtn]}) + ], + cssStyle: { + display: 'flex', + alignItems: 'center', + listStyle: 'none', + justifyContent: 'space-between', + width: '100%', + } + }); + layout.push({ + label: item, + cssStyle: 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' + }, + }) + } + + super.attach(layout); + } + + + show() { + console.log("show") + super.show({ + position: { + anchor: this.anchor, + direction: 'bottom', + }, + cssStyle: { + width: '15em', + //overflowY: 'scroll', + //maxHeight: '500px', + color: 'white', + backgroundColor: 'black', + border: '1px solid white', + } + }) + } + + hide() { + super._hide(); + } + + static singleton; + + static getInstance(aladin, menu, fsm) { + if (!OverlayStack.singleton) { + OverlayStack.singleton = new OverlayStack(aladin, menu, fsm); + } + + return OverlayStack.singleton; + } + } + \ No newline at end of file diff --git a/src/js/gui/Toolbar/Controls/Settings.js b/src/js/gui/Toolbar/Controls/Settings.js index 3a007dbd..30021209 100644 --- a/src/js/gui/Toolbar/Controls/Settings.js +++ b/src/js/gui/Toolbar/Controls/Settings.js @@ -33,6 +33,7 @@ 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"; export class Settings extends ContextMenu { // Constructor @@ -55,7 +56,7 @@ export class Settings extends ContextMenu { change(e) { let hex = e.target.value; let reticle = aladin.getReticle(); - reticle.setColor(hex) + reticle.update({color: hex}) } }); @@ -95,7 +96,24 @@ export class Settings extends ContextMenu { self.reticleCheckbox = Input.checkbox({name: 'reticle', checked: this.aladin.isReticleDisplayed()}) this.menu = menu; + + let sampBtn = new SAMPActionButton({ + action(conn) { + if (conn.isConnected()) { + conn.unregister(); + sampBtn.update({tooltip: {content: 'Connect to SAMP Hub'}}) + } else { + conn.register(); + sampBtn.update({tooltip: {content: 'Disconnect'}}) + } + + self._hide() + } + }, aladin); + this.sampBtn = sampBtn; + this._attach(); + } _attach() { @@ -118,7 +136,7 @@ export class Settings extends ContextMenu { label: em, action(o) { let reticle = self.aladin.getReticle(); - reticle.setSize(pxSize); + reticle.update({size: pxSize}) } }) } @@ -169,6 +187,9 @@ export class Settings extends ContextMenu { self._attach(); } }, + { + label: Layout.horizontal({layout: [self.sampBtn, 'SAMP']}), + }, { label: 'Features', subMenu: [ diff --git a/src/js/gui/Toolbar/Controls/StackLayer/EditBox.js b/src/js/gui/Toolbar/Controls/StackLayer/EditBox.js index d1c8380b..01a25c30 100644 --- a/src/js/gui/Toolbar/Controls/StackLayer/EditBox.js +++ b/src/js/gui/Toolbar/Controls/StackLayer/EditBox.js @@ -122,7 +122,7 @@ import { CmapSelector } from "./ColormapSelector.js"; this.minCutInput = Input.number({ cssStyle: { padding: '0', - width: '4em', + width: '8ex', 'font-family': 'monospace', }, tooltip: {content: 'Min cut', position: {direction: 'bottom'}}, @@ -136,7 +136,7 @@ import { CmapSelector } from "./ColormapSelector.js"; this.maxCutInput = Input.number({ cssStyle: { padding: '0', - width: '4em', + width: '8ex', 'font-family': 'monospace', }, tooltip: {content: 'Max cut', position: {direction: 'bottom'}}, @@ -388,7 +388,7 @@ import { CmapSelector } from "./ColormapSelector.js"; ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, (e) => { const layerChanged = e.detail.layer; let selectedLayer = this.options.layer; - if (layerChanged.layer === selectedLayer.layer) { + if (selectedLayer && layerChanged.layer === selectedLayer.layer) { let colorCfg = selectedLayer.getColorCfg(); let cmap = colorCfg.getColormap(); diff --git a/src/js/gui/Toolbar/Controls/StackLayer/Menu.js b/src/js/gui/Toolbar/Controls/StackLayer/Menu.js index 4c63e4b1..685af1eb 100644 --- a/src/js/gui/Toolbar/Controls/StackLayer/Menu.js +++ b/src/js/gui/Toolbar/Controls/StackLayer/Menu.js @@ -29,7 +29,7 @@ *****************************************************************************/ import { LayerEditBox } from "./EditBox"; -import { FSM } from "../../../Utils.js"; +import { FSM } from "../../../../FiniteStateMachine"; import { Stack } from "./Stack.js"; import { DOMElement } from "../../../Widgets/Widget.js"; diff --git a/src/js/gui/Toolbar/Controls/StackLayer/Stack.js b/src/js/gui/Toolbar/Controls/StackLayer/Stack.js index 301e7196..70c5b54a 100644 --- a/src/js/gui/Toolbar/Controls/StackLayer/Stack.js +++ b/src/js/gui/Toolbar/Controls/StackLayer/Stack.js @@ -284,7 +284,7 @@ export class Stack extends ContextMenu { } hide() { - super._hide(); + super._hide(); } static singleton; diff --git a/src/js/gui/Toolbar/Menu.js b/src/js/gui/Toolbar/Menu.js index 7546c6f3..90308809 100644 --- a/src/js/gui/Toolbar/Menu.js +++ b/src/js/gui/Toolbar/Menu.js @@ -24,13 +24,14 @@ import { DOMElement } from "../Widgets/Widget"; /* Control import */ import { Settings } from "./Controls/Settings"; import { StackLayerMenu } from "./Controls/StackLayer/Menu"; -import { LayerEditBox } from "./Controls/StackLayer/EditBox"; +import { OverlayStack } from "./Controls/Overlays/Stack"; import { GotoBox } from "./Controls/GotoBox"; import { SimbadPointer } from "./Controls/SimbadPointer"; import { GridBox } from "./Controls/GridBox"; import settingsIcon from './../../../../assets/icons/settings.svg'; -import stackIcon from './../../../../assets/icons/stack.svg'; +import stackOverlayIconUrl from './../../../../assets/icons/stack.svg'; +import stackImageIconUrl from './../../../../assets/icons/telescope.svg'; import gridIcon from './../../../../assets/icons/grid.svg'; import searchIcon from './../../../../assets/icons/search.svg'; import restoreIcon from './../../../../assets/icons/restore.svg'; @@ -83,6 +84,7 @@ import { Utils } from "../Utils"; // Add the layers control if (aladin.options && aladin.options.showLayersControl) { this.appendControl('StackLayerMenu') + this.appendControl('OverlayStack') } // Add the simbad pointer control if (aladin.options && aladin.options.showSimbadPointerControl) { @@ -114,7 +116,7 @@ import { Utils } from "../Utils"; let menu = this; const controls = { StackLayerMenu: new ActionButton({ - iconURL: stackIcon, + iconURL: stackImageIconUrl, tooltip: { content: 'Open the stack layer menu', position: { direction: 'left'}, @@ -133,6 +135,26 @@ import { Utils } from "../Utils"; menu.showControl(StackLayerMenu) } }), + OverlayStack: new ActionButton({ + iconURL: stackOverlayIconUrl, + tooltip: { + content: 'Open the overlays menu', + position: { direction: 'left'}, + }, + cssStyle: { + padding: 0, + backgroundColor: '#bababa', + backgroundPosition: 'center', + borderColor: '#484848', + cursor: 'pointer', + width: '28px', + height: '28px' + + }, + action(o) { + menu.showControl(OverlayStack) + } + }), SimbadPointer: new SimbadPointer(aladin), GotoBox: new ActionButton({ iconURL: searchIcon, diff --git a/src/js/gui/Utils.js b/src/js/gui/Utils.js index 0655ef53..c39147a8 100644 --- a/src/js/gui/Utils.js +++ b/src/js/gui/Utils.js @@ -17,7 +17,6 @@ Element.prototype.swap = function (node) { parent.insertBefore(node, sibling); }; -import { DOMElement } from "./Widgets/Widget"; export let Utils = {} /** * Append el to target @@ -40,27 +39,4 @@ export let Utils = {} else high = mid; } return low; -} - -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]; - console.log(this.state, ' to ', to) - if (action) { - if (params) { - action(params); - } else { - action() - } - - this.state = to; - } - } -} +} \ No newline at end of file diff --git a/src/js/gui/widgets/ActionButton.js b/src/js/gui/widgets/ActionButton.js index 8383ff9d..d7cd01a7 100644 --- a/src/js/gui/widgets/ActionButton.js +++ b/src/js/gui/widgets/ActionButton.js @@ -80,11 +80,10 @@ export class ActionButton extends DOMElement { if (this.options.disable) { this.el.disabled = true; - this.el.style.cursor = "not-allowed"; - this.el.style.filter = 'brightness(70%)'; + this.addClass('disabled') } else { this.el.disabled = false; - this.el.style.cursor = 'pointer'; + this.removeClass('disabled') } // Add the content to the dom diff --git a/src/js/gui/widgets/Form.js b/src/js/gui/widgets/Form.js index 9ef2d710..c2d8ecd0 100644 --- a/src/js/gui/widgets/Form.js +++ b/src/js/gui/widgets/Form.js @@ -164,8 +164,8 @@ export class Form extends DOMElement { let headerEl = document.createElement('div'); headerEl.className = "aladin-form-group-header"; - Utils.appendTo(layout.header, headerEl); - Utils.appendTo(headerEl, groupEl); + DOMElement.appendTo(layout.header, headerEl); + groupEl.appendChild(headerEl); } layout.subInputs.forEach((subInput) => { diff --git a/src/js/gui/widgets/Selector.js b/src/js/gui/widgets/Selector.js index 6c8d6be4..ae593b0f 100644 --- a/src/js/gui/widgets/Selector.js +++ b/src/js/gui/widgets/Selector.js @@ -18,10 +18,10 @@ // import { DOMElement } from "./Widget"; -import { FSM } from "../Utils"; +import { FSM } from "../../FiniteStateMachine"; import { ActionButton } from "./ActionButton"; import { ContextMenu } from "./ContextMenu"; -import { Tooltip } from "./Tooltip"; + /****************************************************************************** * Aladin Lite project * diff --git a/src/js/gui/widgets/Tab.js b/src/js/gui/widgets/Tab.js index 4448ff33..279f2f13 100644 --- a/src/js/gui/widgets/Tab.js +++ b/src/js/gui/widgets/Tab.js @@ -45,9 +45,6 @@ export class Tabs extends DOMElement { * For the list of possibilities, see https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML */ constructor(options, target, position = "beforeend") { - let layout = options.layout; - let cssStyle = options.cssStyle; - let el = document.createElement("div"); el.classList.add('aladin-tabs'); @@ -59,13 +56,22 @@ export class Tabs extends DOMElement { let contentTabOptions = []; let tabsEl = []; - for (const tab of layout) { + for (const tab of options.layout) { // Create the content tab div let contentTabOptionEl = document.createElement("div"); contentTabOptionEl.classList.add('aladin-tabs-content-option'); contentTabOptionEl.style.display = 'none'; - Utils.appendTo(tab.content, contentTabOptionEl); + if (tab.content instanceof DOMElement) { + // And add it to the DOM + tab.content.attachTo(contentTabOptionEl); + } else if (opt.label instanceof Element) { + contentTabOptionEl.insertAdjacentElement('beforeend', tab.content); + } else { + let wrapEl = document.createElement('div'); + wrapEl.innerHTML = tab.content; + contentTabOptionEl.insertAdjacentElement('beforeend', wrapEl); + } contentTabOptions.push(contentTabOptionEl); @@ -102,11 +108,20 @@ export class Tabs extends DOMElement { tabEl.classList.add('aladin-tabs-head-tab-selected') }); - Utils.appendTo(tab.label, tabEl); - Utils.appendTo(tabEl, headerTabEl); + if (tab.label instanceof DOMElement) { + // And add it to the DOM + tab.label.attachTo(tabEl); + } else if (opt.label instanceof Element) { + tabEl.insertAdjacentElement('beforeend', tab.label); + } else { + let wrapEl = document.createElement('div'); + wrapEl.innerHTML = tab.label; + tabEl.insertAdjacentElement('beforeend', wrapEl); + } + headerTabEl.appendChild(tabEl); - if (tab.info) { - new Tooltip({content: tab.info}, tabEl); + if (tab.tooltip) { + Tooltip.add(tab.tooltip, tabEl) } } @@ -117,21 +132,27 @@ export class Tabs extends DOMElement { for(let contentTabOptionEl of contentTabOptions) { // Add it to the view - Utils.appendTo(contentTabOptionEl, contentTabEl); + contentTabEl.appendChild(contentTabOptionEl) } - Utils.appendTo(headerTabEl, el); - Utils.appendTo(contentTabEl, el); + el.appendChild(headerTabEl); + el.appendChild(contentTabEl); super(el, options); - - this.setCss(cssStyle) - this.attachTo(target, position); - this._show(); + + this.attachTo(target, position); } _show() { + if (this.options.cssStyle) { + this.setCss(this.options.cssStyle) + } + + if (this.options.tooltip) { + Tooltip.add(this.options.tooltip, this) + } + super._show(); } } diff --git a/src/js/gui/widgets/Table.js b/src/js/gui/widgets/Table.js index 96e46ba4..8d17e8ba 100644 --- a/src/js/gui/widgets/Table.js +++ b/src/js/gui/widgets/Table.js @@ -71,7 +71,11 @@ export class Table extends DOMElement { let showFieldCallback = opt.showCallback[key]; let el = showFieldCallback(row.data); - Utils.appendTo(el, tdEl); + if (el instanceof Element) { + tdEl.appendChild(el); + } else { + tdEl.innerHTML = el; + } } else { let val = row.data[key] || '--'; tdEl.innerHTML = val; @@ -87,47 +91,6 @@ export class Table extends DOMElement { return tbody; } - /*MeasurementTable.prototype.createTabs = function() { - let self = this; - let layout = []; - this.tables.forEach(function(table, index) { - let backgroundColor = table["color"]; - let hexStdColor = Color.standardizeColor(table["color"]); - let rgbColor = Color.hexToRgb(hexStdColor); - rgbColor = 'rgb(' + rgbColor.r + ', ' + rgbColor.g + ', ' + rgbColor.b + ')'; - let labelColor = Color.getLabelColorForBackground(rgbColor); - - let textContent = '
' + - table.name + '
'; - - let tabContent = Layout.horizontal(['
', textContent]); - - layout.push({ - label: tabContent, - title: table["name"], - cssStyle: { - backgroundColor: backgroundColor, - color: labelColor, - }, - content: tabContent - action(e) { - 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.updateTableBody() - } - }); - }); - - return new Tabs(layout, self.aladinLiteDiv); - }*/ - static _createTableHeader = function(opt) { let theadElement = document.createElement('thead'); var content = ''; diff --git a/src/js/gui/widgets/Tooltip.js b/src/js/gui/widgets/Tooltip.js index acf0cff6..73301a22 100644 --- a/src/js/gui/widgets/Tooltip.js +++ b/src/js/gui/widgets/Tooltip.js @@ -62,9 +62,12 @@ export class Tooltip extends DOMElement { // Set the anchor to the element on which // the tooltip is set - if (options.position) { - options.position.anchor = target; + if (!options.position) { + options.position = { + direction: 'right', + } } + options.position.anchor = target; super(wrapperEl, options) diff --git a/src/js/libs/samp.js b/src/js/libs/samp.js index 7f181a0b..9c3f5f64 100644 --- a/src/js/libs/samp.js +++ b/src/js/libs/samp.js @@ -710,7 +710,7 @@ export let samp = (function() { // Closes this connection. It unregisters from the hub if still // registered, but may harmlessly be called multiple times. Connection.prototype.close = function() { - var e; + var e, oc; if (this.closed) { return; } @@ -992,6 +992,8 @@ export let samp = (function() { } }; ClientTracker.prototype.init = function(connection) { + console.log('init client tracker') + var tracker = this; this.connection = connection; var retrieveInfo = function(id, type, infoFuncName, infoArray) { @@ -1001,11 +1003,14 @@ export let samp = (function() { }); }; connection.getRegisteredClients([], function(idlist) { + console.log(idlist) + var i; var id; tracker.ids = {}; for (i = 0; i < idlist.length; i++) { id = idlist[i]; + tracker.ids[id] = true; retrieveInfo(id, "meta", "getMetadata", tracker.metas); retrieveInfo(id, "subs", "getSubscriptions", tracker.subs); @@ -1082,6 +1087,7 @@ export let samp = (function() { } if (this.callableClient) { if (this.callableClient.init) { + console.log("init") this.callableClient.init(conn); } conn.setCallable(this.callableClient, function() { diff --git a/src/js/vo/Datalink.js b/src/js/vo/Datalink.js index 23726d02..e58ba33d 100644 --- a/src/js/vo/Datalink.js +++ b/src/js/vo/Datalink.js @@ -31,7 +31,7 @@ import { VOTable } from "./VOTable.js"; import { Utils } from './../Utils'; -import { ActionButton } from "../gui/widgets/ActionButton.js"; +import { ActionButton } from "../gui/Widgets/ActionButton.js"; import { Catalog } from "../Catalog.js"; export let Datalink = (function() { @@ -41,8 +41,8 @@ export let Datalink = (function() { this.sodaQueryWindow = undefined; }; - Datalink.prototype.handleActions = function(obscoreRow, aladinInstance) { - const url = obscoreRow["access_url"]; + Datalink.prototype.handleActions = function(url, obscoreRow, aladinInstance) { + //const url = obscoreRow["access_url"]; VOTable.parse( url, (rsc) => { @@ -81,7 +81,7 @@ export let Datalink = (function() { backgroundColor: '#bababa', borderColor: '#484848', }, - info: 'Open the cutout service form', + tooltip: {content: 'Open the cutout service form', position: {direction: 'top'}}, action(e) { aladinInstance.sodaQueryWindow.hide(); aladinInstance.sodaQueryWindow.setParams(self.SODAServerParams); @@ -109,6 +109,8 @@ export let Datalink = (function() { accessUrlEl.innerHTML = '' + url + ''; accessUrlEl.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); let processImageFitsClick = () => { var successCallback = ((ra, dec, fov, _) => { @@ -121,6 +123,11 @@ export let Datalink = (function() { }; switch (contentType) { + // A datalink response containing links to datasets or services attached to the current dataset + case 'application/x-votable+xml;content=datalink': + console.log("datalink recursive") + new Datalink().handleActions(url, obscoreRow, aladinInstance); + break; case 'application/hips': // Clic on a HiPS let survey = aladinInstance.newImageSurvey(url); diff --git a/src/js/vo/ObsCore.js b/src/js/vo/ObsCore.js index 1fa9a7af..6d0b4e7f 100644 --- a/src/js/vo/ObsCore.js +++ b/src/js/vo/ObsCore.js @@ -32,7 +32,7 @@ import { Datalink } from "./Datalink.js"; import { Utils } from '../Utils'; - import { ActionButton } from "../gui/widgets/ActionButton.js"; + import { ActionButton } from "../gui/Widgets/ActionButton.js"; export let ObsCore = (function() { @@ -220,7 +220,7 @@ switch (format) { // A datalink response containing links to datasets or services attached to the current dataset case 'application/x-votable+xml;content=datalink': - new Datalink().handleActions(data, aladinInstance); + new Datalink().handleActions(url, data, aladinInstance); break; // Any multidimensional regularly sampled FITS image or cube case 'image/fits': @@ -256,7 +256,7 @@ backgroundColor: '#bababa', borderColor: '#484848', }, - info: accessFormat, + tooltip: {content: accessFormat, position: {direction: 'left'}}, action(e) {} }).element(); } else { diff --git a/src/js/vo/samp.js b/src/js/vo/samp.js index a461d2eb..09610aab 100644 --- a/src/js/vo/samp.js +++ b/src/js/vo/samp.js @@ -29,14 +29,23 @@ *****************************************************************************/ import { ALEvent } from "../events/ALEvent"; -import { samp } from '../libs/samp'; +import { samp } from "../libs/samp"; import A from "../A"; export class SAMPConnector { + + static _createTag = (function() { + var count = 0; + return function() { + return "t-" + ++count; + }; + })(); + constructor(aladin) { // Define listeners let cc = new samp.ClientTracker(); let callHandler = cc.callHandler; + this.cc = cc; callHandler["script.aladin.send"] = function(senderId, message, isCall) { var params = message["samp.params"]; @@ -71,7 +80,9 @@ export class SAMPConnector { callHandler["table.load.votable"] = function(senderId, message, isCall) { let params = message["samp.params"]; - let {url, name} = params; + let id = params['table-id']; + let url = params['url']; + let name = params['name'] || id; A.catalogFromURL( url, @@ -84,40 +95,241 @@ export class SAMPConnector { ); }; + let selectCatalog = (id, url) => { + for (const cat of aladin.getOverlays()) { + if (cat.name === id || cat.url === url) { + return cat; + } + } + + return null; + } + callHandler["table.select.rowList"] = function(senderId, message, isCall) { + let params = message["samp.params"]; + + let id = params['table-id']; + let url = params['url']; + let rowList = params['row-list']; + + // search for the catalog + let catalog = selectCatalog(id, url) + + if (catalog) { + let objects = []; + for (const idx of rowList) { + objects.push(catalog.sources[idx]); + } + + aladin.selectObjects(objects); + } + }; + + callHandler["table.highlight.row"] = function(senderId, message, isCall) { + let params = message["samp.params"]; + + let id = params['table-id']; + let url = params['url']; + let row = params['row']; + + // search for the catalog + let catalog = selectCatalog(id, url) + + if (catalog) { + aladin.selectObjects([catalog.sources[row]]); + } + }; + let subs = cc.calculateSubscriptions(); let meta = { "samp.name": "Aladin Lite", - "samp.description": "Aladin Lite web visualizer SAMP connector" + "samp.description": "Aladin Lite web visualizer SAMP connector", + "samp.icon.url": "https://raw.githubusercontent.com/cds-astro/aladin-lite/master/assets/aladin-logo.gif" }; // Arrange for document to be adjusted for presence of hub every 2 sec. this.connector = new samp.Connector("Aladin Lite", meta, cc, subs); - /*window.addEventListener('load', (e) => { - this.connector.onHubAvailability((isHubRunning) => { - // Communicate to Aladin Lite - ALEvent.SAMP_AVAILABILITY.dispatchedTo(aladin.aladinDiv, { isHubRunning: isHubRunning } ); - }, 2000); - });*/ + //window.addEventListener('load', (e) => { + this.connector.onHubAvailability((isHubRunning) => { + if (this.isHubRunning !== isHubRunning) { + if (isHubRunning === false) { + // Reset the connector when the hub disconnects + console.log("jkjk") + this.unregister(); + } + } + this.isHubRunning = isHubRunning; + ALEvent.SAMP_HUB_RUNNING.dispatchedTo(aladin.aladinDiv, { isHubRunning } ); + }, 2000); + //}); + this.connected = false; + + // This is triggered when closing the web app window.addEventListener('unload', (e) => { - this.connector.unregister(); + this.unregister(); }); - // TODO put that in a button - //this.connector.register(); + // This is triggered when refreshing the page + window.addEventListener("beforeunload", (e) => { + this.unregister(); + }); + + this.aladin = aladin; + this.connectionAsked = false; + this.msg = {} } // Broadcasts a message given a hub connection. _send(mtype, params) { - // Provides execution of a SAMP operation with register-on-demand. - this.connector.runWithConnection( - (connection) => { - let msg = new samp.Message(mtype, params); - connection.notifyAll([msg]); - }, - (e) => { - window.alert(e) + this._pushMsgToAllClients(mtype, params); + + if (!this.connected) { + let warnMsg = 'Please connect the client. Go to menu Settings -> SAMP'; + alert(warnMsg); + throw warnMsg; + } + + if (this.sending) { + // We are waiting for acks to be received from the clients + return; + } + + this.sending = true; + + let conn = this.connector.connection; + + let sendMsgToAClient = (id) => { + // Check if the id is still valid + let validClient = this.cc.ids[id]; + if (!validClient) { + // Remove all the messages to that client + delete this.msg[id]; + // Do not call any messages + return; } - ) + + const msg = this.msg[id].shift(); + console.log('Send msg:', msg, ' to client ', id) + + let tag = SAMPConnector._createTag(); + + let doneFunc = (responderId, msgTag, response) => { + console.log('done func', responderId, msgTag, response) + delete this.cc.replyHandler[tag]; + + if (this._noMoreMsgToSend()) { + // We finished sending messages + this.sending = false; + return; + } + + // No more messages to send + if (this.msg[id].length === 0) { + // No more message to send for this client + return; + } + + // There are still messages to send for that client + sendMsgToAClient(id); + }; + + this.cc.replyHandler[tag] = doneFunc; + let errFunc = doneFunc; + + conn.call([id, tag, msg], null, errFunc); + } + + // Send the first message to all clients + for (const id in this.msg) { + sendMsgToAClient(id) + } + } + + _pushMsgToAllClients(mtype, params) { + // Create the message + const msg = new samp.Message(mtype, params); + + for (const id in this.cc.ids) { + if (id === 'hub') { + continue; + } + + // New client found ? create a new entry + if (!this.msg[id]) { + this.msg[id] = []; + } + + // Push the message to all clients + this.msg[id].push(msg); + } + } + + _noMoreMsgToSend() { + for (const id in this.msg) { + let validClient = this.cc.ids[id]; + if (!validClient) { + delete this.msg[id]; + } else { + if (this.msg[id] && this.msg[id].length > 0) { + return false; + } + } + } + + return true; + } + + unregister() { + this.connector.unregister(); + this.connected = false; + ALEvent.SAMP_DISCONNECTED.dispatchedTo(this.aladin.aladinDiv); + } + + register() { + new Promise((resolve, reject) => { + if (this.connected) { + if (this.connector.connection) { + return resolve(this.connector.connection) + } else { + return reject(); + } + } + + // It is not connected + if (this.connectionAsked === true) { + return reject('Connection is being asked'); + } + + // It is not connected and the connection has not + // been asked, thus we ask the user + + var regErrHandler = (err) => { + this.connected = false; + this.connectionAsked = false; + + reject(err); + }; + var regSuccessHandler = (conn) => { + this.connected = true; + this.connectionAsked = false; + + ALEvent.SAMP_CONNECTED.dispatchedTo(this.aladin.aladinDiv); + + resolve(conn) + }; + this.connectionAsked = true; + this.connector.runWithConnection( + regSuccessHandler, + regErrHandler + ); + }) + } + + isConnected() { + return this.connected; + } + + isHubCurrentlyRunning() { + return this.isHubRunning; } /** @@ -134,6 +346,34 @@ export class SAMPConnector { }) } + /** + * Highlight a row + * @param {String} url - URL of the VOTable document to load + * @param {String} [tableId] - Identifier which may be used to refer to the loaded table in subsequent messages + * @param {Integer} [row] - Row index (zero-based) of the row to highlight. + */ + tableSelectRowList(tableId, url, rowList) { + this._send("table.select.rowList", { + "table-id": tableId, + url, + "row-list": rowList + }) + } + + /** + * Highlight a row + * @param {String} url - URL of the VOTable document to load + * @param {String} [tableId] - Identifier which may be used to refer to the loaded table in subsequent messages + * @param {Integer} [row] - Row index (zero-based) of the row to highlight. + */ + highlightRowTable(tableId, url, row) { + this._send("table.highlight.row", { + "table-id": tableId, + url, + row + }) + } + /** * Load a fits image by url * @param {String} url - URL of the FITS image to load @@ -179,5 +419,9 @@ export class SAMPConnector { centerAtRaDec(ra, dec) { this._send("coord.pointAt.sky", { "ra": ra.toString(), "dec": dec.toString() }) } + + ping() { + this._send("samp.app.ping", {}) + } }