diff --git a/examples/al-hips-3D.html b/examples/al-hips-3D.html index 3aef41ed..dc7788c3 100644 --- a/examples/al-hips-3D.html +++ b/examples/al-hips-3D.html @@ -28,9 +28,9 @@ } ); - hips = aladin.newImageSurvey("https://alasky.cds.unistra.fr/GALFAHI/GALFAHI-Narrow-DR2-3D", { + hips = aladin.newImageSurvey("http://alasky.cds.unistra.fr/HIPS3D/MUSE-test", { successCallback: (hips) => { - hips.setFrequency({value: 0.21101690259115785, unit: "m"}) // GALFA + //hips.setFrequency({value: 6.374279333565797E-7, unit: "m"}) // GALFA } }); // compressed https://alasky.cds.unistra.fr/test-compression-cubes/DHIGLS/ @@ -40,7 +40,7 @@ aladin.setImageLayer(hips) //hips.setFrequency({value: emMin + delta * i, unit: "m"}) - //hips.setFrequency({value: 0.21059156059066345, unit: "m"}) // MUSE + //hips.setFrequency({value: 6.374279333565797E-7, unit: "m"}) // MUSE //hips.setFrequency({value: 0.21101690259115785, unit: "m"}) // DGHILG /*let id; diff --git a/src/core/src/healpix/cell.rs b/src/core/src/healpix/cell.rs index f218afeb..86dd01b2 100644 --- a/src/core/src/healpix/cell.rs +++ b/src/core/src/healpix/cell.rs @@ -556,6 +556,16 @@ impl HEALPixFreqCell { f0..f1 } + + pub fn pixel_frequencies(&self, num_pixels: usize) -> impl Iterator { + let delta_depth = num_pixels.trailing_zeros(); + let pixel_depth = self.f_depth + delta_depth as u8; + + let h0 = self.f_hash << delta_depth; + let h1 = (self.f_hash + 1) << delta_depth; + + (h0..h1).map(move |hash| Freq::from_hash_with_order(hash, pixel_depth).0 as f32) + } } // Utils diff --git a/src/core/src/renderable/hips/d3/mod.rs b/src/core/src/renderable/hips/d3/mod.rs index 01d6bf9c..19668843 100644 --- a/src/core/src/renderable/hips/d3/mod.rs +++ b/src/core/src/renderable/hips/d3/mod.rs @@ -226,16 +226,24 @@ impl Cursor { let f_hash_val = self.freq.hash(pixel_depth); //let f_hash_val_0 = (f_hash_val as i64 - NUM_VALUES as i64).max(0) as u64; - let f_hash_val_1 = (f_hash_val as i64 + dx) + let h1 = (f_hash_val as i64 + dx) .min( (Frequency::::n_cells_max() >> (Frequency::::MAX_DEPTH - pixel_depth)) as i64, ) .max(0) as u64; - let f_dx_th_slice = Freq::from_hash_with_order(f_hash_val_1, pixel_depth); + let h0 = (f_hash_val as i64 - dx) + .min( + (Frequency::::n_cells_max() >> (Frequency::::MAX_DEPTH - pixel_depth)) + as i64, + ) + .max(0) as u64; - f_dx_th_slice - self.freq + let f1 = Freq::from_hash_with_order(h1, pixel_depth); + let f0 = Freq::from_hash_with_order(h0, pixel_depth); + + Freq((f1 - f0).0 * 0.5) } fn get_surrounding_cell_hashes_along_spectra_axis(&self) -> Range { @@ -578,13 +586,6 @@ impl HiPS3D { ) .unwrap_abort(); - Reflect::set( - &spectra_js_obj, - &JsValue::from_str("fOrder"), - &JsValue::from_f64(pixel_depth as f64), - ) - .unwrap_abort(); - let mut start = window_pixel_hash.start.max(domain_pixel_hash.start); let mut end = window_pixel_hash.end.min(domain_pixel_hash.end); @@ -614,10 +615,12 @@ impl HiPS3D { .unwrap_abort(); } + let mut freqs = vec![]; let spectra = self .cursor .get_surrounding_cells_along_spectra_axis() .flat_map(|c| { + freqs.extend(c.pixel_frequencies(tile_depth as usize)); if let Some(cubic_tex) = self.buffer.get(&c) { (0..(tile_depth as u32)) .map(|z| cubic_tex.read_pixel(x, y, z).unwrap_or(f32::NAN)) @@ -644,6 +647,15 @@ impl HiPS3D { ) .unwrap_abort(); + //al_core::log(&format!("{:?}", freqs)); + + Reflect::set( + &spectra_js_obj, + &JsValue::from_str("freqs"), + &js_sys::Float32Array::from(&freqs[..]), + ) + .unwrap_abort(); + crate::event::send_custom_event("spectra", JsValue::from(spectra_js_obj)); } diff --git a/src/core/src/renderable/hips/d3/texture.rs b/src/core/src/renderable/hips/d3/texture.rs index d6f41e25..817d2cb4 100644 --- a/src/core/src/renderable/hips/d3/texture.rs +++ b/src/core/src/renderable/hips/d3/texture.rs @@ -1,5 +1,6 @@ use crate::time::Time; +use crate::renderable::hips::d3::Freq; use crate::Abort; use crate::WebGlContext; use al_core::image::fits::FitsImage; @@ -304,6 +305,18 @@ impl HpxFreqTex { } } + pub fn frequencies(&self) -> Vec { + let delta_depth = self.num_slices.trailing_zeros(); + let pixel_depth = self.cell.f_depth + delta_depth as u8; + + let h0 = self.cell.f_hash << delta_depth; + let h1 = (self.cell.f_hash + 1) << delta_depth; + + (h0..h1) + .map(|hash| Freq::from_hash_with_order(hash, pixel_depth).0 as f32) + .collect() + } + pub fn set_data_from_jpeg( &mut self, // the tile image of the whole cubic tile diff --git a/src/js/HiPS.js b/src/js/HiPS.js index efb42950..35f0b9bd 100644 --- a/src/js/HiPS.js +++ b/src/js/HiPS.js @@ -675,6 +675,10 @@ export let HiPS = (function () { this.setOptions({additive}); }; + HiPS.prototype.isSpectralCube = function() { + return this.hipsTileDepth !== undefined && this.hipsTileDepth !== null; + } + /** * Sets the colormap when rendering the HiPS. * diff --git a/src/js/SpectraDisplayer.js b/src/js/SpectraDisplayer.js index 01f22230..1154fa42 100644 --- a/src/js/SpectraDisplayer.js +++ b/src/js/SpectraDisplayer.js @@ -22,6 +22,8 @@ import { Input } from "./gui/Widgets/Input"; import HomeIconUrl from '../../assets/icons/maximize.svg'; import SpectraIconUrl from '../../assets/icons/freq.svg'; import { ALEvent } from "./events/ALEvent"; +import { Utils } from "./Utils"; +import { Aladin } from "./Aladin"; /****************************************************************************** * Aladin Lite project @@ -185,20 +187,6 @@ export class SpectraDisplayer { }, }) - this.selector = new Input({ - label: "HiPS3D selector:", - name: "layer selector", - type: 'select', - classList: ['aladin-spectra-hips-selector'], - options: [], - change: (e) => { - let name = e.target.value; - let hips = self.hips3DList.get(name); - - self.attachHiPS3D(hips) - }, - }) - let autoCenterBtn = new ActionButton({ size: 'small', icon: { @@ -249,11 +237,6 @@ export class SpectraDisplayer { divNode.appendChild(autoCenterBtn.element()) divNode.appendChild(extractionBtn.element()) - let divHiPSSelector = document.createElement("div") - divHiPSSelector.innerHTML = 'Survey:'; - divHiPSSelector.appendChild(this.selector.element()) - divNode.appendChild(divHiPSSelector) - this.divNode = divNode; this.view.aladin.aladinDiv.appendChild(divNode); @@ -264,7 +247,7 @@ export class SpectraDisplayer { defineEventListeners() { let lastMouse = { x: 0, y: 0 }; - let isDragging = false; + this.isDragging = false; let canvas = this.canvas; let ctxCursor = this.ctxCursor; @@ -285,7 +268,7 @@ export class SpectraDisplayer { v = this.height - (v - this.minY) * this.scaleY if (my >= v) { - isDragging = true; + this.isDragging = true; lastMouse = { x: mx, y: my }; canvas.style.cursor = 'grabbing'; } else { @@ -294,11 +277,11 @@ export class SpectraDisplayer { this.ctx.beginPath(); this.ctx.moveTo(this.scaleX * len / 2, this.height); this.ctx.lineTo(this.scaleX * len / 2, this.height - (this.maxY - this.minY) * this.scaleY); - this.ctx.strokeStyle = "red"; + this.ctx.strokeStyle = Aladin.DEFAULT_OPTIONS.reticleColor; this.ctx.lineWidth = 10; if (this.ctx.isPointInStroke(mx, my)) { - isDragging = true; + this.isDragging = true; lastMouse = { x: mx, y: my }; canvas.style.cursor = 'grabbing'; } else { @@ -374,7 +357,7 @@ export class SpectraDisplayer { this._redrawLabels() - if (!isDragging) { + if (!this.isDragging) { // Draw the vertical line that can be grabed to move the slice this.ctx.beginPath(); this.ctx.moveTo(this.scaleX * len / 2, this.height); @@ -395,26 +378,32 @@ export class SpectraDisplayer { // is dragged let dx = (mx - lastMouse.x) / this.scaleX; if (dx != 0) { - // set the frequency - let curFreq = self.hips.getFrequency(); + // Set the frequency - // let curHash = Number(self.view.wasm.freq2hash(self.hips.layer, curFreq)); - //let nextHash = curHash - Math.round(dx) - //let nextFreq = self.view.wasm.hash2freq(self.hips.layer, BigInt(nextHash)); - let nextFreq = curFreq - Math.ceil(dx) * self.data.freqStep; + // look where we are in the freq range + let j = Utils.binarySearch(self.data.freqs, self.data.freq); + let df, f; + if (j > 0 && j < self.data.freqs.length - 1) { + df = (self.data.freqs[j + 1] - self.data.freqs[j - 1]) * 0.5; + f = self.data.freq - dx * df; + } else if (j == 0) { + df = self.data.freqs[1] - self.data.freqs[0] + f = self.data.freqs[0] - dx * df; + } else { + df = self.data.freqs[self.data.freqs.length - 1] - self.data.freqs[self.data.freqs.length - 2]; + f = self.data.freqs[self.data.freqs.length - 1] - dx * df; + } self.hips.setFrequency({ - value: nextFreq, + value: f, unit: 'Hz' }) - const correctedMx = (Math.ceil(dx) * this.scaleX) + lastMouse.x; - //const correctedMx = mx; - lastMouse = { x: correctedMx, y: my }; + lastMouse = { x: mx, y: my }; } }); canvas.addEventListener('mouseup', (e) => { - isDragging = false; + this.isDragging = false; canvas.style.cursor = 'default'; const clickEvent = new MouseEvent('click', { @@ -427,7 +416,7 @@ export class SpectraDisplayer { }); canvas.addEventListener('mouseout', (e) => { - isDragging = false; + this.isDragging = false; }); canvas.addEventListener('wheel', (e) => { @@ -450,6 +439,7 @@ export class SpectraDisplayer { this.view.catalogCanvas.dispatchEvent(wheelEvent); }); + /* const updateSelectorList = () => { let options = []; for (const hipsName of this.hips3DList.keys()) { @@ -505,7 +495,7 @@ export class SpectraDisplayer { updateSelectorList() } - ); + );*/ } hide() { @@ -540,7 +530,6 @@ export class SpectraDisplayer { let data = event.detail; if (data.layer === this.hips.layer) { this.data = data; - console.log(data) this._redraw(this.ctx); } }; @@ -550,7 +539,7 @@ export class SpectraDisplayer { this.resetScale(); this.show() - this.selector.update({value: hips.name, title: hips.name}) + //this.selector.update({value: hips.name, title: hips.name}) } } @@ -578,8 +567,6 @@ export class SpectraDisplayer { // Find min and max for scaling let valuesWithNoNans = values.filter(v=>Number.isFinite(v)); - const fOrder = this.data.fOrder; - if (Number.isFinite(this.minY)) { this.minY = Math.min(...valuesWithNoNans, this.minY) } else { @@ -600,7 +587,7 @@ export class SpectraDisplayer { this.ctx.beginPath(); this.ctx.moveTo(this.scaleX * len / 2, this.height); this.ctx.lineTo(this.scaleX * len / 2, this.height - (this.maxY - this.minY) * this.scaleY); - this.ctx.strokeStyle = "red"; + this.ctx.strokeStyle = Aladin.DEFAULT_OPTIONS.reticleColor; this.ctx.lineWidth = 2; this.ctx.stroke(); @@ -644,6 +631,15 @@ export class SpectraDisplayer { // Clear previous drawing this.ctxLabels.clearRect(0, 0, this.width, this.height); + let drawLabel = (ctx, str, x, y, strokeStyle, font, fillStyle) => { + ctx.strokeStyle = strokeStyle; // contour color + ctx.strokeText(str, x, y); + + ctx.fillStyle = fillStyle; + ctx.font = font + ctx.fillText(str, x, y); + }; + // Draw the min and max frequencies this.ctxLabels.font = "20px monospace"; // You can also use "Courier New", "Consolas", etc. this.ctxLabels.fillStyle = "lightgreen"; @@ -651,22 +647,47 @@ export class SpectraDisplayer { // min window freq this.ctxLabels.textAlign = "left"; // Horizontally centered - this.ctxLabels.fillText(spectraValue2String(this.data.freqMin, this.data.freqStep), 0, this.height - 20); + drawLabel( + this.ctxLabels, + spectraValue2String(this.data.freqMin, this.data.freqs[1] - this.data.freqs[0]), + 0, + this.height - 20, + 'black', + '20px monospace', + 'lightgreen' + ) // max window freq this.ctxLabels.textAlign = "right"; // Horizontally centered - this.ctxLabels.fillText(spectraValue2String(this.data.freqMax, this.data.freqStep), this.width, this.height - 20); + drawLabel( + this.ctxLabels, + spectraValue2String(this.data.freqMax, this.data.freqs[this.data.freqs.length - 1] - this.data.freqs[this.data.freqs.length - 2]), + this.width, + this.height - 20, + 'black', + '20px monospace', + 'lightgreen' + ) // current window freq this.ctxLabels.textAlign = "center"; // Horizontally centered - let fStr; - if (this.mouseFreq) { - this.ctxLabels.fillStyle = "yellow"; - fStr = spectraValue2String(this.mouseFreq, this.data.freqStep); + let str, fillStyle; + if (!this.isDragging && this.mouseFreq) { + fillStyle = "yellow"; + str = spectraValue2String(this.mouseFreq, this.data.freqStep); } else { - fStr = spectraValue2String(this.data.freq, this.data.freqStep); + fillStyle = Aladin.DEFAULT_OPTIONS.reticleColor; + str = spectraValue2String(this.data.freq, this.data.freqStep); } - this.ctxLabels.fillText(fStr, this.width / 2, this.height - 20); + drawLabel( + this.ctxLabels, + str, + this.width / 2, + this.height - 20, + 'black', + '20px monospace', + fillStyle + ) } _redrawSpectra(array) { @@ -676,16 +697,15 @@ export class SpectraDisplayer { let strokeStyle = "red"; this.ctx.strokeStyle = strokeStyle - let fOrder = this.data.fOrder; - let prevY; let i = 0; let i1 = array.length; - while (i <= i1) { + + while (i < i1) { let y; const x = i * this.scaleX; - const inValidDomain = this.data.freqIdxStart !== undefined && this.data.freqIdxEnd !== undefined && i > this.data.freqIdxStart && i < this.data.freqIdxEnd; + const inValidDomain = this.data.freqIdxStart !== undefined && this.data.freqIdxEnd !== undefined && i >= this.data.freqIdxStart && i <= this.data.freqIdxEnd; if (inValidDomain) { const tileNotReceived = !Number.isFinite(array[i]); @@ -720,6 +740,9 @@ export class SpectraDisplayer { } y = this.height - (array[i] - this.minY) * this.scaleY; + + + if (i === 0) { this.ctx.moveTo(x, y); } else { diff --git a/src/js/Utils.ts b/src/js/Utils.ts index 98a1bf02..df3d671c 100644 --- a/src/js/Utils.ts +++ b/src/js/Utils.ts @@ -149,12 +149,18 @@ Utils.inverseNewtonRaphson = function(y: number, f: Function, fPrime: Function, Utils.binarySearch = function(array, value) { var low = 0, high = array.length; - + var mid; while (low < high) { - var mid = (low + high) >>> 1; - if (array[mid] > value) low = mid + 1; - else high = mid; + mid = Math.floor((low + high) / 2); + if (array[mid] === value) { + return mid; + } else if (array[mid] < value) { + low = mid + 1; + } else { + high = mid; + } } + return low; } diff --git a/src/js/View.js b/src/js/View.js index f076ee4b..8055ca14 100644 --- a/src/js/View.js +++ b/src/js/View.js @@ -1300,7 +1300,7 @@ export let View = (function () { if (!view.throttledTouchPadZoom) { view.throttledTouchPadZoom = () => { - const factor = Utils.detectTrackPad(e) ? 1.06 : 1.2; + const factor = Utils.detectTrackPad(e) ? 1.05 : 1.2; const currZoomFactor = view.zoom.isZooming ? view.zoom.finalZoom : view.zoomFactor; let newZoomFactor = view.delta > 0 ? currZoomFactor * factor : currZoomFactor / factor; diff --git a/src/js/gui/Box/HiPSSettingsBox.js b/src/js/gui/Box/HiPSSettingsBox.js index 6a701aad..5bb971eb 100644 --- a/src/js/gui/Box/HiPSSettingsBox.js +++ b/src/js/gui/Box/HiPSSettingsBox.js @@ -35,6 +35,7 @@ import { Form } from "../Widgets/Form.js"; import colorIconUrl from '../../../../assets/icons/color.svg'; import pixelHistIconUrl from '../../../../assets/icons/pixel_histogram.svg'; import { RadioButton } from "../Widgets/Radio.js"; + import waveOnIconUrl from '../../../../assets/icons/wave-on.svg'; import { Layout } from "../Layout.js"; @@ -42,58 +43,62 @@ import { Form } from "../Widgets/Form.js"; // Constructor constructor(aladin, options) { let self; - let selector = new RadioButton({ - luminosity: { - icon: { - size: 'small', - monochrome: true, - url: luminosityIconUrl + + let radioOptions = () => { + return { + luminosity: { + icon: { + size: 'small', + monochrome: true, + url: luminosityIconUrl + }, + tooltip: {content: 'Contrast', position: {direction: 'bottom'}}, + action: (e) => { + const content = Layout.vertical({ + layout: [self.selector, self.luminositySettingsContent] + }); + self.update({content}) + } }, - tooltip: {content: 'Contrast', position: {direction: 'bottom'}}, - action: (e) => { - const content = Layout.vertical({ - layout: [self.selector, self.luminositySettingsContent] - }); - self.update({content}) - } - }, - opacity: { - icon: { - size: 'small', - monochrome: true, - url: opacityIconUrl + opacity: { + icon: { + size: 'small', + monochrome: true, + url: opacityIconUrl + }, + tooltip: {content: 'Opacity', position: {direction: 'bottom'}}, + action: (e) => { + const content = Layout.vertical({layout: [self.selector, self.opacitySettingsContent]}); + self.update({content}) + } }, - tooltip: {content: 'Opacity', position: {direction: 'bottom'}}, - action: (e) => { - const content = Layout.vertical({layout: [self.selector, self.opacitySettingsContent]}); - self.update({content}) - } - }, - colors: { - icon: { - size: 'small', - url: colorIconUrl + colors: { + icon: { + size: 'small', + url: colorIconUrl + }, + tooltip: {content: 'Colormap', position: {direction: 'bottom'}}, + action: (e) => { + const content = Layout.vertical({layout: [self.selector, self.colorSettingsContent]}); + self.update({content}) + } }, - tooltip: {content: 'Colormap', position: {direction: 'bottom'}}, - action: (e) => { - const content = Layout.vertical({layout: [self.selector, self.colorSettingsContent]}); - self.update({content}) - } - }, - pixel: { - icon: { - size: 'small', - monochrome: true, - url: pixelHistIconUrl + pixel: { + icon: { + size: 'small', + monochrome: true, + url: pixelHistIconUrl + }, + tooltip: {content: 'Cutouts', position: {direction: 'bottom'}}, + action: (e) => { + const content = Layout.vertical({layout: [self.selector, self.pixelSettingsContent]}); + self.update({content}) + } }, - tooltip: {content: 'Cutouts', position: {direction: 'bottom'}}, - action: (e) => { - const content = Layout.vertical({layout: [self.selector, self.pixelSettingsContent]}); - self.update({content}) - } - }, - selected: 'opacity' - }, aladin); + selected: 'opacity' + } + }; + let selector = new RadioButton(radioOptions(), aladin); // Define the contents @@ -283,6 +288,8 @@ import { Form } from "../Widgets/Form.js"; aladin.aladinDiv) self = this; + this.radioOptions = radioOptions; + this.aladin = aladin; this._addListeners() @@ -328,6 +335,34 @@ import { Form } from "../Widgets/Form.js"; update(options) { if (options.layer) { + let self = this; + if (options.layer.isSpectralCube()) { + self.selector = new RadioButton({ + ...this.radioOptions(), + spectra: { + icon: { + size: 'small', + monochrome: true, + url: waveOnIconUrl + }, + tooltip: {content: 'Spectra', position: {direction: 'bottom'}}, + action: (e) => { + let spectraDisplayer = self.aladin.view.spectraDisplayer; + if (spectraDisplayer.isHidden) { + spectraDisplayer.attachHiPS3D(options.layer) + spectraDisplayer.show() + } else { + spectraDisplayer.hide() + } + } + } + }, self.aladin); + + console.log(self) + + self.update({content: Layout.vertical([self.selector, self.opacitySettingsContent])}) + } + this._update(options.layer) } @@ -335,9 +370,12 @@ import { Form } from "../Widgets/Form.js"; } _addListeners() { + let self = this; + ALEvent.HIPS_LAYER_CHANGED.listenedBy(this.aladin.aladinDiv, (e) => { const hips = e.detail.layer; let selectedLayer = this.options.layer; + if (selectedLayer && hips.layer === selectedLayer.layer) { this._update(hips) }