WIP: spectra plot

This commit is contained in:
Matthieu Baumann
2025-07-12 15:01:33 +02:00
committed by Matthieu Baumann
parent e4689cf674
commit 5c6405bf8b
15 changed files with 182 additions and 88 deletions

View File

@@ -24,7 +24,6 @@
showFrame: true,
showZoomControl:true,
showSettingsControl:true,
showCooGrid: true,
fullScreen: true,
samp: true,
}

View File

@@ -18,12 +18,12 @@ futures = "0.3.12"
js-sys = "0.3.47"
wasm-bindgen-futures = "0.4.20"
cgmath = "*"
url-lite = "0.1.0"
# url-lite = "0.1.0"
serde_json = "1.0.104"
serde-wasm-bindgen = "0.5"
enum_dispatch = "0.3.8"
wasm-bindgen = "=0.2.92"
wasm-streams = "0.3.0"
#wasm-streams = "0.3.0"
async-channel = "1.8.0"
mapproj = "0.3.0"
fitsrs = "0.3.4"
@@ -64,7 +64,7 @@ path = "./al-api"
[dependencies.web-sys]
version = "0.3.56"
features = [ "console", "CssStyleDeclaration", "Document", "Element", "HtmlCollection", "HtmlElement", "HtmlImageElement", "HtmlCanvasElement", "Blob", "ImageBitmap", "ImageData", "CanvasRenderingContext2d", "WebGlBuffer", "WebGlContextAttributes", "WebGlFramebuffer", "WebGlProgram", "WebGlShader", "WebGlUniformLocation", "WebGlTexture", "WebGlActiveInfo", "Headers", "Window", "Request", "RequestInit", "RequestMode", "RequestCredentials", "Response", "XmlHttpRequest", "XmlHttpRequestResponseType", "PerformanceTiming", "Performance", "Url", "ReadableStream", "File", "FileList",]
features = [ "console", "CssStyleDeclaration", "Document", "Element", "HtmlCollection", "CustomEvent", "CustomEventInit", "HtmlElement", "HtmlImageElement", "HtmlCanvasElement", "Blob", "ImageBitmap", "ImageData", "CanvasRenderingContext2d", "WebGlBuffer", "WebGlContextAttributes", "WebGlFramebuffer", "WebGlProgram", "WebGlShader", "WebGlUniformLocation", "WebGlTexture", "WebGlActiveInfo", "Headers", "Window", "Request", "RequestInit", "RequestMode", "RequestCredentials", "Response", "XmlHttpRequest", "XmlHttpRequestResponseType", "PerformanceTiming", "Performance", "Url", "ReadableStream", "File", "FileList",]
[dev-dependencies.image-decoder]
package = "image"

View File

@@ -7,14 +7,14 @@ edition = "2018"
[dependencies]
js-sys = "0.3.47"
cgmath = "*"
jpeg-decoder = "0.3.0"
png = "0.17.6"
#jpeg-decoder = "0.3.0"
#png = "0.17.6"
fitsrs = "0.3.4"
al-api = { path = "../al-api" }
serde = { version = "^1.0.59", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.4"
wasm-streams = "0.3.0"
# wasm-streams = "0.3.0"
futures = "0.3.25"
colorgrad = "0.6.2"
wasm-bindgen = "0.2.92"

View File

@@ -1,8 +1,8 @@
extern crate futures;
extern crate jpeg_decoder as jpeg;
extern crate png;
//extern crate jpeg_decoder as jpeg;
//extern crate png;
extern crate serde_json;
extern crate wasm_streams;
//extern crate wasm_streams;
pub mod convert;
pub mod image;

View File

@@ -44,12 +44,13 @@ impl TextureFormat for RGB8U {
const PIXEL_TYPE: PixelType = PixelType::RGB8U;
fn decode(raw_bytes: &[u8]) -> Result<Bytes<'_>, &'static str> {
let mut decoder = jpeg::Decoder::new(raw_bytes);
todo!()
/*let mut decoder = jpeg::Decoder::new(raw_bytes);
let bytes = decoder
.decode()
.map_err(|_| "Cannot decoder jpeg. This image may not be compressed.")?;
Ok(Bytes::Owned(bytes))
Ok(Bytes::Owned(bytes))*/
}
type ArrayBufferView = js_sys::Uint8Array;
@@ -73,12 +74,14 @@ impl TextureFormat for RGBA8U {
const PIXEL_TYPE: PixelType = PixelType::RGBA8U;
fn decode(raw_bytes: &[u8]) -> Result<Bytes<'_>, &'static str> {
let mut decoder = jpeg::Decoder::new(raw_bytes);
/*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))
*/
todo!()
}
type ArrayBufferView = js_sys::Uint8Array;

View File

@@ -49,7 +49,7 @@ fn read_shader<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<String> {
let shader_src = std::io::BufReader::new(file)
.lines()
.map_while(Result::ok)
.map(|l| {
.filter_map(|l| {
if l.starts_with("#include") {
let incl_file_names: Vec<_> = l.split_terminator(&[';', ' '][..]).collect();
let incl_file_name_rel = incl_file_names[1];
@@ -57,9 +57,12 @@ fn read_shader<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<String> {
println!("{}", incl_file_name.to_string_lossy());
read_shader(incl_file_name.to_str().unwrap()).unwrap()
Some(read_shader(incl_file_name.to_str().unwrap()).unwrap())
} else if l.trim_start().starts_with("//") {
// comment
None
} else {
l
Some(l)
}
})
.collect::<Vec<_>>()

View File

@@ -35,6 +35,7 @@ use fitsrs::WCS;
use moclib::qty::{Frequency, MocQty};
use std::hint::unreachable_unchecked;
use std::io::Cursor;
use std::time::Duration;
use wasm_bindgen::prelude::*;
@@ -490,6 +491,10 @@ impl App {
}
pub(crate) fn update(&mut self, dt: DeltaTime) -> Result<bool, JsValue> {
// a timer stopping the frame if it takes too long
// useful for garanting a framerate
let rendering_timer = Time::now();
if let Some(inertia) = self.inertia.as_mut() {
inertia.apply(&mut self.camera, &self.projection, dt);
// Always request for new tiles while moving
@@ -565,10 +570,25 @@ impl App {
let mut tile_copied = false;
const MAX_FRAME_TIME: DeltaTime = DeltaTime::from_millis(1000.0/25.0);
for rsc in rscs_received {
if Time::now() - rendering_timer >= MAX_FRAME_TIME {
self.downloader
.borrow_mut()
.delay(rsc);
continue;
}
match rsc {
RequestType::Tile(tile) => {
//if !_has_camera_zoomed {
if self.camera.has_moved() {
self.downloader
.borrow_mut()
.delay(RequestType::Tile(tile));
continue;
}
if let Some(hips) = self.layers.get_mut_hips_from_cdid(&tile.hips_cdid) {
let cfg = hips.get_config();

18
src/core/src/event.rs Normal file
View File

@@ -0,0 +1,18 @@
use wasm_bindgen::prelude::*;
use web_sys::{window, CustomEvent, CustomEventInit};
pub(crate) fn send_custom_event(name: &str, value: JsValue) {
// Create event details (optional)
let mut event_init = CustomEventInit::new();
event_init.detail(&value);
// Create the event
let event = CustomEvent::new_with_event_init_dict(name, &event_init)
.expect("Failed to create custom event");
// Dispatch the event on the window or any target element
window()
.expect("no global `window` exists")
.dispatch_event(&event)
.expect("failed to dispatch event");
}

View File

@@ -103,6 +103,7 @@ mod shaders;
mod coosys;
mod downloader;
mod event;
mod fifo_cache;
mod healpix;
mod inertia;

View File

@@ -130,7 +130,7 @@ impl Cursor {
let freq = cfg.em_min.unwrap_abort();
let location = LonLatT::new(0.0.to_angle(), 0.0.to_angle());
let f_max_order = cfg.max_depth_freq.unwrap_abort();
let f_max_order = cfg.max_depth_freq.unwrap_or(Frequency::<u64>::MAX_DEPTH);
let s_max_order = cfg.max_depth_tile;
let cell = HEALPixFreqCell::from_lonlat(location, freq, 0, 0);
@@ -485,7 +485,10 @@ impl HiPS3D {
.collect::<Vec<_>>()
.into_boxed_slice();
al_core::log(&format!("{:?}", spectra));
//al_core::log(&format!("{:?}", spectra));
let array = js_sys::Float32Array::from(&spectra[..]);
crate::event::send_custom_event("spectra", JsValue::from(array))
}
pub fn set_cursor_location(&mut self, lonlat: LonLatT<f64>, camera: &CameraViewPort) {
@@ -494,18 +497,23 @@ impl HiPS3D {
.get_tile_depth()
.min(cfg.get_max_depth_tile())
.max(cfg.get_min_depth_tile());
let dataproduct_type = cfg.dataproduct_type;
self.cursor.set_location(lonlat, s_order);
// update the spectra
self.compute_spectra_on_cursor();
if dataproduct_type == DataproductType::SpectralCube {
self.compute_spectra_on_cursor();
}
}
pub fn set_freq(&mut self, f: Freq) {
self.cursor.set_freq(f);
// update the spectra
self.compute_spectra_on_cursor();
if self.get_config().dataproduct_type == DataproductType::SpectralCube {
self.compute_spectra_on_cursor();
}
// Flag telling to recompute the mesh afterwards
self.move_freq = true;
@@ -565,14 +573,24 @@ impl HiPS3D {
.max()
.unwrap();
let cfg = self.get_config();
let dataproduct_type = cfg.dataproduct_type;
let max_depth_tile = cfg.max_depth_tile;
let min_depth_tile = cfg.get_min_depth_tile();
let em_min = cfg.em_min;
let em_max = cfg.em_max;
let cube_depth = cfg.get_cube_depth();
let max_depth_freq = cfg.max_depth_freq;
for cell in &self.hpx_cells_in_view {
// filter textures that are not in the moc
let cell = match self.get_config().dataproduct_type {
let cell = match dataproduct_type {
DataproductType::SpectralCube => {
// Determination of the f_order from the s_order
// From https://aladin.cds.unistra.fr/java/DocTechHiPS3D.pdf page 3
let f_max_order = self.get_config().max_depth_freq.unwrap_abort();
let s_max_order = self.get_config().max_depth_tile;
let f_max_order = max_depth_freq.unwrap_abort();
let s_max_order = max_depth_tile;
let s_order = cell.depth();
let f_order = f_max_order - (s_max_order - s_order);
@@ -599,19 +617,11 @@ impl HiPS3D {
}
}
DataproductType::Cube => {
/*al_core::log(&format!(
"{:?}, {:?} {:?} {:?}",
self.freq.0,
self.get_config().em_min,
self.get_config().em_max,
self.get_config().get_cube_depth(),
));*/
let channel_idx = (((self.get_freq().0
- self.get_config().em_min.unwrap_abort().0)
/ (self.get_config().em_max.unwrap_abort().0
- self.get_config().em_min.unwrap_abort().0))
* (self.get_config().get_cube_depth().unwrap_abort() as f64))
- em_min.unwrap_abort().0)
/ (em_max.unwrap_abort().0
- em_min.unwrap_abort().0))
* (cube_depth.unwrap_abort() as f64))
as u64;
let tile_depth = 32;
@@ -666,25 +676,29 @@ impl HiPS3D {
};
if let Some(texture) = hpx_cell_texture {
self.cells.push(texture.cell.clone());
let texture_cell = texture.cell.clone();
// The slice is sure to be contained so we can unwrap
let slice_position = match self.get_config().dataproduct_type {
let slice_position = match dataproduct_type {
DataproductType::SpectralCube => {
let f_hash_0 = texture.cell.f_hash
<< (Frequency::<u64>::MAX_DEPTH - texture.cell.f_depth);
let f_hash_1 = (texture.cell.f_hash + 1)
<< (Frequency::<u64>::MAX_DEPTH - texture.cell.f_depth);
// 1. hash of the frequency at max order
let f_hash = Frequency::<u64>::freq2hash(self.get_freq().0);
// b. compute the hash range
let delta_f_order = Frequency::<u64>::MAX_DEPTH - texture_cell.f_depth;
let f_order_hash_0 = texture_cell.f_hash;
let f_order_hash_1 = f_order_hash_0 + 1;
// 3. hash range at max order
let f_hash_0 = f_order_hash_0 << delta_f_order;
let f_hash_1 = f_order_hash_1 << delta_f_order;
(f_hash - f_hash_0) as f32 / (f_hash_1 - f_hash_0) as f32
}
DataproductType::Cube => {
let channel_idx = (((self.get_freq().0
- self.get_config().em_min.unwrap_abort().0)
/ (self.get_config().em_max.unwrap_abort().0
- self.get_config().em_min.unwrap_abort().0))
* (self.get_config().get_cube_depth().unwrap_abort() as f64))
- em_min.unwrap_abort().0)
/ (em_max.unwrap_abort().0
- em_min.unwrap_abort().0))
* (cube_depth.unwrap_abort() as f64))
as u64;
let tile_depth = 32;
@@ -693,7 +707,7 @@ impl HiPS3D {
_ => unreachable!(),
};
let uv_1 = TileUVW::new(&cell.hpx, &Some(texture.cell.hpx), slice_position);
let uv_1 = TileUVW::new(&cell.hpx, &Some(texture_cell.hpx), slice_position);
let d01e = uv_1[TileCorner::BottomRight].x - uv_1[TileCorner::BottomLeft].x;
let d02e = uv_1[TileCorner::TopLeft].y - uv_1[TileCorner::BottomLeft].y;
@@ -732,12 +746,12 @@ impl HiPS3D {
// GL TRIANGLES
self.idx_vertices.extend([
idx + off_indices,
idx + 1 + off_indices,
idx + 3 + off_indices,
idx + 2 + off_indices,
idx + 1 + off_indices,
idx + off_indices,
idx + 3 + off_indices,
idx + 2 + off_indices,
]);
// GL LINES
/*self.idx_vertices.extend([
@@ -764,6 +778,9 @@ impl HiPS3D {
// Replace options with an arbitrary vertex
let position_iter = pos.into_iter().flatten();
self.position.extend(position_iter);
self.cells.push(texture_cell);
}
}
}

View File

@@ -78,10 +78,11 @@ impl HpxFreqData {
let z = z - trim.2;
let data_raw_bytes = &raw_bytes[data_byte_offset.clone()];
let pixel_bytes_off = (x + y * naxis.0 + z * (naxis.0 * naxis.1)) as usize;
let bytes_per_pixel = bitpix.byte_size();
let pixel_bytes_off =
bytes_per_pixel * (x + y * naxis.0 + z * (naxis.0 * naxis.1)) as usize;
let p = &data_raw_bytes[pixel_bytes_off..(pixel_bytes_off + bytes_per_pixel)];
let pixel = match bitpix {
Bitpix::U8 => Pixel::U8(p[0]),
@@ -92,7 +93,7 @@ impl HpxFreqData {
_ => unreachable!(),
};
Some(pixel.to_f32() * *bscale + *bzero)
Some(pixel.to_f32() * (*bscale) + (*bzero))
}
}
HpxFreqData::Jpeg { data, size } => {
@@ -191,14 +192,7 @@ impl HpxFreqTex {
let start_time = None;
let texture = match pixel_format {
PixelType::RGBA8U => Texture3D::create_empty::<R8U>(
gl,
tile_size as i32,
tile_size as i32,
num_slices as i32,
TEX_PARAMS,
),
PixelType::RGB8U => Texture3D::create_empty::<R8U>(
PixelType::RGBA8U | PixelType::RGB8U | PixelType::R8U => Texture3D::create_empty::<R8U>(
gl,
tile_size as i32,
tile_size as i32,
@@ -212,13 +206,6 @@ impl HpxFreqTex {
num_slices as i32,
TEX_PARAMS,
),
PixelType::R8U => Texture3D::create_empty::<R8U>(
gl,
tile_size as i32,
tile_size as i32,
num_slices as i32,
TEX_PARAMS,
),
PixelType::R16I => Texture3D::create_empty::<R16I>(
gl,
tile_size as i32,
@@ -233,8 +220,6 @@ impl HpxFreqTex {
num_slices as i32,
TEX_PARAMS,
),
// No color cubes
_ => unreachable!(),
}?;
let data = None;

View File

@@ -53,15 +53,6 @@ float one_minus_z_neg(vec3 p) {
int ij2z(int i, int j) {
int i4 = i | (j << 2);
/*int j1 = (i1 ^ (i1 >> 8)) & 0x0000FF00;
int i2 = i1 ^ j1 ^ (j1 << 8);
int j2 = (i2 ^ (i2 >> 4)) & 0x00F000F0;
int i3 = i2 ^ j2 ^ (j2 << 4);
int j3 = (i3 ^ (i3 >> 2)) & 0x0C0C0C0C;
int i4 = i3 ^ j3 ^ (j3 << 2);*/
int j4 = (i4 ^ (i4 >> 1)) & 0x22222222;
int i5 = i4 ^ j4 ^ (j4 << 1);

View File

@@ -26,27 +26,27 @@ uniform int u_proj;
vec2 proj(vec3 p) {
if (u_proj == 0) {
/* TAN, Gnomonic projection */
// TAN, Gnomonic projection
return w2c_tan(p);
} else if (u_proj == 1) {
/* STG, Stereographic projection */
// STG, Stereographic projection
return w2c_stg(p);
} else if (u_proj == 2) {
/* SIN, Orthographic */
// SIN, Orthographic
return w2c_sin(p);
} else if (u_proj == 3) {
/* ZEA, Equal-area */
// ZEA, Equal-area
return w2c_zea(p);
} else if (u_proj == 4) {
// Pseudo-cylindrical projections
/* AIT, Aitoff */
// AIT, Aitoff
return w2c_ait(p);
} else if (u_proj == 5) {
// MOL, Mollweide */
// MOL, Mollweide
return w2c_mol(p);
} else {
// Cylindrical projections
// MER, Mercator */
// MER, Mercator
return w2c_mer(p);
}
}

View File

@@ -494,6 +494,63 @@ export let HiPS = (function () {
// dataproduct type
self.dataproductType = properties && properties.dataproduct_type;
if (self.dataproductType === "spectral-cube") {
if (!self.spectraUpdatedCallback) {
let createPlotCanvas = (id = "plot", width = 600, height = 300) => {
const canvas = document.createElement("canvas");
canvas.id = id;
canvas.width = width;
canvas.height = height;
canvas.style.pointerEvents = "none";
canvas.style.position = "absolute";
self.view.aladinDiv.appendChild(canvas); // or insert it into a specific container
return canvas;
};
const canvas = createPlotCanvas();
const ctx = canvas.getContext("2d");
function drawPlot(ctx, data) {
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const len = data.length;
// Clear previous drawing
ctx.clearRect(0, 0, width, height);
// Find min and max for scaling
const minY = Math.min(...data);
const maxY = Math.max(...data);
const scaleX = width / (len - 1);
const scaleY = (maxY - minY === 0) ? 1 : height / (maxY - minY);
ctx.beginPath();
for (let i = 0; i < len; i++) {
const x = i * scaleX;
const y = height - (data[i] - minY) * scaleY;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
}
self.spectraUpdatedCallback = (event) => {
const data = event.detail;
drawPlot(ctx, data);
};
} else {
window.removeEventListener("spectra", self.spectraUpdatedCallback);
}
window.addEventListener("spectra", self.spectraUpdatedCallback);
}
// Tile size
self.tileSize =

View File

@@ -71,7 +71,7 @@ export class ALEvent {
static SAMP_CONNECTED = new ALEvent("AL:samp.connected");
static SAMP_DISCONNECTED = new ALEvent("AL:samp.disconnected");
static CANVAS_EVENT = new ALEvent("AL:Event");
static CANVAS_EVENT = new ALEvent("AL:Event");
static RETICLE_CHANGED = new ALEvent("AL:Reticle.changed")