diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ef3f08..ad26cb46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## 3.4.3-beta * [bugfix] zoom control buttons +* [feat] save a MOC as a JSON file from the UI +* [feat] save a source selection from the measurement table as a CSV file +* [feat] allow to display jpeg/jpg images with a wcs passed as a JS dictionary: ## 3.4.2-beta diff --git a/assets/icons/HiPS.svg b/assets/icons/HiPS.svg new file mode 100644 index 00000000..50526a67 --- /dev/null +++ b/assets/icons/HiPS.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/examples/al-displayJPG.html b/examples/al-displayJPG.html index 265c5dc7..a15c4988 100644 --- a/examples/al-displayJPG.html +++ b/examples/al-displayJPG.html @@ -42,7 +42,7 @@ 'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg', // no options { - transparency: 0.6, + transparency: 1.0, }, // A callback fn once the overlay is set callback diff --git a/examples/al-image-with-WCS.html b/examples/al-image-with-WCS.html new file mode 100644 index 00000000..53c3f885 --- /dev/null +++ b/examples/al-image-with-WCS.html @@ -0,0 +1,46 @@ + + + + + +
+ + + \ No newline at end of file diff --git a/examples/al-init-custom-options.html b/examples/al-init-custom-options.html index 193f558e..bbf6154f 100644 --- a/examples/al-init-custom-options.html +++ b/examples/al-init-custom-options.html @@ -21,9 +21,9 @@ reticleColor: '#ff89ff', // change reticle color reticleSize: 64, // change reticle size showContextMenu: true, - showCooGrid: true, showFrame: true, - showZoomControl:true + showZoomControl:true, + showSettingsControl:true, } ); diff --git a/examples/al-moc-sdss9.html b/examples/al-moc-sdss9.html index e9e5413d..fda81b89 100644 --- a/examples/al-moc-sdss9.html +++ b/examples/al-moc-sdss9.html @@ -17,6 +17,8 @@ // moc is ready console.log(moc.contains(205.9019247, +2.4492764)); console.log(moc.contains(-205.9019247, +2.4492764)); + + console.log(moc.serialize("json")) }); var moc10 = A.MOCFromURL('https://alasky.unistra.fr/MocServer/query?ivorn=ivo%3A%2F%2FCDS%2FV%2F139%2Fsdss9&get=moc&order=11&fmt=fits', {color: '#ffffff', perimeter: true, fillColor: '#aabbcc', opacity: 0.3, lineWidth: 3}); var moc9 = A.MOCFromURL('https://alasky.unistra.fr/MocServer/query?ivorn=ivo%3A%2F%2FCDS%2FV%2F139%2Fsdss9&get=moc&order=4&fmt=fits', {color: '#00ff00', opacity: 0.5, lineWidth: 3, perimeter: true}); diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index ab69560c..b5795fae 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -28,6 +28,8 @@ async-channel = "1.8.0" mapproj = "0.3.0" fitsrs = "0.2.9" wcs = { git = "https://github.com/cds-astro/wcs-rs", branch = 'master' } +#wcs = { path = "../../../wcs" } + colorgrad = "0.6.2" [features] diff --git a/src/core/al-api/src/fov.rs b/src/core/al-api/src/fov.rs index 177b08ac..9b00e26d 100644 --- a/src/core/al-api/src/fov.rs +++ b/src/core/al-api/src/fov.rs @@ -2,8 +2,7 @@ use wasm_bindgen::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -#[derive(Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[wasm_bindgen] pub struct CenteredFoV { /// Position of the field of view @@ -12,4 +11,4 @@ pub struct CenteredFoV { /// Aperture pub fov: f64, -} \ No newline at end of file +} diff --git a/src/core/al-api/src/hips.rs b/src/core/al-api/src/hips.rs index a970f4bc..b2913880 100644 --- a/src/core/al-api/src/hips.rs +++ b/src/core/al-api/src/hips.rs @@ -25,16 +25,6 @@ impl HiPSCfg { } } -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct FITSCfg { - /// Layer name - pub layer: String, - pub url: String, - /// Its color - pub meta: ImageMetadata, -} - use crate::coo_system::CooSystem; #[derive(Deserialize, Debug, Clone)] diff --git a/src/core/al-api/src/image.rs b/src/core/al-api/src/image.rs index c1fea664..2755204a 100644 --- a/src/core/al-api/src/image.rs +++ b/src/core/al-api/src/image.rs @@ -3,16 +3,10 @@ use serde::{Deserialize, Serialize}; use crate::fov::CenteredFoV; // This struct is intended to be returned // to the javascript to create a layer based on it -#[derive(Deserialize, Serialize)] -#[derive(Clone)] +#[derive(Deserialize, Serialize, Clone)] pub struct ImageParams { pub centered_fov: CenteredFoV, - // a new layer - pub layer: String, - // and its url - pub url: String, - - pub automatic_min_cut: f32, - pub automatic_max_cut: f32, -} \ No newline at end of file + pub min_cut: Option, + pub max_cut: Option, +} diff --git a/src/core/al-core/src/convert.rs b/src/core/al-core/src/convert.rs new file mode 100644 index 00000000..9cce9185 --- /dev/null +++ b/src/core/al-core/src/convert.rs @@ -0,0 +1,32 @@ +pub trait Cast: Clone + Copy { + fn cast(self) -> T; +} + +impl Cast for u8 { + fn cast(self) -> f32 { + self as f32 + } +} +impl Cast for i16 { + fn cast(self) -> f32 { + self as f32 + } +} + +impl Cast for i32 { + fn cast(self) -> f32 { + self as f32 + } +} + +impl Cast for f32 { + fn cast(self) -> f32 { + self + } +} + +impl Cast for f64 { + fn cast(self) -> f32 { + self as f32 + } +} diff --git a/src/core/al-core/src/image/format.rs b/src/core/al-core/src/image/format.rs index e1c25afb..569580af 100644 --- a/src/core/al-core/src/image/format.rs +++ b/src/core/al-core/src/image/format.rs @@ -16,6 +16,8 @@ pub trait ImageFormat { const INTERNAL_FORMAT: i32; const TYPE: u32; + const CHANNEL_TYPE: ChannelType; + /// Creates a JS typed array which is a view into wasm's linear memory at the slice specified. /// This function returns a new typed array which is a view into wasm's memory. This view does not copy the underlying data. /// @@ -42,6 +44,8 @@ impl ImageFormat for RGB8U { const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::RGB as i32; const TYPE: u32 = WebGlRenderingCtx::UNSIGNED_BYTE; + const CHANNEL_TYPE: ChannelType = ChannelType::RGB8U; + fn decode(raw_bytes: &[u8]) -> Result, &'static str> { let mut decoder = jpeg::Decoder::new(raw_bytes); let bytes = decoder @@ -70,30 +74,7 @@ impl ImageFormat for RGBA8U { const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::RGBA as i32; const TYPE: u32 = WebGlRenderingCtx::UNSIGNED_BYTE; - fn decode(raw_bytes: &[u8]) -> Result, &'static str> { - let mut decoder = jpeg::Decoder::new(raw_bytes); - let bytes = decoder - .decode() - .map_err(|_| "Cannot decoder png. This image may not be compressed.")?; - - Ok(Bytes::Owned(bytes)) - } - - type ArrayBufferView = js_sys::Uint8Array; - - unsafe fn view(s: &[::Item]) -> Self::ArrayBufferView { - Self::ArrayBufferView::view(s) - } -} -#[cfg(feature = "webgl1")] -impl ImageFormat for RGBA8U { - type P = [u8; 4]; - - const NUM_CHANNELS: usize = 4; - - const FORMAT: u32 = WebGlRenderingCtx::RGBA as u32; - const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::RGBA as i32; - const TYPE: u32 = WebGlRenderingCtx::UNSIGNED_BYTE; + const CHANNEL_TYPE: ChannelType = ChannelType::RGBA8U; fn decode(raw_bytes: &[u8]) -> Result, &'static str> { let mut decoder = jpeg::Decoder::new(raw_bytes); @@ -125,6 +106,8 @@ impl ImageFormat for RGBA32F { #[cfg(feature = "webgl1")] const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::RGBA as i32; + const CHANNEL_TYPE: ChannelType = ChannelType::RGBA32F; + const TYPE: u32 = WebGlRenderingCtx::FLOAT; fn decode(raw_bytes: &[u8]) -> Result, &'static str> { @@ -151,6 +134,8 @@ impl ImageFormat for RGB32F { #[cfg(feature = "webgl1")] const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::RGB as i32; + const CHANNEL_TYPE: ChannelType = ChannelType::RGB32F; + const TYPE: u32 = WebGlRenderingCtx::FLOAT; fn decode(raw_bytes: &[u8]) -> Result, &'static str> { @@ -183,6 +168,8 @@ impl ImageFormat for R32F { const TYPE: u32 = WebGlRenderingCtx::FLOAT; + const CHANNEL_TYPE: ChannelType = ChannelType::R32F; + fn decode(raw_bytes: &[u8]) -> Result, &'static str> { Ok(Bytes::Borrowed(raw_bytes)) } @@ -215,6 +202,8 @@ impl ImageFormat for R64F { const TYPE: u32 = WebGlRenderingCtx::FLOAT; + const CHANNEL_TYPE: ChannelType = ChannelType::R64F; + fn decode(raw_bytes: &[u8]) -> Result, &'static str> { Ok(Bytes::Borrowed(raw_bytes)) } @@ -239,6 +228,8 @@ impl ImageFormat for R8UI { const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::R8UI as i32; const TYPE: u32 = WebGlRenderingCtx::UNSIGNED_BYTE; + const CHANNEL_TYPE: ChannelType = ChannelType::R8UI; + fn decode(raw_bytes: &[u8]) -> Result, &'static str> { Ok(Bytes::Borrowed(raw_bytes)) } @@ -262,6 +253,7 @@ impl ImageFormat for R16I { const FORMAT: u32 = WebGlRenderingCtx::RED_INTEGER as u32; const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::R16I as i32; const TYPE: u32 = WebGlRenderingCtx::SHORT; + const CHANNEL_TYPE: ChannelType = ChannelType::R16I; fn decode(raw_bytes: &[u8]) -> Result, &'static str> { Ok(Bytes::Borrowed(raw_bytes)) @@ -287,6 +279,8 @@ impl ImageFormat for R32I { const INTERNAL_FORMAT: i32 = WebGlRenderingCtx::R32I as i32; const TYPE: u32 = WebGlRenderingCtx::INT; + const CHANNEL_TYPE: ChannelType = ChannelType::R32I; + fn decode(raw_bytes: &[u8]) -> Result, &'static str> { Ok(Bytes::Borrowed(raw_bytes)) } diff --git a/src/core/al-core/src/lib.rs b/src/core/al-core/src/lib.rs index 152455c1..1a8b84cc 100644 --- a/src/core/al-core/src/lib.rs +++ b/src/core/al-core/src/lib.rs @@ -1,9 +1,10 @@ +extern crate futures; extern crate jpeg_decoder as jpeg; extern crate png; extern crate serde_json; -extern crate futures; extern crate wasm_streams; +pub mod convert; pub mod image; mod object; pub mod shader; @@ -17,8 +18,8 @@ pub mod colormap; pub use colormap::{Colormap, Colormaps}; pub use texture::pixel; -pub use texture::{Texture2D, Texture2DBound}; pub use texture::Texture2DArray; +pub use texture::{Texture2D, Texture2DBound}; pub use webgl_ctx::WebGlContext; @@ -36,12 +37,14 @@ pub use object::vertex_array_object::vao::{ pub trait Abort { type Item; - fn unwrap_abort(self) -> Self::Item where Self: Sized; + fn unwrap_abort(self) -> Self::Item + where + Self: Sized; } impl Abort for Option { type Item = T; - + #[inline] fn unwrap_abort(self) -> Self::Item { use std::process; diff --git a/src/core/al-core/src/log.rs b/src/core/al-core/src/log.rs index 498f56b8..ecbc9962 100644 --- a/src/core/al-core/src/log.rs +++ b/src/core/al-core/src/log.rs @@ -23,30 +23,3 @@ pub fn console_warn(s: impl Into) { pub fn console_error(s: impl Into) { web_sys::console::error_1(&s.into()); } - -#[macro_export] -macro_rules! log { - // The pattern for a single `eval` - ($($arg:tt)*) => { - $( self::log(&format!("{:?}", $arg)); )* - }; -} - -#[macro_export] -macro_rules! inforec { - // The pattern for a single `eval` - // Base case: - ($x:tt) => (format!("{:?}", $x)); - // `$x` followed by at least one `$y,` - ($x:tt, $($y:tt),+) => { - // Call `find_min!` on the tail `$y` - ( format!( "{} {}", inforec!($x), inforec!($($y),+) ) ); - } -} - -#[macro_export] -macro_rules! info { - ($($arg:tt),*) => { - self::log( &inforec!( $( $arg ),* ) ); - }; -} diff --git a/src/core/al-core/src/object/array_buffer.rs b/src/core/al-core/src/object/array_buffer.rs index ab835a96..d854c892 100644 --- a/src/core/al-core/src/object/array_buffer.rs +++ b/src/core/al-core/src/object/array_buffer.rs @@ -5,6 +5,8 @@ use super::buffer_data::BufferDataStorage; pub trait VertexBufferObject { fn bind(&self); + + #[allow(dead_code)] fn unbind(&self); } @@ -45,7 +47,10 @@ pub trait VertexAttribPointerType: std::marker::Sized { usage: u32, data: B, ) -> WebGlBuffer { - let buffer = gl.create_buffer().ok_or("failed to create buffer").unwrap_abort(); + let buffer = gl + .create_buffer() + .ok_or("failed to create buffer") + .unwrap_abort(); // Bind the buffer gl.bind_buffer(WebGlRenderingCtx::ARRAY_BUFFER, Some(buffer.as_ref())); @@ -311,8 +316,7 @@ impl VertexAttribPointerType for f32 { let len = data.len(); let ptr = data.as_ptr() as u32 / 4; - Float32Array::new(&memory_buffer) - .subarray(ptr, ptr + len as u32) + Float32Array::new(&memory_buffer).subarray(ptr, ptr + len as u32) } fn buffer_sub_data_with_i32_and_array_buffer_view<'a, B: BufferDataStorage<'a, Self>>( diff --git a/src/core/al-core/src/texture/pixel.rs b/src/core/al-core/src/texture/pixel.rs index bdc8a845..e724078a 100644 --- a/src/core/al-core/src/texture/pixel.rs +++ b/src/core/al-core/src/texture/pixel.rs @@ -4,13 +4,15 @@ use wasm_bindgen::JsValue; use crate::webgl_ctx::WebGlContext; pub trait Pixel: - AsRef<[Self::Item]> - + Default - + std::cmp::PartialEq - + std::fmt::Debug - + std::clone::Clone + AsRef<[Self::Item]> + Default + std::cmp::PartialEq + std::fmt::Debug + std::clone::Clone { - type Item: std::cmp::PartialOrd + Clone + Copy + std::fmt::Debug + cgmath::Zero; + type Item: std::cmp::PartialOrd + + Clone + + Copy + + std::fmt::Debug + + cgmath::Zero + + std::cmp::PartialEq + + crate::convert::Cast; type Container: ArrayBuffer; const BLACK: Self; @@ -248,4 +250,4 @@ impl Pixel for [i32; 1] { Ok([pixels.to_vec()[0]]) } -} \ No newline at end of file +} diff --git a/src/core/src/app.rs b/src/core/src/app.rs index 14788346..abef60fc 100644 --- a/src/core/src/app.rs +++ b/src/core/src/app.rs @@ -1,3 +1,4 @@ +use crate::renderable::ImageLayer; use crate::{ //async_task::{BuildCatalogIndex, ParseTableTask, TaskExecutor, TaskResult, TaskType}, camera::CameraViewPort, @@ -11,13 +12,12 @@ use crate::{ }, renderable::grid::ProjetedGrid, renderable::Layers, - renderable::{ - catalog::Manager, line::RasterizedLineRenderer, moc::MOCRenderer, ImageCfg, Renderer, - }, + renderable::{catalog::Manager, line::RasterizedLineRenderer, moc::MOCRenderer, Renderer}, shader::ShaderManager, tile_fetcher::TileFetcherQueue, time::DeltaTime, }; +use wcs::WCS; use wasm_bindgen::prelude::*; @@ -29,11 +29,10 @@ use crate::Abort; use al_api::{ coo_system::CooSystem, grid::GridCfg, - hips::{FITSCfg, HiPSCfg, ImageMetadata}, + hips::{HiPSCfg, ImageMetadata}, }; use cgmath::Vector4; use fitsrs::{fits::AsyncFits, hdu::extension::AsyncXtensionHDU}; -use wasm_bindgen_futures::JsFuture; use web_sys::{HtmlElement, WebGl2RenderingContext}; @@ -97,11 +96,11 @@ pub struct App { pub projection: ProjectionType, // Async data receivers - fits_send: async_channel::Sender, - fits_recv: async_channel::Receiver, + img_send: async_channel::Sender, + img_recv: async_channel::Receiver, - ack_send: async_channel::Sender, - ack_recv: async_channel::Receiver, + ack_img_send: async_channel::Sender, + ack_img_recv: async_channel::Receiver, // callbacks //callback_position_changed: js_sys::Function, } @@ -194,8 +193,8 @@ impl App { let moc = MOCRenderer::new(&gl)?; gl.clear_color(0.15, 0.15, 0.15, 1.0); - let (fits_send, fits_recv) = async_channel::unbounded::(); - let (ack_send, ack_recv) = async_channel::unbounded::(); + let (img_send, img_recv) = async_channel::unbounded::(); + let (ack_img_send, ack_img_recv) = async_channel::unbounded::(); let line_renderer = RasterizedLineRenderer::new(&gl)?; @@ -253,11 +252,10 @@ impl App { colormaps, projection, - fits_send, - fits_recv, - ack_send, - ack_recv, - //callback_position_changed, + img_send, + img_recv, + ack_img_send, + ack_img_recv, }) } @@ -460,7 +458,7 @@ impl App { let mut j = c.len() - 1; for i in 0..c.len() { - if crate::math::vector::dist2(&c[j], &c[i]) > 0.04 { + if crate::math::vector::dist2(&c[j], &c[i]) > 0.05 { return None; } @@ -828,16 +826,16 @@ impl App { //} // Check for async retrieval - if let Ok(fits) = self.fits_recv.try_recv() { - let params = fits.get_params(); + if let Ok(img) = self.img_recv.try_recv() { + let params = img.get_params(); self.layers - .add_image_fits(fits, &mut self.camera, &self.projection)?; + .add_image(img, &mut self.camera, &self.projection)?; self.request_redraw = true; // Send the ack to the js promise so that she finished - let ack_send = self.ack_send.clone(); + let ack_img_send = self.ack_img_send.clone(); wasm_bindgen_futures::spawn_local(async move { - ack_send.send(params).await.unwrap_throw(); + ack_img_send.send(params).await.unwrap_throw(); }) } @@ -1022,41 +1020,127 @@ impl App { Ok(()) } - pub(crate) fn add_image_fits(&mut self, cfg: FITSCfg) -> Result { - let FITSCfg { layer, url, meta } = cfg; + pub(crate) fn add_image_from_blob_and_wcs( + &mut self, + layer: String, + stream: web_sys::ReadableStream, + wcs: WCS, + cfg: ImageMetadata, + ) -> Result { let gl = self.gl.clone(); - let fits_sender = self.fits_send.clone(); - let ack_recv = self.ack_recv.clone(); + let img_sender = self.img_send.clone(); + let ack_img_recv = self.ack_img_recv.clone(); // Stop the current inertia self.inertia = None; // And disable it while the fits has not been loaded let disable_inertia = self.disable_inertia.clone(); *(disable_inertia.borrow_mut()) = true; + let camera_coo_sys = self.camera.get_coo_system(); + let fut = async move { use crate::renderable::image::Image; use futures::future::Either; use futures::TryStreamExt; use js_sys::Uint8Array; use wasm_streams::ReadableStream; - use web_sys::window; - use web_sys::Response; - use web_sys::{Request, RequestInit, RequestMode}; - let mut opts = RequestInit::new(); - opts.method("GET"); - opts.mode(RequestMode::Cors); + let body = ReadableStream::from_raw(stream.dyn_into()?); - let window = window().unwrap(); - let request = Request::new_with_str_and_init(&url, &opts)?; + // Convert the JS ReadableStream to a Rust stream + let bytes_reader = match body.try_into_async_read() { + Ok(async_read) => Either::Left(async_read), + Err((_err, body)) => Either::Right( + body.into_stream() + .map_ok(|js_value| { + js_value.dyn_into::().unwrap_throw().to_vec() + }) + .map_err(|_js_error| { + std::io::Error::new(std::io::ErrorKind::Other, "failed to read") + }) + .into_async_read(), + ), + }; + use al_core::image::format::RGBA8U; + match Image::from_reader_and_wcs::<_, RGBA8U>( + &gl, + bytes_reader, + wcs, + None, + None, + None, + camera_coo_sys, + ) + .await + { + Ok(image) => { + let img = ImageLayer { + images: vec![image], + id: layer.clone(), + layer, + meta: cfg, + }; + + img_sender.send(img).await.unwrap(); + + // Wait for the ack here + let image_params = ack_img_recv + .recv() + .await + .map_err(|_| JsValue::from_str("Problem receiving fits"))?; + + serde_wasm_bindgen::to_value(&image_params).map_err(|e| e.into()) + } + Err(error) => Err(error), + } + }; - let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; - let resp: Response = resp_value.dyn_into()?; + let reenable_inertia = Closure::new(move || { + // renable inertia again + *(disable_inertia.borrow_mut()) = false; + }); + + let promise = wasm_bindgen_futures::future_to_promise(fut) + // Reenable inertia independantly from whether the + // fits has been correctly parsed or not + .finally(&reenable_inertia); + + // forget the closure, it is not very proper to do this as + // it won't be deallocated + reenable_inertia.forget(); + + Ok(promise) + } + + pub(crate) fn add_image_fits( + &mut self, + id: String, + stream: web_sys::ReadableStream, + meta: ImageMetadata, + layer: String, + ) -> Result { + let gl = self.gl.clone(); + + let fits_sender = self.img_send.clone(); + let ack_fits_recv = self.ack_img_recv.clone(); + // Stop the current inertia + self.inertia = None; + // And disable it while the fits has not been loaded + let disable_inertia = self.disable_inertia.clone(); + *(disable_inertia.borrow_mut()) = true; + + let camera_coo_sys = self.camera.get_coo_system(); + + let fut = async move { + use crate::renderable::image::Image; + use futures::future::Either; + use futures::TryStreamExt; + use js_sys::Uint8Array; + use wasm_streams::ReadableStream; // Get the response's body as a JS ReadableStream - let raw_body = resp.body().unwrap(); - let body = ReadableStream::from_raw(raw_body.dyn_into()?); + let body = ReadableStream::from_raw(stream.dyn_into()?); // Convert the JS ReadableStream to a Rust stream let bytes_reader = match body.try_into_async_read() { @@ -1080,29 +1164,11 @@ impl App { .map_err(|e| JsValue::from_str(&format!("Fits file parsing: reason: {}", e)))?; let mut hdu_ext_idx = 0; - let mut images_params = vec![]; + let mut images = vec![]; - match Image::from_fits_hdu_async(&gl, &mut hdu.0).await { + match Image::from_fits_hdu_async(&gl, &mut hdu.0, camera_coo_sys).await { Ok(image) => { - let layer_ext = layer.clone(); - let url_ext = url.clone(); - - let fits = ImageCfg { - image: image, - layer: layer_ext, - url: url_ext, - meta: meta.clone(), - }; - - fits_sender.send(fits).await.unwrap(); - - // Wait for the ack here - let image_params = ack_recv - .recv() - .await - .map_err(|_| JsValue::from_str("Problem receiving fits"))?; - - images_params.push(image_params); + images.push(image); let mut hdu_ext = hdu.next().await; @@ -1110,27 +1176,11 @@ impl App { while let Ok(Some(mut xhdu)) = hdu_ext { match &mut xhdu { AsyncXtensionHDU::Image(xhdu_img) => { - match Image::from_fits_hdu_async(&gl, xhdu_img).await { + match Image::from_fits_hdu_async(&gl, xhdu_img, camera_coo_sys) + .await + { Ok(image) => { - let layer_ext = - layer.clone() + "_ext_" + &format!("{hdu_ext_idx}"); - let url_ext = - url.clone() + "_ext_" + &format!("{hdu_ext_idx}"); - - let fits_ext = ImageCfg { - image: image, - layer: layer_ext, - url: url_ext, - meta: meta.clone(), - }; - - fits_sender.send(fits_ext).await.unwrap(); - - let image_params = ack_recv.recv().await.map_err(|_| { - JsValue::from_str("Problem receving fits") - })?; - - images_params.push(image_params); + images.push(image); } Err(error) => { al_core::log::console_warn(& @@ -1161,27 +1211,11 @@ impl App { while let Ok(Some(mut xhdu)) = hdu_ext { match &mut xhdu { AsyncXtensionHDU::Image(xhdu_img) => { - match Image::from_fits_hdu_async(&gl, xhdu_img).await { + match Image::from_fits_hdu_async(&gl, xhdu_img, camera_coo_sys) + .await + { Ok(image) => { - let layer_ext = - layer.clone() + "_ext_" + &format!("{hdu_ext_idx}"); - let url_ext = - url.clone() + "_ext_" + &format!("{hdu_ext_idx}"); - - let fits_ext = ImageCfg { - image: image, - layer: layer_ext, - url: url_ext, - meta: meta.clone(), - }; - - fits_sender.send(fits_ext).await.unwrap(); - - let image_params = ack_recv.recv().await.map_err(|_| { - JsValue::from_str("Problem receving fits") - })?; - - images_params.push(image_params); + images.push(image); } Err(error) => { al_core::log::console_warn(& @@ -1206,10 +1240,25 @@ impl App { } } - if !images_params.is_empty() { - serde_wasm_bindgen::to_value(&images_params).map_err(|e| e.into()) + if images.is_empty() { + Err(JsValue::from_str("no images have been parsed")) } else { - Err(JsValue::from_str("The fits could not be parsed")) + let fits = ImageLayer { + images, + layer, + id, + meta, + }; + + fits_sender.send(fits).await.unwrap(); + + // Wait for the ack here + let image_params = ack_fits_recv + .recv() + .await + .map_err(|_| JsValue::from_str("Problem receiving fits"))?; + + serde_wasm_bindgen::to_value(&image_params).map_err(|e| e.into()) } }; diff --git a/src/core/src/camera/fov.rs b/src/core/src/camera/fov.rs index 5a1a150b..0af726fa 100644 --- a/src/core/src/camera/fov.rs +++ b/src/core/src/camera/fov.rs @@ -162,6 +162,10 @@ impl FieldOfView { self.reg.intersects_meridian(lon) } + pub fn intersects_region(&self, reg: &Region) -> bool { + self.reg.intersects(reg) + } + /*pub fn intersects_great_circle(&self, n: &Vector3) -> Intersection { self.reg.intersects_great_circle(n) }*/ diff --git a/src/core/src/downloader/query.rs b/src/core/src/downloader/query.rs index 14a7abd2..a79ca7a3 100644 --- a/src/core/src/downloader/query.rs +++ b/src/core/src/downloader/query.rs @@ -4,7 +4,6 @@ use super::request::RequestType; pub trait Query: Sized { type Request: From + Into; - fn hips_cdid(&self) -> &CreatorDid; fn id(&self) -> &QueryId; } @@ -59,10 +58,6 @@ use super::request::tile::TileRequest; impl Query for Tile { type Request = TileRequest; - fn hips_cdid(&self) -> &CreatorDid { - &self.hips_cdid - } - fn id(&self) -> &QueryId { &self.id } @@ -107,10 +102,6 @@ use super::request::allsky::AllskyRequest; impl Query for Allsky { type Request = AllskyRequest; - fn hips_cdid(&self) -> &CreatorDid { - &self.hips_cdid - } - fn id(&self) -> &QueryId { &self.id } @@ -148,10 +139,6 @@ use super::request::blank::PixelMetadataRequest; impl Query for PixelMetadata { type Request = PixelMetadataRequest; - fn hips_cdid(&self) -> &CreatorDid { - &self.hips_cdid - } - fn id(&self) -> &QueryId { &self.id } @@ -178,10 +165,6 @@ use super::request::moc::MOCRequest; impl Query for Moc { type Request = MOCRequest; - fn hips_cdid(&self) -> &CreatorDid { - &self.hips_cdid - } - fn id(&self) -> &QueryId { &self.url } diff --git a/src/core/src/downloader/request/allsky.rs b/src/core/src/downloader/request/allsky.rs index a53f4b24..35fd2cb9 100644 --- a/src/core/src/downloader/request/allsky.rs +++ b/src/core/src/downloader/request/allsky.rs @@ -24,7 +24,8 @@ impl From for RequestType { } } -use crate::renderable::Url; +use super::Url; + use wasm_bindgen_futures::JsFuture; use web_sys::{RequestInit, RequestMode, Response}; diff --git a/src/core/src/downloader/request/blank.rs b/src/core/src/downloader/request/blank.rs index eb95d25f..2e7c2768 100644 --- a/src/core/src/downloader/request/blank.rs +++ b/src/core/src/downloader/request/blank.rs @@ -38,7 +38,7 @@ impl From for RequestType { } } -use crate::renderable::Url; +use super::Url; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; diff --git a/src/core/src/downloader/request/moc.rs b/src/core/src/downloader/request/moc.rs index 5664db94..43be0e98 100644 --- a/src/core/src/downloader/request/moc.rs +++ b/src/core/src/downloader/request/moc.rs @@ -19,7 +19,8 @@ impl From for RequestType { RequestType::Moc(request) } } -use crate::renderable::Url; +use super::Url; + use moclib::deser::fits; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; diff --git a/src/core/src/downloader/request/mod.rs b/src/core/src/downloader/request/mod.rs index 0fa79db4..860b92f8 100644 --- a/src/core/src/downloader/request/mod.rs +++ b/src/core/src/downloader/request/mod.rs @@ -11,6 +11,7 @@ use crate::time::Time; use std::cell::Cell; use std::rc::Rc; use std::sync::{Arc, Mutex}; +pub type Url = String; pub struct Request { data: Arc>>, time_request: Time, diff --git a/src/core/src/downloader/request/tile.rs b/src/core/src/downloader/request/tile.rs index 4b4b579b..65b2ecbf 100644 --- a/src/core/src/downloader/request/tile.rs +++ b/src/core/src/downloader/request/tile.rs @@ -5,6 +5,7 @@ use al_core::image::format::{ChannelType, ImageFormatType, RGB8U, RGBA8U}; use crate::downloader::query; use al_core::image::ImageType; +use super::Url; use super::{Request, RequestType}; use crate::downloader::QueryId; @@ -44,7 +45,6 @@ async fn query_html_image(url: &str) -> Result { Ok(image) } -use crate::renderable::Url; use al_core::image::html::HTMLImage; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index ea0a366b..4da56537 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -20,6 +20,15 @@ #[cfg(feature = "dbg")] use std::panic; +#[macro_export] +macro_rules! log { + // The pattern for a single `eval` + ($arg:tt) => { + let s = format!("{:?}", $arg); + web_sys::console::log_1(&s.into()); + }; +} + pub trait Abort { type Item; fn unwrap_abort(self) -> Self::Item @@ -76,6 +85,7 @@ mod utils; use math::projection::*; +use moclib::moc::RangeMOCIntoIterator; //use votable::votable::VOTableWrapper; use wasm_bindgen::prelude::*; use web_sys::HtmlElement; @@ -112,7 +122,6 @@ use std::io::Cursor; use al_api::color::{Color, ColorRGBA}; use al_api::coo_system::CooSystem; -use al_api::hips::FITSCfg; use al_api::hips::HiPSProperties; use al_core::colormap::Colormaps; @@ -371,10 +380,33 @@ impl WebClient { } #[wasm_bindgen(js_name = addImageFITS)] - pub fn add_image_fits(&mut self, fits_cfg: JsValue) -> Result { - let fits_cfg: FITSCfg = serde_wasm_bindgen::from_value(fits_cfg)?; + pub fn add_image_fits( + &mut self, + id: String, + stream: web_sys::ReadableStream, + cfg: JsValue, + layer: String, + ) -> Result { + let cfg: ImageMetadata = serde_wasm_bindgen::from_value(cfg)?; + //let wcs: Option = serde_wasm_bindgen::from_value(wcs)?; + + self.app.add_image_fits(id, stream, cfg, layer) + } - self.app.add_image_fits(fits_cfg) + #[wasm_bindgen(js_name = addImageWithWCS)] + pub fn add_image_with_wcs( + &mut self, + data: web_sys::ReadableStream, + wcs: JsValue, + cfg: JsValue, + layer: String, + ) -> Result { + use wcs::{WCSParams, WCS}; + let cfg: ImageMetadata = serde_wasm_bindgen::from_value(cfg)?; + let wcs_params: WCSParams = serde_wasm_bindgen::from_value(wcs)?; + let wcs = WCS::new(&wcs_params).map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; + + self.app.add_image_from_blob_and_wcs(layer, data, wcs, cfg) } #[wasm_bindgen(js_name = removeLayer)] @@ -1104,6 +1136,28 @@ impl WebClient { Ok(moc.contains_lonlat(&location)) } + #[wasm_bindgen(js_name = mocSerialize)] + pub fn moc_serialize( + &mut self, + params: &al_api::moc::MOC, + _format: String, // todo support the fits/ascii serialization + ) -> Result { + let moc = self + .app + .get_moc(params) + .ok_or_else(|| JsValue::from(js_sys::Error::new("MOC not found")))?; + + let mut buf: Vec = Default::default(); + let json = (&moc.0) + .into_range_moc_iter() + .cells() + .to_json_aladin(None, &mut buf) + .map(|()| unsafe { String::from_utf8_unchecked(buf) }) + .map_err(|err| JsValue::from_str(&format!("{:?}", err)))?; + + serde_wasm_bindgen::to_value(&json).map_err(|err| JsValue::from_str(&format!("{:?}", err))) + } + #[wasm_bindgen(js_name = getMOCSkyFraction)] pub fn get_moc_sky_fraction(&mut self, params: &al_api::moc::MOC) -> f32 { if let Some(moc) = self.app.get_moc(params) { diff --git a/src/core/src/math/sph_geom/bbox.rs b/src/core/src/math/sph_geom/bbox.rs index ae3fc4ac..8cd87f0d 100644 --- a/src/core/src/math/sph_geom/bbox.rs +++ b/src/core/src/math/sph_geom/bbox.rs @@ -1,16 +1,18 @@ -use super::super::{ZERO, PI, HALF_PI, TWICE_PI, MINUS_HALF_PI}; -use crate::math::{sph_geom::region::PoleContained, lonlat::LonLatT}; +use super::super::{HALF_PI, MINUS_HALF_PI, PI, TWICE_PI, ZERO}; +use crate::math::{lonlat::LonLatT, sph_geom::region::PoleContained}; pub const ALLSKY_BBOX: BoundingBox = BoundingBox { lon: ZERO..TWICE_PI, lat: MINUS_HALF_PI..HALF_PI, + intersect_zero_meridian: true, }; -use std::ops::{Range}; +use std::ops::Range; #[derive(Debug)] pub struct BoundingBox { pub lon: Range, pub lat: Range, + intersect_zero_meridian: bool, } impl BoundingBox { @@ -82,7 +84,11 @@ impl BoundingBox { ), }; - BoundingBox { lon, lat } + BoundingBox { + lon, + lat, + intersect_zero_meridian, + } } #[inline] @@ -136,20 +142,62 @@ impl BoundingBox { } #[inline] - pub fn contains_meridian(&self, lon: f64) -> bool { + pub fn contains_longitude(&self, mut lon: f64) -> bool { + lon = if self.intersect_zero_meridian && lon > PI { + lon - TWICE_PI + } else { + lon + }; + self.lon.contains(&lon) } #[inline] pub fn contains_lonlat(&self, lonlat: &LonLatT) -> bool { - self.contains_meridian(lonlat.lon().to_radians()) && self.contains_latitude(lonlat.lat().to_radians()) - } + self.contains_longitude(lonlat.lon().to_radians()) + && self.contains_latitude(lonlat.lat().to_radians()) + } + + #[inline] + pub fn intersects(&self, other: &Self) -> bool { + let (sl, ol) = match (self.intersect_zero_meridian, other.intersect_zero_meridian) { + (true, false) => { + // self lon are in [-PI; PI] + // other lon are in [0; 2PI] + if other.lon.start >= PI { + ( + self.lon.clone(), + (other.lon.start - TWICE_PI)..(other.lon.end - TWICE_PI), + ) + } else { + (self.lon.clone(), other.lon.clone()) + } + } + (false, true) => { + // self lon are in [0; 2PI] + // other lon are in [-PI; PI] + if self.lon.start >= PI { + ( + (self.lon.start - TWICE_PI)..(self.lon.end - TWICE_PI), + other.lon.clone(), + ) + } else { + (self.lon.clone(), other.lon.clone()) + } + } + _ => (self.lon.clone(), other.lon.clone()), + }; + + (sl.start <= ol.end && ol.start <= sl.end) + && (self.lat.start <= other.lat.end && other.lat.start <= self.lat.end) + } #[inline] pub const fn fullsky() -> Self { BoundingBox { lon: ZERO..TWICE_PI, lat: MINUS_HALF_PI..HALF_PI, + intersect_zero_meridian: true, } } -} \ No newline at end of file +} diff --git a/src/core/src/math/sph_geom/region.rs b/src/core/src/math/sph_geom/region.rs index 523730f7..fe38acd4 100644 --- a/src/core/src/math/sph_geom/region.rs +++ b/src/core/src/math/sph_geom/region.rs @@ -119,6 +119,66 @@ impl Region { } } + pub fn intersects(&self, other: &Self) -> bool { + match (self, other) { + ( + Region::Polygon { + polygon: p1, + bbox: bbox1, + .. + }, + Region::Polygon { + polygon: p2, + bbox: bbox2, + .. + }, + ) => { + if !bbox1.intersects(bbox2) { + return false; + } + + for v1 in p1.vertices() { + let ll = v1.lonlat(); + let ll = LonLatT::new(ll.0.to_angle(), ll.1.to_angle()); + if other.contains(&ll) { + return true; + } + } + + for v2 in p2.vertices() { + let ll = v2.lonlat(); + let ll = LonLatT::new(ll.0.to_angle(), ll.1.to_angle()); + if self.contains(&ll) { + return true; + } + } + + let vertices = p2.vertices(); + let mut j = vertices.len() - 1; + for i in 0..vertices.len() { + let llj = vertices[j].lonlat(); + let lli = vertices[i].lonlat(); + + let llj = LonLatT::new(llj.0.to_angle(), llj.1.to_angle()); + let lli = LonLatT::new(lli.0.to_angle(), lli.1.to_angle()); + + let inter = self.intersects_great_circle_arc(&llj, &lli); + match inter { + Intersection::Empty => {} + _ => { + return true; + } + } + + j = i; + } + + false + } + _ => true, + } + } + pub fn intersects_great_circle_arc( &self, lonlat1: &LonLatT, diff --git a/src/core/src/renderable/image/cuts.rs b/src/core/src/renderable/image/cuts.rs new file mode 100644 index 00000000..87f01d91 --- /dev/null +++ b/src/core/src/renderable/image/cuts.rs @@ -0,0 +1,25 @@ +use std::cmp::Ordering; +use std::ops::Range; +pub fn first_and_last_percent(slice: &mut [T], first_percent: i32, last_percent: i32) -> Range +where + T: PartialOrd + Copy, +{ + let n = slice.len(); + let first_pct_idx = ((first_percent as f32) * 0.01 * (n as f32)) as usize; + let last_pct_idx = ((last_percent as f32) * 0.01 * (n as f32)) as usize; + + let min_val = { + let (_, min_val, _) = slice.select_nth_unstable_by(first_pct_idx, |a, b| { + a.partial_cmp(b).unwrap_or(Ordering::Greater) + }); + *min_val + }; + let max_val = { + let (_, max_val, _) = slice.select_nth_unstable_by(last_pct_idx, |a, b| { + a.partial_cmp(b).unwrap_or(Ordering::Greater) + }); + *max_val + }; + + min_val..max_val +} diff --git a/src/core/src/renderable/image/grid.rs b/src/core/src/renderable/image/grid.rs index 4401d8d8..e7eb782f 100644 --- a/src/core/src/renderable/image/grid.rs +++ b/src/core/src/renderable/image/grid.rs @@ -1,8 +1,8 @@ +use cgmath::Vector4; use std::ops::RangeInclusive; use wcs::ImgXY; use crate::camera::CameraViewPort; -use crate::math::angle::ToAngle; use crate::math::projection::ProjectionType; use crate::renderable::utils::index_patch::CCWCheckPatchIndexIter; use al_api::coo_system::CooSystem; @@ -173,9 +173,7 @@ pub fn vertices( num_tri_per_tex_patch: u64, camera: &CameraViewPort, wcs: &WCS, - image_coo_sys: CooSystem, projection: &ProjectionType, - towards_east: bool, ) -> (Vec, Vec, Vec, Vec) { let (x_it, y_it) = get_grid_params(xy_min, xy_max, max_tex_size, num_tri_per_tex_patch); @@ -188,15 +186,11 @@ pub fn vertices( let pos = y_it .map(|(y, uvy)| { x_it.clone().map(move |(x, uvx)| { - let ndc = if let Some(lonlat) = wcs.unproj(&ImgXY::new(x as f64, y as f64)) { - let lon = lonlat.lon(); - let lat = lonlat.lat(); - - let xyzw = crate::math::lonlat::radec_to_xyzw(lon.to_angle(), lat.to_angle()); + let ndc = if let Some(xyz) = wcs.unproj_xyz(&ImgXY::new(x as f64, y as f64)) { let xyzw = crate::coosys::apply_coo_system( CooSystem::ICRS, camera.get_coo_system(), - &xyzw, + &Vector4::new(xyz.y(), xyz.z(), xyz.x(), 1.0), ); projection @@ -220,14 +214,8 @@ pub fn vertices( let mut num_indices = vec![]; for idx_x_range in &idx_x_ranges { for idx_y_range in &idx_y_ranges { - let build_indices_iter = CCWCheckPatchIndexIter::new( - idx_x_range, - idx_y_range, - num_x_vertices, - &pos, - camera, - towards_east, - ); + let build_indices_iter = + CCWCheckPatchIndexIter::new(idx_x_range, idx_y_range, num_x_vertices, &pos, camera); let patch_indices = build_indices_iter .flatten() diff --git a/src/core/src/renderable/image/mod.rs b/src/core/src/renderable/image/mod.rs index 74e83aa3..fad4923f 100644 --- a/src/core/src/renderable/image/mod.rs +++ b/src/core/src/renderable/image/mod.rs @@ -1,14 +1,13 @@ +pub mod cuts; pub mod grid; pub mod subdivide_texture; -use std::cmp::Ordering; use std::fmt::Debug; use std::marker::Unpin; use std::vec; use al_api::coo_system::CooSystem; -use cgmath::Vector3; -use cgmath::Zero; +use cgmath::Vector4; use futures::stream::TryStreamExt; use futures::AsyncRead; @@ -29,13 +28,10 @@ use al_core::WebGlContext; use al_core::{Texture2D, VertexArrayObject}; use crate::camera::CameraViewPort; -use crate::math::angle::ToAngle; -use crate::math::lonlat::LonLat; +use crate::math::sph_geom::region::Region; use crate::Colormaps; -use crate::LonLatT; use crate::ProjectionType; use crate::ShaderManager; -use cgmath::InnerSpace; use std::ops::Range; @@ -52,12 +48,10 @@ pub struct Image { /// Parameters extracted from the fits wcs: WCS, - image_coo_sys: CooSystem, - blank: f32, - scale: f32, - offset: f32, - pub cuts: Range, - + blank: Option, + scale: Option, + offset: Option, + cuts: Option>, /// The center of the fits centered_fov: CenteredFoV, @@ -70,222 +64,51 @@ pub struct Image { /// The maximum webgl supported texture size max_tex_size: usize, - // Is the increasing longitude on the image goes towards the east ? - towards_east: bool, + reg: Region, + // The coo system in which the polygonal region has been defined + coo_sys: CooSystem, } use fitsrs::hdu::header::extension; use fitsrs::hdu::AsyncHDU; use futures::io::BufReader; - -pub fn compute_automatic_cuts( - slice: &mut [T], - first_percent: i32, - second_percent: i32, -) -> Range -where - T: PartialOrd + Copy, -{ - let n = slice.len(); - let first_pct_idx = ((first_percent as f32) * 0.01 * (n as f32)) as usize; - let last_pct_idx = ((second_percent as f32) * 0.01 * (n as f32)) as usize; - - let min_val = { - let (_, min_val, _) = slice.select_nth_unstable_by(first_pct_idx, |a, b| { - a.partial_cmp(b).unwrap_or(Ordering::Greater) - }); - *min_val - }; - let max_val = { - let (_, max_val, _) = slice.select_nth_unstable_by(last_pct_idx, |a, b| { - a.partial_cmp(b).unwrap_or(Ordering::Greater) - }); - *max_val - }; - - min_val..max_val -} - +use futures::AsyncReadExt; impl Image { - pub async fn from_fits_hdu_async<'a, R>( + pub async fn from_reader_and_wcs( gl: &WebGlContext, - hdu: &mut AsyncHDU<'a, BufReader, extension::image::Image>, - //reader: &'a mut BufReader, + reader: R, + wcs: WCS, + mut scale: Option, + mut offset: Option, + mut blank: Option, + // Coo sys of the view + coo_sys: CooSystem, ) -> Result where - R: AsyncRead + Unpin + Debug + 'a, + F: ImageFormat, + R: AsyncReadExt + Unpin, { - // Load the fits file - let header = hdu.get_header(); - - let naxis = header.get_xtension().get_naxis(); + let (width, height) = wcs.img_dimensions(); - if naxis == 0 { - return Err(JsValue::from_str("The fits is empty, NAXIS=0")); - } let max_tex_size = WebGl2RenderingContext::get_parameter(gl, WebGl2RenderingContext::MAX_TEXTURE_SIZE)? .as_f64() .unwrap_or(4096.0) as usize; + let (textures, mut cuts) = + subdivide_texture::build::(gl, width, height, reader, max_tex_size, blank) + .await?; - let scale = header - .get_parsed::(b"BSCALE ") - .unwrap_or(Ok(1.0)) - .unwrap() as f32; - let offset = header - .get_parsed::(b"BZERO ") - .unwrap_or(Ok(0.0)) - .unwrap() as f32; - let blank = header - .get_parsed::(b"BLANK ") - .unwrap_or(Ok(std::f64::NAN)) - .unwrap() as f32; - - // Create a WCS from a specific header unit - let wcs = WCS::from_fits_header(&header) - .map_err(|e| JsValue::from_str(&format!("WCS parsing error: reason: {}", e)))?; - - let image_coo_sys = match wcs.coo_system() { - wcs::coo_system::CooSystem::GALACTIC => CooSystem::GAL, - _ => CooSystem::ICRS, - }; - - let (w, h) = wcs.img_dimensions(); - let width = w as f64; - let height = h as f64; - - let data = hdu.get_data_mut(); - - let (textures, channel, mut cuts) = match data { - stream::Data::U8(data) => { - let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); - - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| if v == (blank as u8) { None } else { Some(v) }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - ( - textures, - ChannelType::R8UI, - (cuts.start as f32)..(cuts.end as f32), - ) - } - stream::Data::I16(data) => { - let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); - - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| if v == (blank as i16) { None } else { Some(v) }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - ( - textures, - ChannelType::R16I, - (cuts.start as f32)..(cuts.end as f32), - ) - } - stream::Data::I32(data) => { - let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); - - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| if v == (blank as i32) { None } else { Some(v) }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - ( - textures, - ChannelType::R32I, - (cuts.start as f32)..(cuts.end as f32), - ) - } - stream::Data::I64(data) => { - let reader = data - .map_ok(|v| { - let v = v[0] as i32; - v.to_le_bytes() - }) - .into_async_read(); - - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| { - if v == (blank as i32) { - None - } else { - Some(v as i32) - } - }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - ( - textures, - ChannelType::R32I, - (cuts.start as f32)..(cuts.end as f32), - ) - } - stream::Data::F32(data) => { - let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| { - if v == blank || v.is_nan() || v.is_zero() { - None - } else { - Some(v) - } - }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - (textures, ChannelType::R32F, cuts) - } - stream::Data::F64(data) => { - let reader = data - .map_ok(|v| { - let v = v[0] as f32; - v.to_le_bytes() - }) - .into_async_read(); - - let (textures, samples) = - subdivide_texture::build::(gl, w, h, reader, max_tex_size).await?; - - let mut samples = samples - .into_iter() - .filter_map(|v| { - if v == blank || v.is_nan() || v.is_zero() { - None - } else { - Some(v) - } - }) - .collect::>(); - - let cuts = compute_automatic_cuts(&mut samples, 1, 99); - - (textures, ChannelType::R32F, cuts) - } - }; + // apply bscale to the cuts + if F::NUM_CHANNELS == 1 { + offset = offset.or(Some(0.0)); + scale = scale.or(Some(1.0)); + blank = blank.or(Some(std::f32::NAN)); + cuts = cuts.map(|cuts| { + let start = cuts.start * scale.unwrap() + offset.unwrap(); + let end = cuts.end * scale.unwrap() + offset.unwrap(); + start..end + }); + } let num_indices = vec![]; let indices = vec![]; @@ -316,80 +139,57 @@ impl Image { VecData::(&indices), ) .unbind(); - #[cfg(feature = "webgl1")] - vao.bind_for_update() - .add_array_buffer_single( - 2, - "ndc_pos", - WebGl2RenderingContext::DYNAMIC_DRAW, - VecData::(&pos), - ) - .add_array_buffer_single( - 2, - "uv", - WebGl2RenderingContext::DYNAMIC_DRAW, - VecData::(&uv), - ) - // Set the element buffer - .add_element_buffer( - WebGl2RenderingContext::DYNAMIC_DRAW, - VecData::(&indices), - ) - .unbind(); vao }; - - // apply bscale to the cuts - cuts.start = cuts.start * scale + offset; - cuts.end = cuts.end * scale + offset; - let gl = gl.clone(); // Compute the fov let center = wcs - .unproj_lonlat(&ImgXY::new(width / 2.0, height / 2.0)) + .unproj_lonlat(&ImgXY::new(width as f64 / 2.0, height as f64 / 2.0)) .ok_or(JsValue::from_str("(w / 2, h / 2) px cannot be unprojected"))?; - let top_lonlat = wcs - .unproj_lonlat(&ImgXY::new(width / 2.0, height)) - .ok_or(JsValue::from_str("(w / 2, h) px cannot be unprojected"))?; - let left_lonlat = wcs - .unproj_lonlat(&ImgXY::new(0.0, height / 2.0)) - .ok_or(JsValue::from_str("(0, h / 2) px cannot be unprojected"))?; - - let a_xyz: Vector3 = crate::coosys::apply_coo_system( - CooSystem::ICRS, - image_coo_sys, - &LonLatT::new(left_lonlat.lon().to_angle(), left_lonlat.lat().to_angle()).vector(), - ) - .truncate(); - let b_xyz = crate::coosys::apply_coo_system( + let center_xyz = center.to_xyz(); + let inside = crate::coosys::apply_coo_system( CooSystem::ICRS, - image_coo_sys, - &LonLatT::new(center.lon().to_angle(), center.lat().to_angle()).vector(), - ) - .truncate(); - - let towards_east = a_xyz.cross(b_xyz).dot(Vector3::unit_y()) <= 0.0; + coo_sys, + &Vector4::new(center_xyz.y(), center_xyz.z(), center_xyz.x(), 1.0), + ); - let half_fov1 = - crate::math::lonlat::ang_between_lonlat(top_lonlat.into(), center.clone().into()); - let half_fov2 = - crate::math::lonlat::ang_between_lonlat(left_lonlat.into(), center.clone().into()); + let vertices = [ + wcs.unproj_lonlat(&ImgXY::new(0.0, 0.0)) + .ok_or(JsValue::from_str("(0, 0) does not lie in the sky"))?, + wcs.unproj_lonlat(&ImgXY::new(width as f64 - 1.0, 0.0)) + .ok_or(JsValue::from_str("(w - 1, 0) does not lie in the sky"))?, + wcs.unproj_lonlat(&ImgXY::new(width as f64 - 1.0, height as f64 - 1.0)) + .ok_or(JsValue::from_str("(w - 1, h - 1) does not lie in the sky"))?, + wcs.unproj_lonlat(&ImgXY::new(0.0, height as f64 - 1.0)) + .ok_or(JsValue::from_str("(0, h - 1) does not lie in the sky"))?, + ] + .iter() + .map(|lonlat| { + let xyz = lonlat.to_xyz(); + + crate::coosys::apply_coo_system( + CooSystem::ICRS, + coo_sys, + &Vector4::new(xyz.y(), xyz.z(), xyz.x(), 1.0), + ) + }) + .collect::>(); - let half_fov = half_fov1.max(half_fov2); + let reg = Region::from_vertices(&vertices, &inside); // ra and dec must be given in ICRS coo system, which is the case because wcs returns // only icrs coo let centered_fov = CenteredFoV { ra: center.lon().to_degrees(), dec: center.lat().to_degrees(), - fov: 2.0 * half_fov.to_degrees(), + fov: wcs.field_of_view().0, }; let idx_tex = (0..textures.len()).collect(); - let image = Image { + Ok(Image { gl, // The positions @@ -402,25 +202,158 @@ impl Image { // Metadata extracted from the fits wcs, // CooSystem of the wcs, this should belong to the WCS - image_coo_sys, scale, offset, blank, - towards_east, // Centered field of view allowing to locate the fits centered_fov, // Texture parameters - channel, + channel: F::CHANNEL_TYPE, textures, cuts, max_tex_size, // Indices of textures that must be drawn idx_tex, - }; + // The polygonal region in the sky + reg, + // The coo system in which the polygonal region has been defined + coo_sys, + }) + } - Ok(image) + pub fn get_cuts(&self) -> &Option> { + &self.cuts + } + + pub async fn from_fits_hdu_async<'a, R>( + gl: &WebGlContext, + hdu: &mut AsyncHDU<'a, BufReader, extension::image::Image>, + coo_sys: CooSystem, + //reader: &'a mut BufReader, + ) -> Result + where + R: AsyncRead + Unpin + Debug + 'a, + { + // Load the fits file + let header = hdu.get_header(); + + let scale = header + .get_parsed::(b"BSCALE ") + .unwrap_or(Ok(1.0)) + .unwrap() as f32; + let offset = header + .get_parsed::(b"BZERO ") + .unwrap_or(Ok(0.0)) + .unwrap() as f32; + let blank = header + .get_parsed::(b"BLANK ") + .unwrap_or(Ok(std::f64::NAN)) + .unwrap() as f32; + + // Create a WCS from a specific header unit + let wcs = WCS::from_fits_header(&header) + .map_err(|e| JsValue::from_str(&format!("WCS parsing error: reason: {}", e)))?; + + let data = hdu.get_data_mut(); + + match data { + stream::Data::U8(data) => { + let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); + + Self::from_reader_and_wcs::<_, R8UI>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + stream::Data::I16(data) => { + let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); + + Self::from_reader_and_wcs::<_, R16I>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + stream::Data::I32(data) => { + let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); + + Self::from_reader_and_wcs::<_, R32I>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + stream::Data::I64(data) => { + let reader = data + .map_ok(|v| { + let v = v[0] as i32; + v.to_le_bytes() + }) + .into_async_read(); + + Self::from_reader_and_wcs::<_, R32I>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + stream::Data::F32(data) => { + let reader = data.map_ok(|v| v[0].to_le_bytes()).into_async_read(); + + Self::from_reader_and_wcs::<_, R32F>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + stream::Data::F64(data) => { + let reader = data + .map_ok(|v| { + let v = v[0] as f32; + v.to_le_bytes() + }) + .into_async_read(); + + Self::from_reader_and_wcs::<_, R32F>( + gl, + reader, + wcs, + Some(scale), + Some(offset), + Some(blank), + coo_sys, + ) + .await + } + } } pub fn recompute_vertices( @@ -431,7 +364,7 @@ impl Image { let (width, height) = self.wcs.img_dimensions(); let width = width as f64; let height = height as f64; - + /* // Determine the x and y pixels ranges that must be drawn into the screen let (x_mesh_range, y_mesh_range) = if let Some(vertices) = camera.get_vertices() { // The field of view is defined, so we can compute its projection into the wcs @@ -439,32 +372,41 @@ impl Image { std::f64::INFINITY..std::f64::NEG_INFINITY, std::f64::INFINITY..std::f64::NEG_INFINITY, ); - - for vertex in vertices.iter() { - let xyzw = crate::coosys::apply_coo_system( + use crate::math::lonlat::LonLat; + for xyzw in vertices.iter() { + /*let xyzw = crate::coosys::apply_coo_system( camera.get_coo_system(), CooSystem::ICRS, vertex, - ); + );*/ let lonlat = xyzw.lonlat(); - let lon = lonlat.lon(); - let lat = lonlat.lat(); - - let img_vert = self - .wcs - .proj(&wcs::LonLat::new(lon.to_radians(), lat.to_radians())); + let mut lon = lonlat.lon().to_radians(); + let lat = lonlat.lat().to_radians(); + use crate::math::angle::PI; + if lon > PI { + lon -= TWICE_PI; + } - if let Some(img_vert) = img_vert { - x_fov_proj_range.start = x_fov_proj_range.start.min(img_vert.x()); - x_fov_proj_range.end = x_fov_proj_range.end.max(img_vert.x()); + if let Some(xy) = self.wcs.proj_xyz(&(xyzw.z, xyzw.x, xyzw.y)) { + //dbg!((img_vert.x(), img_vert.y())); + x_fov_proj_range.start = x_fov_proj_range.start.min(xy.x()); + x_fov_proj_range.end = x_fov_proj_range.end.max(xy.x()); - y_fov_proj_range.start = y_fov_proj_range.start.min(img_vert.y()); - y_fov_proj_range.end = y_fov_proj_range.end.max(img_vert.y()); + y_fov_proj_range.start = y_fov_proj_range.start.min(xy.y()); + y_fov_proj_range.end = y_fov_proj_range.end.max(xy.y()); } } + console_log(&format!( + "fov: {:?}", + (x_fov_proj_range.clone(), y_fov_proj_range.clone()) + )); + + let x_fov_proj_range = (0.0..width); + let y_fov_proj_range = (0.0..height); + // Check if the FoV is overlapping the image // If not we can exit this update faster let is_ranges_overlapping = |x: &std::ops::Range, y: &std::ops::Range| { @@ -515,11 +457,29 @@ impl Image { self.idx_tex = (0..self.textures.len()).collect(); (0.0..width, 0.0..height) - }; + };*/ + + let (x_mesh_range, y_mesh_range) = + if camera.get_field_of_view().intersects_region(&self.reg) { + self.idx_tex = (0..self.textures.len()).collect(); + + (0.0..width, 0.0..height) + } else { + // out of field of view + self.idx_tex.clear(); + + // terminate here + return Ok(()); + }; - const MAX_NUM_TRI_PER_SIDE_IMAGE: usize = 25; + /*console_log(&format!( + "{:?}", + (x_mesh_range.clone(), y_mesh_range.clone()) + ));*/ + + const MAX_NUM_TRI_PER_SIDE_IMAGE: usize = 15; let num_vertices = - ((self.centered_fov.fov / 360.0) * (MAX_NUM_TRI_PER_SIDE_IMAGE as f64)).ceil() as u64; + ((self.centered_fov.fov / 180.0) * (MAX_NUM_TRI_PER_SIDE_IMAGE as f64)).ceil() as u64; let (pos, uv, indices, num_indices) = grid::vertices( &(x_mesh_range.start, y_mesh_range.start), @@ -528,10 +488,9 @@ impl Image { num_vertices, camera, &self.wcs, - self.image_coo_sys, projection, - self.towards_east, ); + self.pos = pos; self.uv = uv; @@ -569,10 +528,56 @@ impl Image { camera: &CameraViewPort, projection: &ProjectionType, ) -> Result<(), JsValue> { - if camera.has_moved() { - self.recompute_vertices(camera, projection)?; + if self.coo_sys != camera.get_coo_system() { + self.coo_sys = camera.get_coo_system(); + + let (width, height) = self.wcs.img_dimensions(); + + // the camera coo system is not sync with the one in which the region + // has been defined + // let's redefine the region + let center = self + .wcs + .unproj_lonlat(&ImgXY::new(width as f64 / 2.0, height as f64 / 2.0)) + .ok_or(JsValue::from_str("(w / 2, h / 2) px cannot be unprojected"))?; + let center_xyz = center.to_xyz(); + let inside = crate::coosys::apply_coo_system( + CooSystem::ICRS, + self.coo_sys, + &Vector4::new(center_xyz.y(), center_xyz.z(), center_xyz.x(), 1.0), + ); + + let vertices = [ + self.wcs + .unproj_lonlat(&ImgXY::new(0.0, 0.0)) + .ok_or(JsValue::from_str("(0, 0) does not lie in the sky"))?, + self.wcs + .unproj_lonlat(&ImgXY::new(width as f64 - 1.0, 0.0)) + .ok_or(JsValue::from_str("(w - 1, 0) does not lie in the sky"))?, + self.wcs + .unproj_lonlat(&ImgXY::new(width as f64 - 1.0, height as f64 - 1.0)) + .ok_or(JsValue::from_str("(w - 1, h - 1) does not lie in the sky"))?, + self.wcs + .unproj_lonlat(&ImgXY::new(0.0, height as f64 - 1.0)) + .ok_or(JsValue::from_str("(0, h - 1) does not lie in the sky"))?, + ] + .iter() + .map(|lonlat| { + let xyz = lonlat.to_xyz(); + + crate::coosys::apply_coo_system( + CooSystem::ICRS, + self.coo_sys, + &Vector4::new(xyz.y(), xyz.z(), xyz.x(), 1.0), + ) + }) + .collect::>(); + + self.reg = Region::from_vertices(&vertices, &inside); } + self.recompute_vertices(camera, projection)?; + if self.num_indices.is_empty() { return Ok(()); } @@ -587,6 +592,12 @@ impl Image { } = cfg; let shader = match self.channel { + ChannelType::RGBA8U => crate::shader::get_shader( + &self.gl, + shaders, + "image_base.vert", + "image_sampler.frag", + )?, ChannelType::R32F => { crate::shader::get_shader(&self.gl, shaders, "fits_base.vert", "fits_sampler.frag")? } @@ -630,9 +641,9 @@ impl Image { .attach_uniforms_with_params_from(color, colormaps) .attach_uniform("opacity", opacity) .attach_uniform("tex", texture) - .attach_uniform("scale", &self.scale) - .attach_uniform("offset", &self.offset) - .attach_uniform("blank", &self.blank) + .attach_uniform("scale", &self.scale.unwrap_or(1.0)) + .attach_uniform("offset", &self.offset.unwrap_or(0.0)) + .attach_uniform("blank", &self.blank.unwrap_or(std::f32::NAN)) .bind_vertex_array_object_ref(&self.vao) .draw_elements_with_i32( WebGl2RenderingContext::TRIANGLES, diff --git a/src/core/src/renderable/image/subdivide_texture.rs b/src/core/src/renderable/image/subdivide_texture.rs index 9b7a72b0..f1dd8fa1 100644 --- a/src/core/src/renderable/image/subdivide_texture.rs +++ b/src/core/src/renderable/image/subdivide_texture.rs @@ -2,22 +2,28 @@ use wasm_bindgen::JsValue; use futures::AsyncReadExt; +use super::cuts; +use al_core::image::format::ImageFormat; +use al_core::texture::pixel::Pixel; use al_core::texture::TEX_PARAMS; -use al_core::texture::{ - pixel::Pixel, -}; use al_core::Texture2D; use al_core::WebGlContext; -use al_core::image::format::ImageFormat; - - - -pub async fn build<'a, F, R>(gl: &WebGlContext, width: u64, height: u64, mut reader: R, max_tex_size: usize) -> Result<(Vec, Vec< ::Item >), JsValue> +use std::ops::Range; + +pub async fn build<'a, F, R>( + gl: &WebGlContext, + width: u64, + height: u64, + mut reader: R, + max_tex_size: usize, + blank: Option, +) -> Result<(Vec, Option>), JsValue> where F: ImageFormat, - R: AsyncReadExt + Unpin + R: AsyncReadExt + Unpin, { - let mut buf = vec![0; max_tex_size * std::mem::size_of::<::Item>()]; + let mut buf = + vec![0; max_tex_size * std::mem::size_of::<::Item>() * F::NUM_CHANNELS]; let max_tex_size = max_tex_size as u64; // Subdivision @@ -25,7 +31,13 @@ where let mut tex_chunks = vec![]; for _ in 0..num_textures { - tex_chunks.push(Texture2D::create_from_raw_pixels::(gl, max_tex_size as i32, max_tex_size as i32, TEX_PARAMS, None)?); + tex_chunks.push(Texture2D::create_from_raw_pixels::( + gl, + max_tex_size as i32, + max_tex_size as i32, + TEX_PARAMS, + None, + )?); } let mut pixels_written = 0; @@ -46,7 +58,7 @@ where let id_tx = (pixels_written % width) / max_tex_size; let id_ty = (pixels_written / width) / max_tex_size; - let id_t = id_ty + id_tx*num_texture_y; + let id_t = id_ty + id_tx * num_texture_y; // For textures along the right-x border let num_pixels_to_read = if id_tx == num_texture_x - 1 { @@ -55,7 +67,10 @@ where max_tex_size }; - let num_bytes_to_read = (num_pixels_to_read as usize) * std::mem::size_of::<::Item>(); + let num_bytes_to_read = (num_pixels_to_read as usize) + * std::mem::size_of::<::Item>() + * F::NUM_CHANNELS; + if let Ok(()) = reader.read_exact(&mut buf[..num_bytes_to_read]).await { // Tell where the data must go inside the texture let off_y_px = id_ty * max_tex_size; @@ -64,19 +79,33 @@ where let view = unsafe { let slice = std::slice::from_raw_parts( buf[..num_bytes_to_read].as_ptr() as *const ::Item, - num_pixels_to_read as usize + (num_pixels_to_read as usize) * F::NUM_CHANNELS, ); - // fill the samples buffer - if (pixels_written / width) % (step_cut as u64) == 0 { - // We are in a good line - let xmin = pixels_written % width; - - for i in (0..width).step_by(step_cut) { - if (xmin..(xmin + num_pixels_to_read)).contains(&i) { - let j = (i - xmin) as usize; - - samples.push(slice[j]); + // compute the cuts if the pixel is grayscale + if F::NUM_CHANNELS == 1 { + // fill the samples buffer + if (pixels_written / width) % (step_cut as u64) == 0 { + // We are in a good line + let xmin = pixels_written % width; + + for i in (0..width).step_by(step_cut) { + if (xmin..(xmin + num_pixels_to_read)).contains(&i) { + let j = (i - xmin) as usize; + + let sj: f32 = <::Item as al_core::convert::Cast< + f32, + >>::cast(slice[j]); + if !sj.is_nan() { + if let Some(b) = blank { + if b != sj { + samples.push(sj); + } + } else { + samples.push(sj); + } + } + } } } } @@ -91,14 +120,22 @@ where dy as i32, num_pixels_to_read as i32, 1, - Some(view.as_ref()) + Some(view.as_ref()), ); pixels_written += num_pixels_to_read; } else { - pixels_written = num_pixels; + return Err(JsValue::from_str( + "invalid data with respect to the NAXIS given in the WCS", + )); } } - Ok((tex_chunks, samples)) + let cuts = if F::NUM_CHANNELS == 1 { + Some(cuts::first_and_last_percent(&mut samples, 1, 99)) + } else { + None + }; + + Ok((tex_chunks, cuts)) } diff --git a/src/core/src/renderable/moc/mode/mod.rs b/src/core/src/renderable/moc/mode/mod.rs index 4cbed358..78930803 100644 --- a/src/core/src/renderable/moc/mode/mod.rs +++ b/src/core/src/renderable/moc/mode/mod.rs @@ -1,17 +1,2 @@ -use crate::healpix::cell::CellVertices; -use crate::HEALPixCell; -use crate::HEALPixCoverage; - pub mod edge; pub mod filled; -pub mod perimeter; - -pub(super) trait RenderMode { - fn build(moc: &HEALPixCoverage) -> impl Iterator; -} - -#[derive(Debug)] -pub struct Node { - pub cell: HEALPixCell, - pub vertices: Option, -} diff --git a/src/core/src/renderable/mod.rs b/src/core/src/renderable/mod.rs index 63efbfb5..aec5991e 100644 --- a/src/core/src/renderable/mod.rs +++ b/src/core/src/renderable/mod.rs @@ -49,14 +49,15 @@ pub trait Renderer { fn end(&mut self); } -pub(crate) type Url = String; +pub(crate) type Id = String; // ID of an image, can be an url or a uuidv4 pub(crate) type CreatorDid = String; type LayerId = String; pub struct Layers { // Surveys to query surveys: HashMap, - images: HashMap, + images: HashMap>, // an url can contain multiple images i.e. a fits file can contain + // multiple image extensions // The meta data associated with a layer meta: HashMap, // Hashmap between FITS image urls/HiPS creatorDid and layers @@ -94,23 +95,27 @@ fn get_backgroundcolor_shader<'a>( .map_err(|e| e.into()) } -pub struct ImageCfg { +pub struct ImageLayer { /// Layer name pub layer: String, - pub url: String, - pub image: Image, + pub id: String, + pub images: Vec, /// Its color pub meta: ImageMetadata, } -impl ImageCfg { +impl ImageLayer { pub fn get_params(&self) -> ImageParams { + let (min_cut, max_cut) = self.images[0] + .get_cuts() + .as_ref() + .map_or((None, None), |r| (Some(r.start), Some(r.end))); + + let centered_fov = self.images[0].get_centered_fov().clone(); ImageParams { - layer: self.layer.clone(), - url: self.url.clone(), - centered_fov: self.image.get_centered_fov().clone(), - automatic_min_cut: self.image.cuts.start, - automatic_max_cut: self.image.cuts.end, + centered_fov, + min_cut, + max_cut, } } } @@ -289,9 +294,11 @@ impl Layers { // 2. Draw it if its opacity is not null survey.draw(shaders, colormaps, camera, raytracer, draw_opt, projection)?; - } else if let Some(image) = self.images.get_mut(id) { + } else if let Some(images) = self.images.get_mut(id) { // 2. Draw it if its opacity is not null - image.draw(shaders, colormaps, draw_opt, camera, projection)?; + for image in images { + image.draw(shaders, colormaps, draw_opt, camera, projection)?; + } } } } @@ -483,16 +490,16 @@ impl Layers { Ok(hips) } - pub fn add_image_fits( + pub fn add_image( &mut self, - image: ImageCfg, + image: ImageLayer, camera: &mut CameraViewPort, proj: &ProjectionType, - ) -> Result<&Image, JsValue> { - let ImageCfg { + ) -> Result<&[Image], JsValue> { + let ImageLayer { layer, - url, - image, + id, + images, meta, } = image; @@ -520,7 +527,7 @@ impl Layers { // The layer does not already exist // Let's check if no other hipses points to the // same url than `hips` - let fits_already_found = self.images.keys().any(|image_url| image_url == &url); + let fits_already_found = self.images.keys().any(|image_id| image_id == &id); if !fits_already_found { // The fits has not been loaded yet @@ -534,16 +541,16 @@ impl Layers { camera.set_aperture::

(Angle((initial_fov).to_radians())); }*/ - self.images.insert(url.clone(), image); + self.images.insert(id.clone(), images); } - self.ids.insert(layer.clone(), url.clone()); + self.ids.insert(layer.clone(), id.clone()); - let fits = self + let img = self .images - .get(&url) + .get(&id) .ok_or(JsValue::from_str("Fits image not found"))?; - Ok(fits) + Ok(img.as_slice()) } pub fn get_layer_cfg(&self, layer: &str) -> Result { @@ -568,8 +575,10 @@ impl Layers { survey.recompute_vertices(camera, projection); } - if let Some(image) = self.get_mut_image_from_layer(layer_ref) { - image.recompute_vertices(camera, projection)?; + if let Some(images) = self.get_mut_image_from_layer(layer_ref) { + for image in images { + image.recompute_vertices(camera, projection)?; + } } } else if meta_old.visible() && !meta.visible() { // There is an important point here, if we hide a specific layer @@ -585,8 +594,10 @@ impl Layers { if let Some(survey) = self.get_mut_hips_from_layer(&cur_layer) { survey.recompute_vertices(camera, projection); - } else if let Some(image) = self.get_mut_image_from_layer(&cur_layer) { - image.recompute_vertices(camera, projection)?; + } else if let Some(images) = self.get_mut_image_from_layer(&cur_layer) { + for image in images { + image.recompute_vertices(camera, projection)?; + } } } } @@ -634,18 +645,21 @@ impl Layers { } // Fits images getters - pub fn get_mut_image_from_layer(&mut self, layer: &str) -> Option<&mut Image> { + pub fn get_mut_image_from_layer(&mut self, layer: &str) -> Option<&mut [Image]> { if let Some(url) = self.ids.get(layer) { - self.images.get_mut(url) + self.images.get_mut(url).map(|images| images.as_mut_slice()) } else { None } } - pub fn get_image_from_layer(&self, layer: &str) -> Option<&Image> { - self.ids + pub fn get_image_from_layer(&self, layer: &str) -> Option<&[Image]> { + let images = self + .ids .get(layer) .map(|url| self.images.get(url)) - .flatten() + .flatten(); + + images.map(|images| images.as_slice()) } } diff --git a/src/core/src/renderable/shape/mod.rs b/src/core/src/renderable/shape/mod.rs index 6c9fc140..7e52001b 100644 --- a/src/core/src/renderable/shape/mod.rs +++ b/src/core/src/renderable/shape/mod.rs @@ -48,6 +48,7 @@ pub enum Style { Dotted, } +#[allow(dead_code)] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Footprint { diff --git a/src/core/src/renderable/utils/index_patch.rs b/src/core/src/renderable/utils/index_patch.rs index 27cafcc5..7579c167 100644 --- a/src/core/src/renderable/utils/index_patch.rs +++ b/src/core/src/renderable/utils/index_patch.rs @@ -1,7 +1,7 @@ use std::ops::RangeInclusive; -use super::triangle::Triangle; use crate::CameraViewPort; +use cgmath::Vector2; // This iterator construct indices from a set of vertices defining // a grid. @@ -12,7 +12,6 @@ pub struct CCWCheckPatchIndexIter<'a> { ndc: &'a [Option<[f32; 2]>], camera: &'a CameraViewPort, - towards_east: bool, } impl<'a> CCWCheckPatchIndexIter<'a> { @@ -22,7 +21,6 @@ impl<'a> CCWCheckPatchIndexIter<'a> { num_x_vertices: usize, ndc: &'a [Option<[f32; 2]>], camera: &'a CameraViewPort, - towards_east: bool, ) -> Self { let patch_iter = DefaultPatchIndexIter::new(idx_x_range, idx_y_range, num_x_vertices); @@ -30,7 +28,6 @@ impl<'a> CCWCheckPatchIndexIter<'a> { patch_iter, ndc, camera, - towards_east, } } } @@ -52,17 +49,34 @@ impl<'a> Iterator for CCWCheckPatchIndexIter<'a> { match (ndc_tl, ndc_tr, ndc_bl, ndc_br) { (Some(ndc_tl), Some(ndc_tr), Some(ndc_bl), Some(ndc_br)) => { - let t1 = Triangle::new(&ndc_tl, &ndc_tr, &ndc_bl); - let t2 = Triangle::new(&ndc_tr, &ndc_br, &ndc_bl); - - if (self.towards_east && t1.is_valid(&self.camera) && t2.is_valid(&self.camera)) - || (!self.towards_east - && !t1.is_valid(&self.camera) - && !t2.is_valid(&self.camera)) - { - Some(indices) + let tlc = crate::math::projection::ndc_to_clip_space( + &Vector2::new(ndc_tl[0] as f64, ndc_tl[1] as f64), + &self.camera, + ); + let brc = crate::math::projection::ndc_to_clip_space( + &Vector2::new(ndc_br[0] as f64, ndc_br[1] as f64), + &self.camera, + ); + + let d1 = crate::math::vector::dist2::(tlc.as_ref(), brc.as_ref()); + if d1 > 0.1 { + self.next() } else { - self.next() // crossing projection tri + let trc = crate::math::projection::ndc_to_clip_space( + &Vector2::new(ndc_tr[0] as f64, ndc_tr[1] as f64), + &self.camera, + ); + let blc = crate::math::projection::ndc_to_clip_space( + &Vector2::new(ndc_bl[0] as f64, ndc_bl[1] as f64), + &self.camera, + ); + + let d2 = crate::math::vector::dist2::(trc.as_ref(), blc.as_ref()); + if d2 > 0.1 { + self.next() + } else { + Some(indices) + } } } _ => self.next(), // out of proj diff --git a/src/core/src/shader.rs b/src/core/src/shader.rs index d462ac48..34f868e5 100644 --- a/src/core/src/shader.rs +++ b/src/core/src/shader.rs @@ -41,6 +41,7 @@ impl From for JsValue { } use serde::Deserialize; +#[allow(dead_code)] #[derive(Debug, Deserialize)] pub struct FileSrc { pub id: String, diff --git a/src/css/aladin.css b/src/css/aladin.css index 5bbedbfb..84eefc1b 100644 --- a/src/css/aladin.css +++ b/src/css/aladin.css @@ -714,7 +714,7 @@ canvas { box-shadow:inset 1px 1px 0px 0px #fff; - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.9); margin: 0; box-sizing: content-box; diff --git a/src/glsl/webgl2/image/base.vert b/src/glsl/webgl2/image/base.vert new file mode 100644 index 00000000..34564d4c --- /dev/null +++ b/src/glsl/webgl2/image/base.vert @@ -0,0 +1,12 @@ +#version 300 es +precision highp float; + +layout (location = 0) in vec2 ndc_pos; +layout (location = 1) in vec2 uv; + +out vec2 frag_uv; + +void main() { + gl_Position = vec4(ndc_pos, 0.0, 1.0); + frag_uv = uv; +} \ No newline at end of file diff --git a/src/glsl/webgl2/image/sampler.frag b/src/glsl/webgl2/image/sampler.frag new file mode 100644 index 00000000..4005d66d --- /dev/null +++ b/src/glsl/webgl2/image/sampler.frag @@ -0,0 +1,16 @@ +#version 300 es +precision highp float; +precision highp sampler2D; + +out vec4 out_frag_color; +in vec2 frag_uv; + +uniform sampler2D tex; +uniform float opacity; + +#include ../hips/color.glsl; + +void main() { + out_frag_color = texture(tex, frag_uv); + out_frag_color.a = out_frag_color.a * opacity; +} \ No newline at end of file diff --git a/src/js/A.js b/src/js/A.js index 4bd21b26..f2b75dbd 100644 --- a/src/js/A.js +++ b/src/js/A.js @@ -128,16 +128,16 @@ A.imageHiPS = function (id, options) { * Creates a celestial source object with the given coordinates. * * @function - * @name A.imageFITS + * @name A.image * @memberof A * @param {string} url - Options describing the fits file. An url is mandatory - * @param {ImageFITSOptions} [options] - Options describing the fits file. An url is mandatory - * @returns {ImageFITS} - A HiPS image object + * @param {ImageOptions} [options] - Options describing the fits file. An url is mandatory + * @returns {Image} - A HiPS image object * @example * const sourceObj = A.source(180.0, 30.0, data, options); */ -A.imageFITS = function (url, options) { - return Aladin.createImageFITS(url, options.name, options, options.successCallback, options.errorCallback); +A.image = function (url, options) { + return Aladin.createImageFITS(url, options, options.successCallback, options.errorCallback); } /** @@ -509,7 +509,9 @@ A.catalogFromURL = function (url, options, successCallback, errorCallback, usePr catalog.setFields(fields); catalog.addSources(sources); - if ('s_region' in fields && typeof catalog.shape !== 'function') { + const s_regionFieldFound = Array.from(Object.keys(fields)).find((f) => f.toLowerCase() === 's_region'); + + if (s_regionFieldFound && typeof catalog.shape !== 'function') { // set the shape catalog.setShape((s) => { if (!s.data.s_region) diff --git a/src/js/Aladin.js b/src/js/Aladin.js index f0880cb0..52f8ff5f 100644 --- a/src/js/Aladin.js +++ b/src/js/Aladin.js @@ -46,7 +46,7 @@ import { ProjectionEnum } from "./ProjectionEnum.js"; import { ALEvent } from "./events/ALEvent.js"; import { Color } from "./Color.js"; -import { ImageFITS } from "./ImageFITS.js"; +import { Image } from "./ImageFITS.js"; import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js"; import { SAMPConnector } from "./vo/samp.js"; import { Reticle } from "./Reticle.js"; @@ -1586,12 +1586,12 @@ export let Aladin = (function () { * @throws A warning when the asset is currently present in the view * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *

    *
  • 1. An url that refers to a HiPS
  • *
  • 2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
  • *
  • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
  • - *
  • 4. A {@link ImageFITS} FITS image object
  • + *
  • 4. A {@link Image} FITS image object
  • *
*/ Aladin.prototype.removeHiPSFromFavorites = function (survey) { @@ -1611,12 +1611,12 @@ export let Aladin = (function () { * Check whether a survey is currently in the view * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *
    *
  • 1. An url that refers to a HiPS
  • *
  • 2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
  • *
  • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
  • - *
  • 4. A {@link ImageFITS} FITS image object
  • + *
  • 4. A {@link Image} Image object
  • *
*/ Aladin.prototype.contains = function(survey) { @@ -1625,16 +1625,16 @@ export let Aladin = (function () { /** * Creates a FITS image object - * @deprecated prefer use {@link A.imageFITS} + * @deprecated prefer use {@link A.image} * * @function createImageFITS * @memberof Aladin * @static * @param {string} url - The url of the fits. - * @param {ImageFITSOptions} [options] - Options for rendering the image + * @param {ImageOptions} [options] - Options for rendering the image * @param {function} [success] - A success callback * @param {function} [error] - A success callback - * @returns {ImageFITS} A FITS image object. + * @returns {Image} A FITS image object. */ Aladin.prototype.createImageFITS = function ( url, @@ -1657,8 +1657,8 @@ export let Aladin = (function () { let image = HiPSCache.get(url); if (!image) { - options = { name, successCallback, errorCallback, ...options }; - image = new ImageFITS(url, options); + options = { successCallback, errorCallback, ...options }; + image = new Image(url, options); HiPSCache.append(url, image); } @@ -1673,10 +1673,10 @@ export let Aladin = (function () { * @memberof Aladin * @static * @param {string} url - The url of the fits. - * @param {ImageFITSOptions} [options] - Options for rendering the image + * @param {ImageOptions} [options] - Options for rendering the image * @param {function} [success] - A success callback * @param {function} [error] - A success callback - * @returns {ImageFITS} A FITS image object. + * @returns {Image} A FITS image object. */ Aladin.createImageFITS = Aladin.prototype.createImageFITS; @@ -1705,7 +1705,7 @@ export let Aladin = (function () { * Add a new HiPS layer to the view on top of the others * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} [survey="CDS/P/DSS2/color"] - Can be: + * @param {string|ImageHiPS|Image} [survey="CDS/P/DSS2/color"] - Can be: *
    *
  • 1. An url that refers to a HiPS.
  • *
  • 2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
  • @@ -1721,15 +1721,15 @@ export let Aladin = (function () { /** * Change the base layer of the view * - * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageHiPS}/{@link ImageFITS} given + * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageHiPS}/{@link Image} given * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *
      *
    • 1. An url that refers to a HiPS.
    • *
    • 2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
    • *
    • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
    • - *
    • 4. A {@link ImageFITS} FITS image object
    • + *
    • 4. A {@link Image} FITS image object
    • *
    */ Aladin.prototype.setImageLayer = function (imageLayer) { @@ -1739,15 +1739,15 @@ export let Aladin = (function () { /** * Change the base layer of the view * - * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageHiPS}/{@link ImageFITS} given + * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link ImageHiPS}/{@link Image} given * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *
      *
    • 1. An url that refers to a HiPS.
    • *
    • 2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
    • *
    • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
    • - *
    • 4. A {@link ImageFITS} FITS image object
    • + *
    • 4. A {@link Image} FITS image object
    • *
    */ Aladin.prototype.setImageSurvey = Aladin.prototype.setImageLayer; @@ -1787,12 +1787,12 @@ export let Aladin = (function () { * Change the base layer of the view * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *
      *
    • 1. An url that refers to a HiPS.
    • *
    • 2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
    • *
    • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
    • - *
    • 4. A {@link ImageFITS} FITS image object
    • + *
    • 4. A {@link Image} FITS image object
    • *
    */ Aladin.prototype.setBaseImageLayer = function (urlOrHiPSOrFITS) { @@ -1803,7 +1803,7 @@ export let Aladin = (function () { * Get the base image layer object * * @memberof Aladin - * @returns {ImageHiPS|ImageFITS} - Returns the image layer corresponding to the base layer + * @returns {ImageHiPS|Image} - Returns the image layer corresponding to the base layer */ Aladin.prototype.getBaseImageLayer = function () { return this.view.getImageLayer("base"); @@ -1813,12 +1813,12 @@ export let Aladin = (function () { * Add a new HiPS/FITS image layer in the view * * @memberof Aladin - * @param {string|ImageHiPS|ImageFITS} urlOrHiPSOrFITS - Can be: + * @param {string|ImageHiPS|Image} urlOrHiPSOrFITS - Can be: *
      *
    • 1. An url that refers to a HiPS.
    • *
    • 2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
    • *
    • 3. A {@link ImageHiPS} HiPS object created from {@link A.imageHiPS}
    • - *
    • 4. A {@link ImageFITS} FITS image object
    • + *
    • 4. A {@link Image} FITS image object
    • *
    * @param {string} [layer="overlay"] - A layer name. By default 'overlay' is chosen and it is destined to be plot * on top the 'base' layer. If the layer is already present in the view, it will be replaced by the new HiPS/FITS image given here. @@ -1848,7 +1848,7 @@ export let Aladin = (function () { * @memberof Aladin * @param {string} [layer="overlay"] - The name of the layer - * @returns {ImageHiPS|ImageFITS} - The requested image layer. + * @returns {ImageHiPS|Image} - The requested image layer. */ Aladin.prototype.getOverlayImageLayer = function (layer = "overlay") { const survey = this.view.getImageLayer(layer); @@ -1901,7 +1901,7 @@ export let Aladin = (function () { * Get list of overlays layers * * @memberof Aladin - * @returns {MOC[]|Catalog[]|ProgressiveCat[]|GraphicOverlay[]} - Returns the ordered list of image layers. Items can be {@link ImageHiPS} or {@link ImageFITS} objects. + * @returns {MOC[]|Catalog[]|ProgressiveCat[]|GraphicOverlay[]} - Returns the ordered list of image layers. Items can be {@link ImageHiPS} or {@link Image} objects. */ Aladin.prototype.getOverlays = function () { return this.view.allOverlayLayers; @@ -1911,7 +1911,7 @@ export let Aladin = (function () { * Get list of layers * * @memberof Aladin - * @returns {ImageHiPS[]|ImageFITS[]} - Returns the ordered list of image layers. Items can be {@link ImageHiPS} or {@link ImageFITS} objects. + * @returns {ImageHiPS[]|Image[]} - Returns the ordered list of image layers. Items can be {@link ImageHiPS} or {@link Image} objects. */ Aladin.prototype.getStackLayers = function () { return this.view.overlayLayers; @@ -2198,7 +2198,9 @@ aladin.on("positionChanged", ({ra, dec}) => { // TODO : integrate somehow into API ? Aladin.prototype.exportAsPNG = function (downloadFile = false) { (async () => { + const url = await this.getViewDataURL(); + if (downloadFile) { Utils.download(url, "screenshot"); } else { @@ -2231,12 +2233,14 @@ aladin.on("positionChanged", ({ra, dec}) => { var imgFormat = options; options = { format: imgFormat }; } + const canvasDataURL = await this.view.getCanvasDataURL( options.format, options.width, options.height, options.logo ); + return canvasDataURL; }; @@ -2735,7 +2739,7 @@ aladin.on("positionChanged", ({ra, dec}) => { * * @memberof Aladin * @param {string} url - The URL of the FITS image. - * @param {ImageFITSOptions} [options] - Options to customize the display + * @param {ImageOptions} [options] - Options to customize the display * @param {Function} [successCallback=
    ] - The callback function to be executed on a successful display. * The callback gives the ra, dec, and fov of the image; By default, it centers the view on the FITS file loaded. * @param {Function} [errorCallback] - The callback function to be executed if an error occurs during display. @@ -2771,13 +2775,13 @@ aladin.displayFITS( this.gotoRaDec(ra, dec); this.setFoV(fov); }); - const imageFits = this.createImageFITS( + const image = this.createImageFITS( url, options, successCallback, errorCallback ); - return this.setOverlayImageLayer(imageFits, layer); + return this.setOverlayImageLayer(image, layer); }; /** diff --git a/src/js/DefaultActionsForContextMenu.js b/src/js/DefaultActionsForContextMenu.js index 7eca01d8..26e5cae7 100644 --- a/src/js/DefaultActionsForContextMenu.js +++ b/src/js/DefaultActionsForContextMenu.js @@ -102,10 +102,6 @@ export let DefaultActionsForContextMenu = (function () { label: 'New catalogue layer', action(o) { let catBox = new CatalogQueryBox(a) catBox._show({ - header: { - title: 'Add a new catalogue', - draggable: true - }, position: { anchor :'center center' }, diff --git a/src/js/ImageFITS.js b/src/js/ImageFITS.js index 03e096d6..daa43815 100644 --- a/src/js/ImageFITS.js +++ b/src/js/ImageFITS.js @@ -27,11 +27,11 @@ *****************************************************************************/ import { ALEvent } from "./events/ALEvent.js"; import { ColorCfg } from "./ColorCfg.js"; -import { Utils } from "./Utils"; import { HiPSCache } from "./DefaultHiPSCache"; +import { Aladin } from "./Aladin.js"; /** - * @typedef {Object} ImageFITSOptions + * @typedef {Object} ImageOptions * * @property {string} [name] - A human-readable name for the FITS image * @property {Function} [successCallback] - A callback executed when the FITS has been loaded @@ -47,31 +47,32 @@ import { HiPSCache } from "./DefaultHiPSCache"; * @property {number} [saturation=0.0] - The saturation value for the color configuration. * @property {number} [brightness=0.0] - The brightness value for the color configuration. * @property {number} [contrast=0.0] - The contrast value for the color configuration. + * @property {Object} [wcs] - an object describing the WCS of the image. In case of a fits image + * this property will be ignored as the WCS taken will be the one present in the fits file. + * @property {number} [imgFormat='fits'] - The image format of the image to load. */ -export let ImageFITS = (function () { +export let Image = (function () { /** * The object describing a FITS image * * @class - * @constructs ImageFITS + * @constructs Image * * @param {string} url - Mandatory unique identifier for the layer. Can be an arbitrary name - * @param {ImageFITSOptions} [options] - The option for the survey + * @param {ImageOptions} [options] - The option for the survey * */ - function ImageFITS(url, options) { + function Image(url, options) { // Name of the layer this.layer = null; this.added = false; // Set it to a default value this.url = url; this.id = url; - this.ext = options && options.ext; this.name = (options && options.name) || this.url; - - this.imgFormat = "fits"; - this.formats = ["fits"]; + this.imgFormat = (options && options.imgFormat) || "fits"; + this.formats = [this.imgFormat]; // callbacks this.successCallback = options && options.successCallback; this.errorCallback = options && options.errorCallback; @@ -81,29 +82,29 @@ export let ImageFITS = (function () { options.stretch = options.stretch || "asinh"; }*/ this.colorCfg = new ColorCfg(options); + this.options = options; let self = this; - this.query = Promise.resolve(self); } - ImageFITS.prototype._saveInCache = function () { + Image.prototype._saveInCache = function () { if (HiPSCache.contains(self.id)) { HiPSCache.append(this.id, this); } }; // A cache storing directly the images to not query for the properties each time - //ImageFITS.cache = {}; + //Image.cache = {}; - ImageFITS.prototype.setView = function (view) { + Image.prototype.setView = function (view) { this.view = view; this._saveInCache(); }; // @api - ImageFITS.prototype.setOpacity = function (opacity) { + Image.prototype.setOpacity = function (opacity) { let self = this; this._updateMetadata(() => { self.colorCfg.setOpacity(opacity); @@ -111,54 +112,54 @@ export let ImageFITS = (function () { }; // @api - ImageFITS.prototype.setBlendingConfig = function (additive = false) { + Image.prototype.setBlendingConfig = function (additive = false) { this._updateMetadata(() => { this.colorCfg.setBlendingConfig(additive); }); }; // @api - ImageFITS.prototype.setColormap = function (colormap, options) { + Image.prototype.setColormap = function (colormap, options) { this._updateMetadata(() => { this.colorCfg.setColormap(colormap, options); }); }; // @api - ImageFITS.prototype.setCuts = function (lowCut, highCut) { + Image.prototype.setCuts = function (lowCut, highCut) { this._updateMetadata(() => { this.colorCfg.setCuts(lowCut, highCut); }); }; // @api - ImageFITS.prototype.setGamma = function (gamma) { + Image.prototype.setGamma = function (gamma) { this._updateMetadata(() => { this.colorCfg.setGamma(gamma); }); }; // @api - ImageFITS.prototype.setSaturation = function (saturation) { + Image.prototype.setSaturation = function (saturation) { this._updateMetadata(() => { this.colorCfg.setSaturation(saturation); }); }; - ImageFITS.prototype.setBrightness = function (brightness) { + Image.prototype.setBrightness = function (brightness) { this._updateMetadata(() => { this.colorCfg.setBrightness(brightness); }); }; - ImageFITS.prototype.setContrast = function (contrast) { + Image.prototype.setContrast = function (contrast) { this._updateMetadata(() => { this.colorCfg.setContrast(contrast); }); }; // Private method for updating the view with the new meta - ImageFITS.prototype._updateMetadata = function (callback) { + Image.prototype._updateMetadata = function (callback) { if (callback) { callback(); } @@ -184,92 +185,124 @@ export let ImageFITS = (function () { } }; - ImageFITS.prototype.add = function (layer) { + Image.prototype.add = function (layer) { this.layer = layer; let self = this; - const promise = self.view.wasm - .addImageFITS({ - layer: self.layer, - url: self.url, - meta: { - ...this.colorCfg.get(), - longitudeReversed: false, - imgFormat: this.imgFormat, - }, - }) - .then((imagesParams) => { - // There is at least one entry in imageParams - self.added = true; - - self.children = []; - - let hduIdx = 0; - imagesParams.forEach((imageParams) => { - // This fits has HDU extensions - let image = new ImageFITS(imageParams.url, { - name: self.name, - ext: hduIdx.toString() - }); - - // Set the layer corresponding to the onein the backend - image.layer = imageParams.layer; - image.added = true; - image.setView(self.view); - // deep copy of the color object of self - image.colorCfg = Utils.deepCopy(self.colorCfg); - // Set the automatic computed cuts - image.setCuts( - imageParams.automatic_min_cut, - imageParams.automatic_max_cut - ); - - image.ra = imageParams.centered_fov.ra; - image.dec = imageParams.centered_fov.dec; - image.fov = imageParams.centered_fov.fov; - - if (!self.ra) { - self.ra = image.ra; - } - if (!self.dec) { - self.dec = image.dec; + let promise; + if (this.imgFormat === 'fits') { + let id = this.url; + promise = fetch(this.url) + .then((resp) => resp.body) + .then((readableStream) => { + return self.view.wasm + .addImageFITS( + id, + readableStream, + { + ...self.colorCfg.get(), + longitudeReversed: false, + imgFormat: self.imgFormat, + }, + layer + ) + }) + } else if (this.imgFormat === 'jpg' || this.imgFormat === 'jpeg') { + let img = document.createElement('img'); + + promise = + new Promise((resolve, reject) => { + img.src = Aladin.JSONP_PROXY + '?url=' + this.url; + img.crossOrigin = "Anonymous"; + img.onload = () => { + var canvas = document.createElement("canvas"); + + canvas.width = img.width; + canvas.height = img.height; + + // Copy the image contents to the canvas + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height); + + const imageData = ctx.getImageData(0, 0, img.width, img.height); + + const blob = new Blob([imageData.data]); + const stream = blob.stream(1024); + + return resolve(stream) } - if (!self.fov) { - self.fov = image.fov; + + img.onerror = () => { + return reject('Error parsing img ' + self.url) } + }) + .then((readableStream) => { + let wcs = self.options && self.options.wcs; + wcs.NAXIS1 = wcs.NAXIS1 || img.width; + wcs.NAXIS2 = wcs.NAXIS2 || img.height; + + return self.view.wasm + .addImageWithWCS( + readableStream, + wcs, + { + ...self.colorCfg.get(), + longitudeReversed: false, + imgFormat: self.imgFormat, + }, + layer + ) + }) + .finally(() => { + img.remove(); + }) + } else { + console.warn(`Image format: ${this.imgFormat} not supported`); + promise = Promise.reject(); + }; + + promise = promise.then((imageParams) => { + // There is at least one entry in imageParams + self.added = true; + self.setView(self.view); + + // Set the automatic computed cuts + self.setCuts( + imageParams.min_cut, + imageParams.max_cut + ); + + self.ra = imageParams.centered_fov.ra; + self.dec = imageParams.centered_fov.dec; + self.fov = imageParams.centered_fov.fov; + + // Call the success callback on the first HDU image parsed + if (self.successCallback) { + self.successCallback( + self.ra, + self.dec, + self.fov, + self + ); + } - self.children.push(image); + return self; + }) + .catch((e) => { + // This error result from a promise + // If I throw it, it will not be catched because + // it is run async + self.view.removeImageLayer(layer); - hduIdx += 1; - }); - - // Call the success callback on the first HDU image parsed - if (self.successCallback) { - self.successCallback( - self.children[0].ra, - self.children[0].dec, - self.children[0].fov, - self.children[0] - ); - } - - return self; - }) - .catch((e) => { - // This error result from a promise - // If I throw it, it will not be catched because - // it is run async - self.view.removeImageLayer(layer); - - return Promise.reject(e); - }); + return Promise.reject(e); + }); return promise; }; // @api - ImageFITS.prototype.toggle = function () { + Image.prototype.toggle = function () { if (this.colorCfg.getOpacity() != 0.0) { this.colorCfg.setOpacity(0.0); } else { @@ -278,12 +311,12 @@ export let ImageFITS = (function () { }; // FITS images does not mean to be used for storing planetary data - ImageFITS.prototype.isPlanetaryBody = function () { + Image.prototype.isPlanetaryBody = function () { return false; }; // @api - ImageFITS.prototype.focusOn = function () { + Image.prototype.focusOn = function () { // ensure the fits have been parsed if (this.added) { this.view.aladin.gotoRaDec(this.ra, this.dec); @@ -292,35 +325,35 @@ export let ImageFITS = (function () { }; // @oldapi - ImageFITS.prototype.setAlpha = ImageFITS.prototype.setOpacity; + Image.prototype.setAlpha = Image.prototype.setOpacity; - ImageFITS.prototype.setColorCfg = function (colorCfg) { + Image.prototype.setColorCfg = function (colorCfg) { this._updateMetadata(() => { this.colorCfg = colorCfg; }); }; // @api - ImageFITS.prototype.getColorCfg = function () { + Image.prototype.getColorCfg = function () { return this.colorCfg; }; // @api - ImageFITS.prototype.getCuts = function () { + Image.prototype.getCuts = function () { return this.colorCfg.getCuts(); }; // @api - ImageFITS.prototype.getOpacity = function () { + Image.prototype.getOpacity = function () { return this.colorCfg.getOpacity(); }; - ImageFITS.prototype.getAlpha = ImageFITS.prototype.getOpacity; + Image.prototype.getAlpha = Image.prototype.getOpacity; // @api - ImageFITS.prototype.readPixel = function (x, y) { + Image.prototype.readPixel = function (x, y) { return this.view.wasm.readPixel(x, y, this.layer); }; - return ImageFITS; + return Image; })(); diff --git a/src/js/MOC.js b/src/js/MOC.js index 15e6ceeb..9c0681e7 100644 --- a/src/js/MOC.js +++ b/src/js/MOC.js @@ -219,6 +219,20 @@ export let MOC = (function() { return this.view.wasm.mocContains(this.mocParams, ra, dec); }; + /** + * Serialize a MOC into different format + * + * @memberof Aladin + * @param {string} [format='json'] - The output format. Only json is currently supported but 'fits' could be added. + */ + MOC.prototype.serialize = function(format) { + if (!this.ready) { + throw this.name + " is not yet ready, either because it has not been downloaded yet or because it has not been added to the aladin instance." + } + + return this.view.wasm.mocSerialize(this.mocParams, format); + } + return MOC; })(); diff --git a/src/js/MeasurementTable.js b/src/js/MeasurementTable.js index e55a1828..67d342c7 100644 --- a/src/js/MeasurementTable.js +++ b/src/js/MeasurementTable.js @@ -68,11 +68,6 @@ export let MeasurementTable = (function() { title: table.name, label, content, - /*cssStyle: { - backgroundColor: rgbColor, - color: labelColor, - padding: '2px', - }*/ } }); diff --git a/src/js/Overlay.js b/src/js/Overlay.js index 6d595007..8a76ee4f 100644 --- a/src/js/Overlay.js +++ b/src/js/Overlay.js @@ -159,6 +159,28 @@ export let GraphicOverlay = (function() { k += 3; } + } else if (s=='ellipse') { + var frame; + k++; + frame = parts[k].toLowerCase(); + if (Utils.isNumber(frame)) { + frame = 'icrs' + k--; + } + + if (frame=='icrs' || frame=='j2000' || frame=='fk5') { + var ra, dec, a, b, theta; + + ra = parseFloat(parts[k+1]); + dec = parseFloat(parts[k+2]); + a = parseFloat(parts[k+3]); + b = parseFloat(parts[k+4]); + theta = parseFloat(parts[k+5]); + + footprints.push(A.ellipse(ra, dec, a, b, theta, options)); + + k += 5; + } } k++; diff --git a/src/js/View.js b/src/js/View.js index 8834c8f2..4491bff5 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -49,7 +49,7 @@ import { Layout } from "./gui/Layout.js"; import { SAMPActionButton } from "./gui/Button/SAMP.js"; import { HiPSCache } from "./DefaultHiPSCache.js"; import { ImageHiPS } from "./ImageHiPS.js"; -import { ImageFITS } from "./ImageFITS.js"; +import { Image } from "./ImageFITS.js"; export let View = (function () { @@ -462,7 +462,7 @@ export let View = (function () { View.prototype.getCanvas = async function (imgType, width, height, withLogo=true) { const loadImage = function (url) { return new Promise((resolve, reject) => { - const image = new Image() + const image = document.createElement("img") image.src = url image.onload = () => resolve(image) image.onerror = () => reject(new Error('could not load image')) @@ -482,13 +482,16 @@ export let View = (function () { ctx.drawImage(canvas, 0, 0, c.width, c.height); ctx.drawImage(this.catalogCanvas, 0, 0, c.width, c.height); + if(withLogo) { - const logo = await loadImage("data:image/png;base64,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"); + const logo = await loadImage("data:image/png;base64,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") + const offX = c.width - logo.width; const offY = c.height - logo.height; ctx.drawImage(logo, offX, offY); } + return c; } @@ -1507,6 +1510,7 @@ export let View = (function () { return source; }); + let table = { 'name': catalog.name, 'color': catalog.color, @@ -1690,7 +1694,7 @@ export let View = (function () { Promise.allSettled(this.promises) .then(() => imageLayerPromise) // The promise is resolved and we now have access - // to the image layer objet (whether it is an ImageHiPS or an ImageFITS) + // to the image layer objet (whether it is an ImageHiPS or an Image) .then((imageLayer) => { // Add to the backend const promise = imageLayer.add(layer); @@ -1702,16 +1706,7 @@ export let View = (function () { // If the image layer has successfuly been added this.empty = false; - if (imageLayer.children) { - imageLayer.children.forEach((imageExtLayer) => { - this._addLayer(imageExtLayer); - }) - - // remove the original fits from the cache as we add separately its extensions instead - //HiPSCache.delete(imageLayer.id) - } else { - this._addLayer(imageLayer); - } + this._addLayer(imageLayer); // change the view frame in case we have a planetary hips loaded if (imageLayer.hipsBody) { @@ -1847,7 +1842,7 @@ export let View = (function () { }; View.prototype.contains = function(survey) { - if (survey instanceof ImageHiPS || survey instanceof ImageFITS) { + if (survey instanceof ImageHiPS || survey instanceof Image) { if (survey.added === true) { return true; } diff --git a/src/js/gui/Box/CatalogQueryBox.js b/src/js/gui/Box/CatalogQueryBox.js index 80bd5575..99bf102e 100644 --- a/src/js/gui/Box/CatalogQueryBox.js +++ b/src/js/gui/Box/CatalogQueryBox.js @@ -28,7 +28,7 @@ import A from "../../A.js"; import { Dropdown } from "../Input/Dropdown.js"; import { ConeSearchActionButton } from "../Button/ConeSearch.js"; import targetIconUrl from '../../../../assets/icons/target.svg'; -import downloadIconUrl from '../../../../assets/icons/download.svg'; +import hipsIconUrl from '../../../../assets/icons/hips.svg'; import { ActionButton } from "../Widgets/ActionButton.js"; /****************************************************************************** @@ -64,7 +64,7 @@ import { ActionButton } from "../Widgets/ActionButton.js"; params.id.replace('CDS/', ''), params.ra + ' ' + params.dec, params.radiusDeg, - {limit: params.limit, onClick: 'showTable'}, + {limit: params.limit, onClick: 'showTable', hoverColor: 'red'}, (catalog) => { aladin.addCatalog(catalog) }, @@ -75,7 +75,7 @@ import { ActionButton } from "../Widgets/ActionButton.js"; A.catalogFromSimbad( params.ra + ' ' + params.dec, params.radiusDeg, - {limit: params.limit, onClick: 'showTable'}, + {limit: params.limit, onClick: 'showTable', hoverColor: 'red'}, (catalog) => { aladin.addCatalog(catalog) }, @@ -90,7 +90,7 @@ import { ActionButton } from "../Widgets/ActionButton.js"; url += 'RA=' + params.ra + '&DEC=' + params.dec + '&SR=' + params.radiusDeg; A.catalogFromURL( url, - {limit: params.limit, onClick: 'showTable'}, + {limit: params.limit, onClick: 'showTable', hoverColor: 'red'}, (catalog) => { aladin.addCatalog(catalog) }, @@ -102,7 +102,7 @@ import { ActionButton } from "../Widgets/ActionButton.js"; const hips = A.catalogHiPS(params.hipsURL, {onClick: 'showTable', name: params.id}); aladin.addCatalog(hips); } else if (type=='votable') { - A.catalogFromURL(params.url, {name: params.url}, (catalog) => { + A.catalogFromURL(params.url, {name: params.url, hoverColor: 'red'}, (catalog) => { aladin.addCatalog(catalog) params.success() }, params.error); @@ -182,9 +182,13 @@ import { ActionButton } from "../Widgets/ActionButton.js"; let hipsCatLoad = new ActionButton({ icon: { monochrome: true, - url: downloadIconUrl, + url: hipsIconUrl, size: 'small', }, + tooltip: { + content: "Load the progressive tiled catalog.
    Adapted for rendering big catalogs", + position: {direction: "bottom"} + }, content: 'HiPS', disable: true, action() { diff --git a/src/js/gui/Box/HiPSBrowserBox.js b/src/js/gui/Box/HiPSBrowserBox.js index 329631bc..4d33fc61 100644 --- a/src/js/gui/Box/HiPSBrowserBox.js +++ b/src/js/gui/Box/HiPSBrowserBox.js @@ -22,6 +22,7 @@ import { MocServer } from "../../MocServer.js"; import { Box } from "../Widgets/Box.js"; import { Dropdown } from "../Input/Dropdown.js"; import filterOnUrl from "../../../../assets/icons/filter-on.svg"; +import hipsIconUrl from "../../../../assets/icons/hips.svg"; import filterOffUrl from "../../../../assets/icons/filter-off.svg"; import { Input } from "../Widgets/Input.js"; import { TogglerActionButton } from "../Button/Toggler.js"; @@ -31,6 +32,7 @@ import A from "../../A.js"; import { Utils } from "../../Utils.ts"; import { ActionButton } from "../Widgets/ActionButton.js"; import infoIconUrl from "../../../../assets/icons/info.svg" +import { Icon } from "../Widgets/Icon.js"; /****************************************************************************** * Aladin Lite project @@ -200,7 +202,11 @@ export class HiPSBrowserBox extends Box { { close: true, header: { - title: "HiPS browser", + title: Layout.horizontal([new Icon({ + size: 'medium', + url: hipsIconUrl, + monochrome: true, + }), "HiPS browser"]) }, onDragged: () => { if (self.filterBtn.toggled) { diff --git a/src/js/gui/Box/StackBox.js b/src/js/gui/Box/StackBox.js index 853ac1a8..e04ae046 100644 --- a/src/js/gui/Box/StackBox.js +++ b/src/js/gui/Box/StackBox.js @@ -35,21 +35,22 @@ import A from "../../A.js"; import { Utils } from "../../Utils"; import { View } from "../../View.js"; import { HiPSSettingsBox } from "./HiPSSettingsBox.js"; -import searchIconUrl from "../../../../assets/icons/search.svg"; +import hipsIconUrl from "../../../../assets/icons/hips.svg"; import showIconUrl from "../../../../assets/icons/show.svg"; import addIconUrl from "../../../../assets/icons/plus.svg"; import hideIconUrl from "../../../../assets/icons/hide.svg"; import removeIconUrl from "../../../../assets/icons/remove.svg"; import settingsIconUrl from "../../../../assets/icons/settings.svg"; - import searchIconImg from "../../../../assets/icons/search.svg"; +import downloadIconUrl from '../../../../assets/icons/download.svg'; + import { TogglerActionButton } from "../Button/Toggler.js"; import { Icon } from "../Widgets/Icon.js"; import { Box } from "../Widgets/Box.js"; import { CtxMenuActionButtonOpener } from "../Button/CtxMenuOpener.js"; import { Input } from "../Widgets/Input.js"; -import { ImageFITS } from "../../ImageFITS.js"; +import { Image } from "../../ImageFITS.js"; import { HiPSCache } from "../../DefaultHiPSCache.js"; import { HiPSBrowserBox } from "./HiPSBrowserBox.js"; @@ -323,8 +324,6 @@ export class OverlayStackBox extends Box { o.preventDefault(); o.stopPropagation(); - //self._hide(); - self.aladin.select( "circle", (c) => { @@ -544,7 +543,7 @@ export class OverlayStackBox extends Box { { label: { icon: { - url: searchIconUrl, + url: hipsIconUrl, monochrome: true, tooltip: { content: "From our database...", @@ -787,15 +786,13 @@ export class OverlayStackBox extends Box { // list of overlays for (const overlay of overlays) { const name = overlay.name; - let showBtn = new ActionButton({ + let optBtn = []; + optBtn.push(new ActionButton({ size: "small", icon: { url: overlay.isShowing ? showIconUrl : hideIconUrl, monochrome: true, }, - /*cssStyle: { - visibility: Utils.hasTouchScreen() ? 'visible' : 'hidden', - },*/ tooltip: { content: overlay.isShowing ? "Hide" : "Show", position: { direction: "top" }, @@ -815,9 +812,9 @@ export class OverlayStackBox extends Box { }); } }, - }); + })); - let deleteBtn = new ActionButton({ + optBtn.push(new ActionButton({ icon: { url: removeIconUrl, monochrome: true, @@ -833,7 +830,27 @@ export class OverlayStackBox extends Box { action(e) { self.aladin.removeLayer(overlay); }, - }); + })); + + if (overlay.serialize) { + optBtn.push(new ActionButton({ + icon: { + url: downloadIconUrl, + monochrome: true, + }, + size: "small", + tooltip: { + content: "Download JSON MOC", + position: { direction: "top" }, + }, + action(e) { + let json = overlay.serialize('json'); + let blob = new Blob([json]); + Utils.download(URL.createObjectURL(blob), overlay.name + '.json'); + }, + })); + } + let item = Layout.horizontal({ layout: [ @@ -841,7 +858,7 @@ export class OverlayStackBox extends Box { '
    ' + name + "
    ", - Layout.horizontal({ layout: [showBtn, deleteBtn] }), + Layout.horizontal({ layout: optBtn }), ], cssStyle: { textAlign: "center", @@ -905,7 +922,7 @@ export class OverlayStackBox extends Box { let HiPS = self.cachedHiPS[name]; let image; - if (HiPS instanceof ImageFITS) { + if (HiPS instanceof Image) { image = HiPS; } else { // HiPS @@ -987,6 +1004,7 @@ export class OverlayStackBox extends Box { icon: { url: Icon.dataURLFromSVG({ svg: Icon.SVG_ICONS.MOC }), + size: "small", monochrome: true, }, tooltip: { @@ -1059,7 +1077,7 @@ export class OverlayStackBox extends Box { let btns = [showBtn, settingsBtn]; - if (!(layer instanceof ImageFITS)) { + if (!(layer instanceof Image)) { btns.push(loadMOCBtn); } btns.push(deleteBtn); diff --git a/src/js/gui/Button/SAMP.js b/src/js/gui/Button/SAMP.js index 78bfab18..444f2aa0 100644 --- a/src/js/gui/Button/SAMP.js +++ b/src/js/gui/Button/SAMP.js @@ -139,7 +139,7 @@ options = { for (const objects of aladin.view.selection) { let s0 = getSource(objects[0]); const cat = s0.catalog; - console.log(cat) + const {url, name} = cat; conn.loadVOTable(url, name, url); diff --git a/src/js/gui/Widgets/Input.js b/src/js/gui/Widgets/Input.js index fd3d18c7..d8f1277c 100644 --- a/src/js/gui/Widgets/Input.js +++ b/src/js/gui/Widgets/Input.js @@ -116,7 +116,6 @@ export class Input extends DOMElement { // calculate adjustment factor var scale = (maxv-minv) / (maxp-minp); - console.log('value', value) return (Math.log(value)-minv) / scale + minp; } @@ -131,7 +130,6 @@ export class Input extends DOMElement { // calculate adjustment factor var scale = (maxv-minv) / (maxp-minp); - console.log(minv, maxv) return (Math.log(value)-minv) / scale + minp; } diff --git a/src/js/gui/Widgets/Tab.js b/src/js/gui/Widgets/Tab.js index ccbac9d5..bb347d05 100644 --- a/src/js/gui/Widgets/Tab.js +++ b/src/js/gui/Widgets/Tab.js @@ -19,6 +19,8 @@ import { Tooltip } from "./Tooltip"; import { SAMPActionButton } from '../Button/SAMP.js'; +import downloadIconUrl from '../../../../assets/icons/download.svg'; +import { Utils } from "../../Utils"; /****************************************************************************** * Aladin Lite project @@ -33,6 +35,7 @@ import { SAMPActionButton } from '../Button/SAMP.js'; *****************************************************************************/ import { DOMElement } from "./Widget"; import { Layout } from "../Layout"; +import { ActionButton } from "./ActionButton"; /** * Class representing a Tabs layout * @extends DOMElement @@ -49,7 +52,8 @@ export class Tabs extends DOMElement { let contentTabOptions = []; let tabsLayout = []; - for (const tab of options.layout) { + let self; + options.layout.forEach((tab, index) => { // Create the content tab div let contentTabOptionEl = document.createElement("div"); contentTabOptionEl.style.display = 'none'; @@ -83,17 +87,58 @@ export class Tabs extends DOMElement { } tab.label.update({toggled: true}) + self.tabSelectedIdx = index; }, }); tab.label.addClass('tab') tabsLayout.push(tab.label); - } + }) if (options.aladin && options.aladin.samp) { tabsLayout.push(SAMPActionButton.sendSources(options.aladin)) } + let aladin = options.aladin; + tabsLayout.push(new ActionButton({ + icon: { + url: downloadIconUrl, + monochrome: true, + }, + size: "small", + tooltip: { + content: "Download the selected tab as CSV", + position: { direction: "top" }, + }, + action(e) { + // retrieve the sources of the currently selected tab + let getSource = (o) => { + let s = o; + if (o.source) { + s = o.source + } + + return s; + }; + + let fieldNames = Object.keys(aladin.view.selection[self.tabSelectedIdx][0].data).join(";"); + var lineArray = [fieldNames]; + + aladin.view.selection[self.tabSelectedIdx].forEach((obj) => { + let source = getSource(obj) + + let line = Array.from(Object.values(source.data)).join(";"); + lineArray.push(line); + }) + + var csvContent = lineArray.join("\n"); + var blob = new Blob([csvContent]); + let cat = aladin.view.selection[self.tabSelectedIdx][0].getCatalog(); + + Utils.download(URL.createObjectURL(blob), cat.name + '-selection.csv'); + }, + })); + let contentTabEl = document.createElement("div"); contentTabEl.style.maxWidth = '100%'; @@ -114,6 +159,8 @@ export class Tabs extends DOMElement { super(el, options); this._show(); + self = this; + this.tabSelectedIdx = 0; this.attachTo(target, position); } diff --git a/src/js/gui/Widgets/Table.js b/src/js/gui/Widgets/Table.js index 19cacb98..0da4a76b 100644 --- a/src/js/gui/Widgets/Table.js +++ b/src/js/gui/Widgets/Table.js @@ -56,10 +56,10 @@ export class Table extends DOMElement { this.addClass("aladin-dark-theme") } - static _createTableBody = function(opt) { + static _createTableBody = function(options) { const tbody = document.createElement('tbody'); - opt.rows.forEach((row) => { + options.rows.forEach((row) => { let trEl = document.createElement('tr'); for (let key in row.data) { @@ -68,8 +68,8 @@ export class Table extends DOMElement { let tdEl = document.createElement('td'); tdEl.classList.add(key); - if (opt.showCallback && opt.showCallback[key]) { - let showFieldCallback = opt.showCallback[key]; + if (options.showCallback && options.showCallback[key]) { + let showFieldCallback = options.showCallback[key]; let el = showFieldCallback(row.data); if (el instanceof Element) { @@ -93,11 +93,11 @@ export class Table extends DOMElement { return tbody; } - static _createTableHeader = function(opt) { + static _createTableHeader = function(options) { let theadElement = document.createElement('thead'); var content = ''; - for (let [_, field] of Object.entries(opt.fields)) { + for (let [_, field] of Object.entries(options.fields)) { if (field.name) { content += '' + field.name + ''; }