feat: add possibility of loading a local hips without the need of starting an http server

This commit is contained in:
Matthieu Baumann
2024-09-18 14:52:18 +02:00
parent bf726088ad
commit 504cab42bb
17 changed files with 343 additions and 99 deletions

View File

@@ -13,6 +13,7 @@
aladin = A.aladin(
'#aladin-lite-div',
{
showSimbadPointerControl: true,
survey: 'P/allWISE/color', // set initial image survey
projection: 'AIT', // set a projection
fov: 1.5, // initial field of view in degrees
@@ -30,20 +31,14 @@
}
);
aladin.on("positionChanged", ({ra, dec, dragging}) => {
console.log('position changed dragging: ', dragging)
});
let id;
/*let id;
aladin.on("zoomChanged", () => {
if (id)
clearTimeout(id);
id = setTimeout(() => {
console.log("wheel stopped, new cone search here")
}, 500);
})
//aladin.removeHiPSFromFavorites('CDS/P/allWISE/color')
})*/
});
</script>
<style>

View File

@@ -70,7 +70,7 @@ path = "./al-api"
[dependencies.web-sys]
version = "*"
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", "Response", "XmlHttpRequest", "XmlHttpRequestResponseType", "PerformanceTiming", "Performance", "Url", "ReadableStream",]
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", "Response", "XmlHttpRequest", "XmlHttpRequestResponseType", "PerformanceTiming", "Performance", "Url", "ReadableStream", "File", "FileList"]
[dev-dependencies.image-decoder]
package = "image"

View File

@@ -61,6 +61,8 @@ features = [
'PerformanceTiming',
'Performance',
'Url',
'File',
'FileList'
]
[profile.dev]

View File

@@ -73,6 +73,8 @@ features = [
'PerformanceTiming',
'Performance',
'Url',
'File',
'FileList'
]
[profile.dev]

View File

@@ -1,4 +1,5 @@
use crate::renderable::ImageLayer;
use crate::tile_fetcher::HiPSLocalFiles;
use crate::{
//async_task::{BuildCatalogIndex, ParseTableTask, TaskExecutor, TaskResult, TaskType},
camera::CameraViewPort,
@@ -302,16 +303,6 @@ impl App {
ancestors.insert(ancestor_tile_cell);
}
}
//let ancestor_next_tile_cell = next_tile_cell.ancestor(3);
//if !survey.contains_tile(&ancestor_tile_cell) {
//self.tile_fetcher.append(
// query::Tile::new(&ancestor_tile_cell, hips_url.clone(), format),
// &mut self.downloader,
//);
//}
//if ancestor_tile_cell != ancestor_next_tile_cell {
//}
}
}
// Request for ancestor
@@ -805,8 +796,12 @@ impl App {
// Check for async retrieval
if let Ok(img) = self.img_recv.try_recv() {
let params = img.get_params();
self.layers
.add_image(img, &mut self.camera, &self.projection)?;
self.layers.add_image(
img,
&mut self.camera,
&self.projection,
&mut self.tile_fetcher,
)?;
self.request_redraw = true;
// Send the ack to the js promise so that she finished
@@ -958,8 +953,12 @@ impl App {
}
pub(crate) fn remove_layer(&mut self, layer: &str) -> Result<(), JsValue> {
self.layers
.remove_layer(layer, &mut self.camera, &self.projection)?;
self.layers.remove_layer(
layer,
&mut self.camera,
&self.projection,
&mut self.tile_fetcher,
)?;
self.request_redraw = true;
@@ -982,17 +981,31 @@ impl App {
Ok(())
}
pub(crate) fn add_image_hips(&mut self, hips_cfg: HiPSCfg) -> Result<(), JsValue> {
let hips =
self.layers
.add_image_hips(&self.gl, hips_cfg, &mut self.camera, &self.projection)?;
pub(crate) fn add_image_hips(
&mut self,
hips_cfg: HiPSCfg,
local_files: Option<HiPSLocalFiles>,
) -> Result<(), JsValue> {
let cdid = hips_cfg.properties.get_creator_did().to_string();
let hips = self.layers.add_image_hips(
&self.gl,
hips_cfg,
&mut self.camera,
&self.projection,
&mut self.tile_fetcher,
)?;
if let Some(local_files) = local_files {
self.tile_fetcher.insert_hips_local_files(cdid, local_files);
}
self.tile_fetcher
.launch_starting_hips_requests(hips, self.downloader.clone());
// Once its added, request the tiles in the view (unless the viewer is at depth 0)
self.request_for_new_tiles = true;
self.request_redraw = true;
//self.grid.update(&self.camera, &self.projection);
Ok(())
}

View File

@@ -87,6 +87,7 @@ use math::projection::*;
use moclib::moc::RangeMOCIntoIterator;
//use votable::votable::VOTableWrapper;
use crate::tile_fetcher::HiPSLocalFiles;
use wasm_bindgen::prelude::*;
use web_sys::HtmlElement;
@@ -160,11 +161,7 @@ impl WebClient {
/// * `shaders` - The list of shader objects containing the GLSL code source
/// * `resources` - Image resource files
#[wasm_bindgen(constructor)]
pub fn new(
aladin_div: &HtmlElement,
//_shaders: JsValue,
resources: JsValue,
) -> Result<WebClient, JsValue> {
pub fn new(aladin_div: &HtmlElement, resources: JsValue) -> Result<WebClient, JsValue> {
#[cfg(feature = "dbg")]
panic::set_hook(Box::new(console_error_panic_hook::hook));
@@ -371,10 +368,14 @@ impl WebClient {
/// of WebGL2 texture units on some architectures, the total number of surveys rendered is
/// limited to 4.
#[wasm_bindgen(js_name = addHiPS)]
pub fn add_image_hips(&mut self, hips: JsValue) -> Result<(), JsValue> {
pub fn add_image_hips(
&mut self,
hips: JsValue,
files: Option<HiPSLocalFiles>,
) -> Result<(), JsValue> {
// Deserialize the survey objects that compose the survey
let hips = serde_wasm_bindgen::from_value(hips)?;
self.app.add_image_hips(hips)?;
self.app.add_image_hips(hips, files)?;
Ok(())
}

View File

@@ -10,6 +10,7 @@ pub mod text;
pub mod utils;
use crate::renderable::image::Image;
use crate::tile_fetcher::TileFetcherQueue;
use al_core::image::format::ChannelType;
@@ -316,6 +317,7 @@ impl Layers {
layer: &str,
camera: &mut CameraViewPort,
proj: &ProjectionType,
tile_fetcher: &mut TileFetcherQueue,
) -> Result<usize, JsValue> {
let err_layer_not_found = JsValue::from_str(&format!(
"Layer {:?} not found, so cannot be removed.",
@@ -351,6 +353,9 @@ impl Layers {
// remove the frame
camera.unregister_view_frame(hips_frame, proj);
// remove the local files access from the tile fetcher
tile_fetcher.delete_hips_local_files(s.get_config().get_creator_did());
Ok(id_layer)
} else if let Some(_) = self.images.remove(&id) {
// A FITS image has been found and removed
@@ -418,6 +423,7 @@ impl Layers {
hips: HiPSCfg,
camera: &mut CameraViewPort,
proj: &ProjectionType,
tile_fetcher: &mut TileFetcherQueue,
) -> Result<&HiPS, JsValue> {
let HiPSCfg {
layer,
@@ -431,7 +437,7 @@ impl Layers {
let layer_already_found = self.layers.iter().any(|l| l == &layer);
let idx = if layer_already_found {
let idx = self.remove_layer(&layer, camera, proj)?;
let idx = self.remove_layer(&layer, camera, proj, tile_fetcher)?;
idx
} else {
self.layers.len()
@@ -492,6 +498,7 @@ impl Layers {
image: ImageLayer,
camera: &mut CameraViewPort,
proj: &ProjectionType,
tile_fetcher: &mut TileFetcherQueue,
) -> Result<&[Image], JsValue> {
let ImageLayer {
layer,
@@ -504,7 +511,7 @@ impl Layers {
let layer_already_found = self.layers.iter().any(|s| s == &layer);
let idx = if layer_already_found {
let idx = self.remove_layer(&layer, camera, proj)?;
let idx = self.remove_layer(&layer, camera, proj, tile_fetcher)?;
idx
} else {
self.layers.len()

View File

@@ -27,6 +27,7 @@ impl<const N: usize> BitVector<N> {
}
}
#[cfg(test)]
mod tests {
use super::BitVector;

View File

@@ -4,7 +4,7 @@ use crate::time::{DeltaTime, Time};
use crate::Abort;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
const MAX_NUM_TILE_FETCHING: usize = 8;
@@ -16,25 +16,68 @@ pub struct TileFetcherQueue {
base_tile_queries: Vec<query::Tile>,
tiles_fetched_time: Time,
num_tiles_fetched: usize,
hips_local_files: HashMap<CreatorDid, HiPSLocalFiles>,
}
#[derive(Debug)]
#[wasm_bindgen]
pub struct HiPSLocalFiles {
paths: Box<[HashMap<u64, web_sys::File>]>,
}
use crate::tile_fetcher::query::Tile;
use crate::HEALPixCell;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
#[wasm_bindgen]
impl HiPSLocalFiles {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
let paths = vec![HashMap::new(); 30].into_boxed_slice();
Self { paths }
}
pub fn insert(&mut self, depth: u8, ipix: u64, file: web_sys::File) {
self.paths[depth as usize].insert(ipix, file);
}
fn get(&self, cell: &HEALPixCell) -> Option<&web_sys::File> {
let d = cell.depth() as usize;
let i = cell.idx();
return self.paths[d].get(&i);
}
}
use crate::renderable::CreatorDid;
impl TileFetcherQueue {
pub fn new() -> Self {
let queries = VecDeque::new();
let base_tile_queries = Vec::new();
let tiles_fetched_time = Time::now();
let num_tiles_fetched = 0;
Self {
queries,
base_tile_queries,
tiles_fetched_time,
num_tiles_fetched,
hips_local_files: HashMap::new(),
}
}
pub fn insert_hips_local_files(&mut self, id: CreatorDid, local_files: HiPSLocalFiles) {
self.hips_local_files.insert(id, local_files);
}
pub fn delete_hips_local_files(&mut self, id: &str) {
self.hips_local_files.remove(id);
}
pub fn clear(&mut self) {
self.queries.clear();
//self.query_set.clear();
}
pub fn append(&mut self, query: query::Tile) {
@@ -72,23 +115,41 @@ impl TileFetcherQueue {
self.num_tiles_fetched
}
fn check_in_file_list(&self, mut query: Tile) -> Result<Tile, JsValue> {
if let Some(files) = self.hips_local_files.get(&query.hips_cdid) {
if let Some(file) = files.get(&query.cell) {
if let Ok(url) = web_sys::Url::create_object_url_with_blob(file.as_ref()) {
// rewrite the url
query.url = url;
Ok(query)
} else {
Err(JsValue::from_str("could not create an url from the tile"))
}
} else {
Ok(query)
}
} else {
Ok(query)
}
}
fn fetch(&mut self, downloader: Rc<RefCell<Downloader>>) {
// Fetch the base tiles with higher priority
while let Some(query) = self.base_tile_queries.pop() {
//if downloader.fetch(query) {
// The fetch has succeded
//self.num_tiles_fetched += 1;
//}
downloader.borrow_mut().fetch(query);
if let Ok(query) = self.check_in_file_list(query) {
downloader.borrow_mut().fetch(query);
}
}
let mut num_fetched_tile = 0;
while num_fetched_tile < MAX_NUM_TILE_FETCHING && !self.queries.is_empty() {
let query = self.queries.pop_back().unwrap_abort();
if downloader.borrow_mut().fetch(query) {
// The fetch has succeded
num_fetched_tile += 1;
if let Ok(query) = self.check_in_file_list(query) {
if downloader.borrow_mut().fetch(query) {
// The fetch has succeded
num_fetched_tile += 1;
}
}
}
@@ -124,20 +185,23 @@ impl TileFetcherQueue {
let hips_fmt = cfg.get_format();
let min_order = cfg.get_min_depth_texture();
let dl = downloader.clone();
crate::utils::set_timeout(
move || {
for tile_cell in crate::healpix::cell::ALLSKY_HPX_CELLS_D0 {
dl.borrow_mut().fetch(query::Tile::new(
tile_cell,
hips_cdid.clone(),
hips_url.clone(),
hips_fmt,
));
}
},
2_000,
);
for tile_cell in crate::healpix::cell::ALLSKY_HPX_CELLS_D0 {
if let Ok(query) = self.check_in_file_list(query::Tile::new(
tile_cell,
hips_cdid.clone(),
hips_url.clone(),
hips_fmt,
)) {
let dl = downloader.clone();
crate::utils::set_timeout(
move || {
dl.borrow_mut().fetch(query);
},
2_000,
);
}
}
}
}
}

View File

@@ -113,17 +113,7 @@ A.aladin = function (divSelector, options) {
* @deprecated
* Old method name, use {@link A.HiPS} instead.
*/
A.imageHiPS = function (id, options) {
let url = id;
return Aladin.createImageSurvey(
id,
options && options.name,
url,
options && options.cooFrame,
options && options.maxOrder,
options
);
}
A.imageHiPS = A.HiPS;
/**
* Creates a HiPS image object
@@ -136,7 +126,17 @@ A.imageHiPS = function (id, options) {
* @param {HiPSOptions} [options] - Options describing the survey
* @returns {HiPS} - A HiPS image object
*/
A.HiPS = A.imageHiPS;
A.HiPS = function (id, options) {
let url = id;
return Aladin.createImageSurvey(
id,
options && options.name,
url,
options && options.cooFrame,
options && options.maxOrder,
options
);
}
/**
* Creates a celestial source object with the given coordinates.
@@ -818,6 +818,7 @@ A.catalogFromSKAORucio = function (target, radiusDegrees, options, successCallba
*
* @example
* const cat = A.catalogFromVizieR('I/311/hip2', 'M 45', 5, {onClick: 'showTable'});
* const cat2 = A.catalogFromVizieR('I/311/hip2', '12 +9', 5, {onClick: 'showTable'});
*/
A.catalogFromVizieR = function (vizCatId, target, radius, options, successCallback, errorCallback) {
options = options || {};

View File

@@ -1471,10 +1471,10 @@ export let Aladin = (function () {
* @memberof Aladin
*/
Aladin.prototype.removeLayers = Aladin.prototype.removeOverlays;
/**
* @typedef {MOC|Catalog|ProgressiveCat|GraphicOverlay} Overlay
* @description Possible overlays
*/
/**
* @typedef {MOC|Catalog|ProgressiveCat|GraphicOverlay} Overlay
* @description Possible overlays
*/
/**
* Remove an overlay by its layer name
*
@@ -1667,7 +1667,8 @@ export let Aladin = (function () {
*/
Aladin.prototype.newImageSurvey = function (id, options) {
// a wrapper on createImageSurvey that aggregates all params in an options object
return this.createImageSurvey(id,
return this.createImageSurvey(
id,
options && options.name,
id,
options && options.cooFrame,

View File

@@ -47,7 +47,7 @@ import { Footprint } from "./Footprint.js";
* @property {string} [name="catalog"] - The name of the catalog.
* @property {string} [color] - The color associated with the catalog.
* @property {number} [sourceSize=8] - The size of the sources in the catalog.
* @property {string|function|Image|HTMLCanvasElement} [shape="square"] - The shape of the sources (can be, "square", "circle", "plus", "cross", "rhomb", "triangle").
* @property {string|Function|Image|HTMLCanvasElement|HTMLImageElement} [shape="square"] - The shape of the sources (can be, "square", "circle", "plus", "cross", "rhomb", "triangle").
* @property {number} [limit] - The maximum number of sources to display.
* @property {string|Function} [onClick] - Whether the source data appears as a table row or a in popup. Can be 'showTable' string, 'showPopup' string or a custom user defined function that handles the click.
* @property {boolean} [readOnly=false] - Whether the catalog is read-only.
@@ -479,7 +479,17 @@ export let Catalog = (function () {
);
};
// API
/**
* Set the shape of the sources
*
* @memberof Catalog
*
* @param {Object} [options] - shape options
* @param {string} [options.color] - the color of the shape
* @param {number} [options.sourceSize] - size of the shape
* @param {string|Function|HTMLImageCanvas|HTMLImageElement} [options.shape="square"] - the type of the shape. Can be square, rhomb, plus, cross, triangle, circle.
* A callback function can also be called that return an HTMLImageElement in function of the source object. A canvas or an image can also be given.
*/
Catalog.prototype.updateShape = function (options) {
options = options || {};
this.color = options.color || this.color || Color.getNextColor();
@@ -524,7 +534,13 @@ export let Catalog = (function () {
this.reportChange();
};
// API
/**
* Add sources to the catalog
*
* @memberof Catalog
*
* @param {Source[]} sources - An array of sources or only one source to add
*/
Catalog.prototype.addSources = function (sources) {
// make sure we have an array and not an individual source
sources = [].concat(sources);

View File

@@ -28,7 +28,7 @@
import { ALEvent } from "./events/ALEvent.js";
import { ColorCfg } from "./ColorCfg.js";
import { HiPSProperties } from "./HiPSProperties.js";
import { Aladin } from "./Aladin.js";
let PropertyParser = {};
// Utilitary functions for parsing the properties and giving default values
/// Mandatory tileSize property
@@ -161,10 +161,11 @@ export let HiPS = (function () {
* @constructs HiPS
*
* @param {string} id - Mandatory unique identifier for the layer. Can be an arbitrary name
* @param {string} location - Can be:
* @param {string|FileList|Object} location - Can be:
* - an http url <br/>
* - a relative path to your HiPS <br/>
* - a special ID pointing towards a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}
* - a dict storing a local HiPS. This object contains a tile file: hips[order][ipix] = <tile File> and refers to the properties file like so: hips["properties"] = <properties File>. A javascript FileList pointing to the opened webkit directory is also accepted.
* @param {HiPSOptions} [options] - The option for the survey
*
* @description Giving a CDS ID will do a query to the MOCServer first to retrieve metadata. Then it will also check for the presence of faster HiPS nodes to choose a faster url to query to tiles from.
@@ -178,6 +179,30 @@ export let HiPS = (function () {
this.name = (options && options.name) || undefined;
this.startUrl = options.startUrl;
if (location instanceof FileList) {
let files = {};
for (var file of location) {
let path = file.webkitRelativePath;
if (path.includes("Norder") && path.includes("Npix")) {
const order = +path.substring(path.indexOf("Norder") + 6).split("/")[0];
if (!files[order]) {
files[order] = {}
}
const ipix = +path.substring(path.indexOf("Npix") + 4).split(".")[0];
files[order][ipix] = file;
}
if (path.includes("properties")) {
files['properties'] = file;
}
}
this.files = files;
} else if (location instanceof Object) {
this.files = location;
}
this.url = location;
this.maxOrder = options.maxOrder;
this.minOrder = options.minOrder || 0;
@@ -402,11 +427,30 @@ export let HiPS = (function () {
}
this.view = view;
let isIncompleteOptions = true;
// This is very dirty but it allows me to differentiate the location from
// whether an ID or a plain url
let isID = this.url.includes("P/") || this.url.includes("C/")
if (this.files) {
// Fetch the properties file
self.query = (async () => {
// look for the properties file
await HiPSProperties.fetchFromFile(self.files["properties"])
.then((p) => {
self._parseProperties(p);
self.url = "local";
delete self.files["properties"]
})
return self;
})();
return;
}
let isIncompleteOptions = true;
// This is very dirty but it allows me to differentiate the location from whether it is an ID or a plain url
let isID = this.url.includes("P/") || this.url.includes("C/")
if (this.imgFormat === "fits") {
// a fits is given
isIncompleteOptions = !(
@@ -428,7 +472,6 @@ export let HiPS = (function () {
}
self.query = (async () => {
if (isIncompleteOptions) {
// ID typed url
if (self.startUrl && isID) {
@@ -459,8 +502,6 @@ export let HiPS = (function () {
},
1000
);
} catch (e) {
throw e;
}
@@ -799,7 +840,7 @@ export let HiPS = (function () {
this.layer = layer;
let self = this;
this.view.wasm.addHiPS({
const config = {
layer,
properties: {
creatorDid: self.creatorDid,
@@ -821,8 +862,24 @@ export let HiPS = (function () {
...this.colorCfg.get(),
longitudeReversed: this.longitudeReversed,
imgFormat: this.imgFormat,
},
});
}
};
let localFiles;
if (this.files) {
localFiles = new Aladin.wasmLibs.core.HiPSLocalFiles();
for (var order in this.files) {
for (var ipix in this.files[order]) {
const file = this.files[order][ipix];
localFiles.insert(+order, BigInt(+ipix), file)
}
}
}
this.view.wasm.addHiPS(
config,
localFiles
);
return Promise.resolve(this)
.then((hips) => {

View File

@@ -70,6 +70,8 @@ HiPSProperties.fetchFromID = async function(ID) {
}
HiPSProperties.fetchFromUrl = async function(urlOrId) {
let url;
try {
urlOrId = new URL(urlOrId);
} catch (e) {
@@ -85,7 +87,7 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) {
// Fetch the properties of the survey
const HiPSServiceUrl = urlOrId.toString();
let url = HiPSServiceUrl;
url = HiPSServiceUrl;
// Use the url for retrieving the HiPS properties
// remove final slash
if (url.slice(-1) === '/') {
@@ -98,6 +100,7 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) {
// fix for HTTPS support --> will work for all HiPS served by CDS
url = Utils.fixURLForHTTPS(url)
let init = {};
if (Utils.requestCORSIfNotSameOrigin(url)) {
init = { mode: 'cors' };
@@ -121,7 +124,9 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) {
if (!metadata.hips_frame || !metadata.hips_order) {
reject('Bad properties: do not contain the mandatory frame or order info')
} else {
metadata.hips_service_url = HiPSServiceUrl;
if (!("hips_service_url" in metadata)) {
metadata.hips_service_url = HiPSServiceUrl;
}
resolve(metadata);
}
} else {
@@ -133,6 +138,27 @@ HiPSProperties.fetchFromUrl = async function(urlOrId) {
return result;
}
HiPSProperties.fetchFromFile = function(file) {
let url = URL.createObjectURL(file);
return fetch(url)
.then((response) => response.text())
.then(
(response) => new Promise((resolve, reject) => {
// We get the property here
let metadata = HiPSDefinition.parseHiPSProperties(response);
URL.revokeObjectURL(url)
// 1. Ensure there is exactly one survey matching
if (metadata && Object.keys(metadata).length > 0) {
resolve(metadata)
} else {
reject('No surveys matching at this url: ' + rootURL);
}
})
);
}
HiPSProperties.getFasterMirrorUrl = function (metadata) {
const pingHiPSServiceUrl = async (baseUrl) => {
baseUrl = Utils.fixURLForHTTPS(baseUrl);

View File

@@ -2088,12 +2088,12 @@ export let View = (function () {
};
View.prototype.removeOverlayByName = function (overlayName) {
let layer = this.allOverlayLayers.find(l => l.name === overlayName);
if (!layer) {
let overlay = this.allOverlayLayers.find(l => l.name === overlayName);
if (!overlay) {
console.error(`Layer with name "${overlayName}" not found.`);
return;
}
this.removeOverlay(layer);
this.removeOverlay(overlay);
};
View.prototype.add = function(overlay) {

View File

@@ -580,7 +580,8 @@ export class OverlayStackBox extends Box {
// Center the view around the new fits object
self.aladin.gotoRaDec(ra, dec);
self.aladin.setFoV(fov * 1.1);
//self.aladin.selectLayer(image.layer);
URL.revokeObjectURL(url);
}
);
@@ -590,6 +591,31 @@ export class OverlayStackBox extends Box {
);
},
}),
ContextMenu.webkitDir({
label: "Load local HiPS",
action(files) {
let id = files[0].webkitRelativePath.split("/")[0];
let name = id;
let hips = self.aladin.createImageSurvey(
id,
name,
files,
null,
null,
{
errorCallback: (e) => {
aladin.addStatusBarMessage({
duration: 2000,
type: 'info',
message: 'Could not add the local HiPS',
})
}
}
)
self.aladin.addNewImageLayer(hips);
},
}),
],
tooltip: {
content: "Add a HiPS or an FITS image",

View File

@@ -407,4 +407,36 @@ export class ContextMenu extends DOMElement {
}
}
}
static webkitDir(itemOptions) {
return {
...itemOptions,
label: {
icon: {
monochrome: true,
tooltip: {content: 'Load a local file from your computer.<br \>Accept ' + itemOptions.accept + ' files'},
url: uploadIconUrl,
cssStyle: {
cursor: 'help',
}
},
content: itemOptions.label
},
action(e) {
let webkitDirLoader = document.createElement('input');
webkitDirLoader.type = 'file';
webkitDirLoader.webkitdirectory = true;
webkitDirLoader.multiple = true;
// Case: The user is loading a FITS file
webkitDirLoader.addEventListener("change", (e) => {
if (itemOptions.action) {
itemOptions.action(e.target.files)
}
});
webkitDirLoader.click();
}
}
}
}