async fits parsing ok

This commit is contained in:
Matthieu BAUMANN
2023-02-28 19:42:04 +01:00
parent 125de63f5e
commit 36933ff6ac
18 changed files with 531 additions and 198 deletions

View File

@@ -34,6 +34,7 @@ fitsrs = "0.1.1"
enum_dispatch = "0.3.8"
wasm-bindgen = "0.2.79"
wasm-streams = "0.3.0"
async-channel = "1.8.0"
al-core = { path = "./al-core" }
al-task-exec = { path = "./al-task-exec" }
al-api = { path = "./al-api" }

View File

@@ -1,5 +1,8 @@
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[wasm_bindgen]
pub struct FoV {
/// Position of the field of view

View File

@@ -25,6 +25,7 @@ struct Texture2DMeta {
}
use web_sys::WebGlTexture;
#[derive(Clone)]
pub struct Texture2D {
pub texture: Option<WebGlTexture>,

View File

@@ -28,6 +28,7 @@ use al_api::{
grid::GridCfg,
hips::{ImageMetadata, HiPSCfg, FITSCfg}, fov::FoV,
};
use wasm_bindgen_futures::JsFuture;
use crate::Abort;
use super::coosys;
use cgmath::Vector4;
@@ -87,10 +88,17 @@ pub struct App {
colormaps: Colormaps,
projection: ProjectionType,
// Async data receivers
fits_send: async_channel::Sender<FitsCfg>,
fits_recv: async_channel::Receiver<FitsCfg>,
ack_send: async_channel::Sender<()>,
ack_recv: async_channel::Receiver<()>,
}
use cgmath::{Vector2, Vector3};
use futures::stream::StreamExt; // for `next`
use futures::{stream::StreamExt, io::BufReader}; // for `next`
/// State for inertia
struct InertiaAnimation {
@@ -189,6 +197,9 @@ impl App {
gl.clear_color(0.15, 0.15, 0.15, 1.0);
let (fits_send, fits_recv) = async_channel::unbounded::<FitsCfg>();
let (ack_send, ack_recv) = async_channel::unbounded::<()>();
Ok(App {
gl,
start_time_frame,
@@ -229,6 +240,11 @@ impl App {
colormaps,
projection,
fits_send,
fits_recv,
ack_send,
ack_recv
})
}
@@ -692,6 +708,20 @@ impl App {
}
}*/
// Check for async retrieval
if let Ok(fits) = self.fits_recv.try_recv() {
al_core::log("received");
self.layers.add_image_fits(fits, &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();
wasm_bindgen_futures::spawn_local(async move {
ack_send.send(()).await
.unwrap_throw();
})
}
self.draw(false)?;
Ok(())
@@ -853,29 +883,74 @@ impl App {
Ok(())
}
pub(crate) fn add_image_fits(&mut self, cfg: FITSCfg, bytes: &[u8]) -> Result<FoV, JsValue> {
pub(crate) fn add_image_fits(&mut self, cfg: FITSCfg) -> Result<js_sys::Promise, JsValue> {
let FITSCfg { layer, url, meta } = cfg;
al_core::log(&format!("url: {:?}", url));
let gl = self.gl.clone();
let fits_sender = self.fits_send.clone();
let ack_recv = self.ack_recv.clone();
let fut = async move {
use wasm_streams::ReadableStream;
use js_sys::Uint8Array;
use web_sys::Response;
use web_sys::window;
use wasm_bindgen_futures::JsFuture;
use futures::StreamExt;
use wasm_bindgen::JsCast;
use crate::renderable::image::FitsImage;
use futures::TryStreamExt;
let window = window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_str(&url))
.await?;
let resp: Response = resp_value.dyn_into()?;
let fits = FitsImage::new(&self.gl, bytes)?;
let center = fits.get_center();
let fov = FoV {
ra: center.lon().to_degrees(),
dec: center.lat().to_degrees(),
fov: 1.0
// Get the response's body as a JS ReadableStream
let raw_body = resp.body().unwrap();
let body = ReadableStream::from_raw(raw_body.dyn_into()?);
al_core::log("begin to parse fits!");
// Convert the JS ReadableStream to a Rust stream
let bytes_reader = body
.into_stream()
.map_ok(|js_value| js_value.dyn_into::<Uint8Array>().unwrap_throw().to_vec())
.map_err(|_js_error| std::io::Error::new(std::io::ErrorKind::Other, "failed to read"))
.into_async_read();
let fits = FitsImage::new_async(&gl, BufReader::new(bytes_reader)).await?;
al_core::log("fits parsed");
let center = fits.get_center();
let ra = center.lon().to_degrees();
let dec = center.lat().to_degrees();
let fov = 1.0;
let fits_cfg = FitsCfg {
fits: fits,
layer: layer,
url: url,
meta: meta
};
fits_sender.send(fits_cfg).await
.unwrap();
// Wait for the ack
if let Ok(_) = ack_recv.recv().await {
let fov = FoV {
ra: ra,
dec: dec,
fov: fov,
};
Ok(serde_wasm_bindgen::to_value(&fov).unwrap())
} else {
Err(JsValue::from_str("Problem receving fits"))
}
};
let cfg = FitsCfg {
layer,
url,
fits,
meta,
};
self.layers.add_image_fits(cfg, &mut self.camera, &self.projection)?;
// Once its added, request the tiles in the view (unless the viewer is at depth 0)
self.request_redraw = true;
Ok(fov)
let promise = wasm_bindgen_futures::future_to_promise(fut);
Ok(promise)
}
pub(crate) fn get_layer_cfg(&self, layer: &str) -> Result<ImageMetadata, JsValue> {

View File

@@ -66,6 +66,7 @@ mod utils;
use math::projection::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
mod app;
pub mod async_task;
@@ -98,6 +99,7 @@ use al_api::hips::HiPSProperties;
use al_api::coo_system::CooSystem;
use al_api::color::{Color, ColorRGBA};
use al_api::fov::FoV;
use al_api::hips::FITSCfg;
use al_core::Colormap;
use al_core::{WebGlContext};
@@ -266,7 +268,7 @@ impl WebClient {
/// # Examples
///
/// ```javascript
/// let al = new Aladin.wasmLibs.webgl.WebClient(...);
/// let al = new Aladin.wasmLibs.core.WebClient(...);
/// const panstarrs = {
/// properties: {
/// url: "http://alasky.u-strasbg.fr/Pan-STARRS/DR1/r",
@@ -309,10 +311,10 @@ impl WebClient {
}
#[wasm_bindgen(js_name = addImageFITS)]
pub fn add_image_fits(&mut self, fits_cfg: JsValue, bytes: &[u8]) -> Result<FoV, JsValue> {
let fits_cfg = serde_wasm_bindgen::from_value(fits_cfg)?;
pub fn add_image_fits(&mut self, fits_cfg: JsValue) -> Result<js_sys::Promise, JsValue> {
let fits_cfg: FITSCfg = serde_wasm_bindgen::from_value(fits_cfg)?;
self.app.add_image_fits(fits_cfg, bytes)
self.app.add_image_fits(fits_cfg)
}
#[wasm_bindgen(js_name = removeLayer)]

View File

@@ -1,6 +1,7 @@
use std::vec;
use al_api::hips::ImageMetadata;
use futures::StreamExt;
use moclib::moc::range::RangeMOC;
use moclib::qty::Hpx;
use moclib::elem::cell::Cell;
@@ -13,7 +14,7 @@ use web_sys::WebGl2RenderingContext;
use al_api::cell::HEALPixCellProjeted;
use al_api::coo_system::CooSystem;
use al_core::{VertexArrayObject, Texture2D};
use al_core::{VertexArrayObject, Texture2D, pixel};
use al_core::WebGlContext;
use al_core::VecData;
use al_core::webgl_ctx::GlWrapper;
@@ -26,16 +27,18 @@ use crate::ShaderManager;
use crate::Colormaps;
use fitsrs::{
fits::Fits,
fits::{AsyncFits, Fits},
hdu::{
data_async::DataOwned,
HDU,
AsyncHDU,
data::DataBorrowed
}
};
use wcs::ImgXY;
use wcs::WCS;
use wasm_bindgen::JsValue;
use wasm_bindgen::{JsValue, UnwrapThrowExt};
pub struct FitsImage {
// The vertex array object of the screen in NDC
@@ -58,13 +61,221 @@ pub struct FitsImage {
center: LonLat,
}
use crate::time::Time;
use futures::io::BufReader;
impl FitsImage {
pub fn new<'a>(
pub fn from_bytes(
gl: &WebGlContext,
raw_bytes: &'a [u8],
bytes: &[u8]
) -> Result<Self, JsValue> {
// Load the fits file
let Fits { hdu: HDU { header, data } } = Fits::from_reader(raw_bytes)
al_core::log("jdjdjdj start");
let Fits { hdu: HDU { header, data } } = Fits::from_reader(bytes)
.map_err(|_| JsValue::from_str("Fits cannot be parsed"))?;
al_core::log("jdjdjdj");
let scale = header
.get_parsed::<f64>(b"BSCALE ")
.unwrap_or(Ok(1.0))
.unwrap() as f32;
let offset = header
.get_parsed::<f64>(b"BZERO ")
.unwrap_or(Ok(0.0))
.unwrap() as f32;
let blank = header
.get_parsed::<f64>(b"BLANK ")
.unwrap_or(Ok(std::f64::NAN))
.unwrap() as f32;
al_core::log("jdjdjdj2");
// Create a WCS from a specific header unit
let wcs = WCS::new(&header).map_err(|_| JsValue::from_str("Failed to parse the WCS"))?;
let (w, h) = wcs.img_dimensions();
let width = w as f64;
let height = h as f64;
let tex_params = &[
(
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::NEAREST,
),
(
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::NEAREST,
),
// Prevents s-coordinate wrapping (repeating)
(
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE,
),
// Prevents t-coordinate wrapping (repeating)
(
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE,
),
];
al_core::log("jdjdjdj3");
let texture = match data {
DataBorrowed::U8(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R8UI>(gl, w as i32, h as i32, tex_params, Some(&data))?
},
DataBorrowed::I16(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R16I>(gl, w as i32, h as i32, tex_params, Some(&data))?
},
DataBorrowed::I32(data) => {
al_core::log("jdjdjd4");
al_core::log("jdjdjdj5");
Texture2D::create_from_raw_pixels::<al_core::image::format::R32I>(gl, w as i32, h as i32, tex_params, Some(&data))?
},
DataBorrowed::I64(data) => {
let values: Vec<f32> = data.iter().map(|v| {
*v as f32
})
.collect();
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},
DataBorrowed::F32(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(data))?
},
DataBorrowed::F64(data) => {
let values: Vec<f32> = data.iter().map(|v| {
*v as f32
})
.collect();
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},
};
let bl = wcs.unproj_lonlat(&ImgXY::new(0.0, 0.0)).ok_or(JsValue::from_str("(0, 0) px cannot be unprojected"))?;
let br = wcs.unproj_lonlat(&ImgXY::new(width - 1.0, 0.0)).ok_or(JsValue::from_str("(w - 1, 0) px cannot be unprojected"))?;
let tr = wcs.unproj_lonlat(&ImgXY::new(width - 1.0, height - 1.0)).ok_or(JsValue::from_str("(w - 1, h - 1) px cannot be unprojected"))?;
let tl = wcs.unproj_lonlat(&ImgXY::new(0.0, height - 1.0)).ok_or(JsValue::from_str("(0, h - 1) px cannot be unprojected"))?;
let center = wcs.unproj_lonlat(&ImgXY::new(width / 2.0, height / 2.0)).ok_or(JsValue::from_str("(w / 2, h / 2) px cannot be unprojected"))?;
let mut num_moc_cells = std::usize::MAX;
let mut depth = 11;
let mut moc = RangeMOC::new_empty(0);
while num_moc_cells > 5 && depth > 3 {
depth = depth - 1;
moc = RangeMOC::from_polygon_with_control_point(
&[
(bl.lon(), bl.lat()),
(br.lon(), br.lat()),
(tr.lon(), tr.lat()),
(tl.lon(), tl.lat()),
],
(center.lon(), center.lat()),
depth
);
num_moc_cells = (&moc).into_range_moc_iter().cells().count();
}
let pos = vec![];
let uv = vec![];
let indices = vec![];
// Define the buffers
let vao = {
let mut vao = VertexArrayObject::new(gl);
#[cfg(feature = "webgl2")]
vao.bind_for_update()
// layout (location = 0) in vec2 ndc_pos;
.add_array_buffer_single(
2,
"ndc_pos",
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<f32>(&pos),
)
.add_array_buffer_single(
2,
"uv",
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<f32>(&uv),
)
// Set the element buffer
.add_element_buffer(
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<u32>(&indices),
)
.unbind();
#[cfg(feature = "webgl1")]
vao.bind_for_update()
.add_array_buffer_single(
2,
"ndc_pos",
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<f32>(&pos),
)
.add_array_buffer_single(
2,
"uv",
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<f32>(&uv),
)
// Set the element buffer
.add_element_buffer(
WebGl2RenderingContext::DYNAMIC_DRAW,
VecData::<u32>(&indices),
)
.unbind();
vao
};
// Automatic methods to compute the min and max cut values
/*let mut values = values.into_iter()
.filter(|x| !x.is_nan() && *x != blank)
.collect::<Vec<_>>();
let n = values.len();
let first_pct_idx = (0.05 * (n as f32)) as usize;
let last_pct_idx = (0.95 * (n as f32)) as usize;
let min_val = crate::utils::select_kth_smallest(&mut values[..], 0, n - 1, first_pct_idx);
let max_val = crate::utils::select_kth_smallest(&mut values[..], 0, n - 1, last_pct_idx);
*/
//al_core::log(&format!("values: {} {}", min_val, max_val));
let gl = gl.clone();
let image = FitsImage {
vao,
wcs,
moc,
gl,
pos,
uv,
indices,
texture,
scale,
offset,
blank,
center,
};
Ok(image)
}
pub async fn new_async<'a, R>(
gl: &WebGlContext,
reader: BufReader<R>,
) -> Result<Self, JsValue>
where
R: futures::AsyncRead + std::marker::Unpin + std::fmt::Debug
{
// Load the fits file
let AsyncFits { hdu: AsyncHDU { header, data } } = AsyncFits::from_reader(reader)
.await
.map_err(|_| JsValue::from_str("Fits cannot be parsed"))?;
let scale = header
@@ -108,36 +319,70 @@ impl FitsImage {
];
let texture = match data {
DataBorrowed::U8(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R8UI>(gl, w as i32, h as i32, tex_params, Some(data))?
DataOwned::U8(data) => {
let data = data.collect::<Vec<u8>>().await;
Texture2D::create_from_raw_pixels::<al_core::image::format::R8UI>(gl, w as i32, h as i32, tex_params, Some(&data))?
},
DataBorrowed::I16(data) => {
let values: Vec<f32> = data.into_iter().map(|v| {
*v as f32
DataOwned::I16(data) => {
let values: Vec<f32> = data.map(|v| {
v as f32
})
.collect();
.collect().await;
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},
DataBorrowed::I32(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R32I>(gl, w as i32, h as i32, tex_params, Some(data))?
DataOwned::I32(mut data) => {
let texture = Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, None)?;
let tex_bound = texture.bind();
let mut dy = 0;
let mut pixels_processed = 0;
let tot_pixels = w * h;
let mut bytes_chunk = Vec::new();
while pixels_processed < tot_pixels {
let chunk_num_pixels = w; // todo adapt the size of the chunk
bytes_chunk.clear();
for _ in 0..chunk_num_pixels {
let value = data.next().await.unwrap_throw() as f32;
bytes_chunk.extend(value.to_le_bytes());
}
tex_bound.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
0,
dy,
w as i32,
1,
Some(&bytes_chunk)
);
dy += 1;
pixels_processed += chunk_num_pixels;
}
texture
},
DataBorrowed::I64(data) => {
let values: Vec<f32> = data.into_iter().map(|v| {
*v as f32
DataOwned::I64(data) => {
let values: Vec<f32> = data.map(|v| {
v as f32
})
.collect();
.collect().await;
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},
DataBorrowed::F32(data) => {
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(data))?
DataOwned::F32(data) => {
al_core::log("f32");
let values: Vec<f32> = data.collect().await;
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},
DataBorrowed::F64(data) => {
let values: Vec<f32> = data.into_iter().map(|v| {
*v as f32
DataOwned::F64(data) => {
al_core::log("f64");
let values: Vec<f32> = data.map(|v| {
v as f32
})
.collect();
.collect().await;
Texture2D::create_from_raw_pixels::<al_core::image::format::R32F>(gl, w as i32, h as i32, tex_params, Some(&values))?
},

View File

@@ -45,6 +45,11 @@ pub unsafe fn transmute_boxed_slice<I, O>(s: Box<[I]>) -> Box<[O]> {
Box::from_raw(out_slice_ptr)
}
pub unsafe fn transmute_vec_to_u8<I>(mut s: Vec<I>) -> Vec<u8> {
s.set_len(std::mem::size_of_val(&s[..]));
std::mem::transmute(s)
}
/// Select the kth smallest element in a slice
///
/// This is a basic implementation of quickselect algorithm: https://fr.wikipedia.org/wiki/Quickselect