Files
aladin-lite/src/js/ProgressiveCat.js
2024-06-10 23:42:53 +10:00

666 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2013 - UDS/CNRS
// The Aladin Lite program is distributed under the terms
// of the GNU General Public License version 3.
//
// This file is part of Aladin Lite.
//
// Aladin Lite is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Aladin Lite is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// The GNU General Public License is available in COPYING file
// along with Aladin Lite.
//
/******************************************************************************
* Aladin Lite project
*
* File ProgressiveCat.js
*
* Author: Thomas Boch[CDS]
*
*****************************************************************************/
import { Catalog } from "./Catalog.js";
import { Source } from "./Source.js";
import { Color } from "./Color.js";
import { Coo } from "./libs/astro/coo.js";
import { Utils } from "./Utils";
import { CooFrameEnum } from "./CooFrameEnum.js";
// TODO: index sources according to their HEALPix ipix
// TODO : merge parsing with class Catalog
export let ProgressiveCat = (function() {
// TODO : test if CORS support. If no, need to pass through a proxy
// currently, we suppose CORS is supported
// constructor
let ProgressiveCat = function(rootUrl, frameStr, maxOrder, options) {
options = options || {};
this.uuid = Utils.uuidv4();
this.type = 'progressivecat';
this.rootUrl = rootUrl; // TODO: method to sanitize rootURL (absolute, no duplicate slashes, remove end slash if existing)
// fast fix for HTTPS support --> will work for all HiPS served by CDS
if (Utils.isHttpsContext() && ( /u-strasbg.fr/i.test(this.rootUrl) || /unistra.fr/i.test(this.rootUrl) ) ) {
this.rootUrl = this.rootUrl.replace('http://', 'https://');
}
this.frameStr = frameStr;
this.frame = CooFrameEnum.fromString(frameStr) || CooFrameEnum.J2000;
this.maxOrder = parseInt(maxOrder);
this.isShowing = true; // TODO : inherit from catalogue
this.name = options.name || "progressive-cat";
this.color = options.color || Color.getNextColor();
this.shape = options.shape || "square";
this.sourceSize = options.sourceSize || 6;
this.selectSize = this.sourceSize + 2;
this.selectionColor = options.selectionColor || '#00ff00'; // TODO: to be merged with Catalog
this.hoverColor = options.hoverColor || this.color;
// allows for filtering of sources
this.filterFn = options.filter || undefined; // TODO: do the same for catalog
this.onClick = options.onClick || undefined; // TODO: inherit from catalog
// we cache the list of sources in each healpix tile. Key of the cache is norder+'-'+npix
this.sourcesCache = new Utils.LRUCache(256);
this.footprintsCache = new Utils.LRUCache(256);
//added to allow hips catalogue to also use shape functions
this.updateShape(options);
this.maxOrderAllsky = 2;
this.isReady = false;
this.tilesInView = [];
};
// TODO: to be put higher in the class diagram, in a HiPS generic class
ProgressiveCat.readProperties = function(rootUrl, successCallback, errorCallback) {
if (! successCallback) {
return;
}
var propertiesURL = rootUrl + '/properties';
Utils.fetch({
url: propertiesURL,
method: 'GET',
dataType: 'text',
success: function(propertiesTxt) {
var props = {};
var lines = propertiesTxt.split('\n');
for (var k=0; k<lines.length; k++) {
var line = lines[k];
var idx = line.indexOf('=');
var propName = line.substring(0, idx).trim();
var propValue = line.substring(idx + 1).trim();
props[propName] = propValue;
}
successCallback(props);
},
error: function(err) { // TODO : which parameters should we put in the error callback
errorCallback && errorCallback(err);
}
});
};
function getFields(instance, xml) {
var attributes = ["name", "ID", "ucd", "utype", "unit", "datatype", "arraysize", "width", "precision"];
var fields = [];
var k = 0;
instance.keyRa = instance.keyDec = null;
xml.querySelectorAll("FIELD").forEach((field) => {
var f = {};
for (var i=0; i<attributes.length; i++) {
var attribute = attributes[i];
if (field.hasAttribute(attribute)) {
f[attribute] = field.getAttribute(attribute);
}
}
if ( ! f.ID) {
f.ID = "col_" + k;
}
if (!instance.keyRa && f.ucd && (f.ucd.indexOf('pos.eq.ra')==0 || f.ucd.indexOf('POS_EQ_RA')==0)) {
if (f.name) {
instance.keyRa = f.name;
}
else {
instance.keyRa = f.ID;
}
}
if (!instance.keyDec && f.ucd && (f.ucd.indexOf('pos.eq.dec')==0 || f.ucd.indexOf('POS_EQ_DEC')==0)) {
if (f.name) {
instance.keyDec = f.name;
}
else {
instance.keyDec = f.ID;
}
}
fields.push(f);
k++;
});
return fields;
}
function getSources(instance, csv, fields) {
// TODO : find ra and dec key names (see in Catalog)
if (!instance.keyRa || ! instance.keyDec) {
return [];
}
var lines = csv.split('\n');
var mesureKeys = [];
for (var k=0; k<fields.length; k++) {
if (fields[k].name) {
mesureKeys.push(fields[k].name);
}
else {
mesureKeys.push(fields[k].ID);
}
}
var sources = [];
var coo = new Coo();
var newSource;
// start at i=1, as first line repeat the fields names
for (var i=2; i<lines.length; i++) {
var mesures = {};
var data = lines[i].split('\t');
if (data.length<mesureKeys.length) {
continue;
}
for (var j=0; j<mesureKeys.length; j++) {
mesures[mesureKeys[j]] = data[j];
}
var ra, dec;
if (Utils.isNumber(mesures[instance.keyRa]) && Utils.isNumber(mesures[instance.keyDec])) {
ra = parseFloat(mesures[instance.keyRa]);
dec = parseFloat(mesures[instance.keyDec]);
}
else {
coo.parse(mesures[instance.keyRa] + " " + mesures[instance.keyDec]);
ra = coo.lon;
dec = coo.lat;
}
newSource = new Source(ra, dec, mesures);
sources.push(newSource);
newSource.setCatalog(instance);
}
let footprints = instance.computeFootprints(sources);
return [sources, footprints];
};
ProgressiveCat.prototype = {
init: function(view) {
var self = this;
this.view = view;
if (this.maxOrder && this.frameStr) {
this._loadMetadata();
}
else {
ProgressiveCat.readProperties(self.rootUrl,
function (properties) {
self.properties = properties;
self.maxOrder = parseInt(self.properties['hips_order']);
self.frame = CooFrameEnum.fromString(self.properties['hips_frame']);
self._loadMetadata();
}, function(err) {
console.log('Could not find properties for HiPS ' + self.rootUrl);
}
);
}
},
updateShape: Catalog.prototype.updateShape,
_loadMetadata: function() {
var self = this;
let request = new Request(self.rootUrl + '/' + 'Metadata.xml', {
method: 'GET'
})
fetch(request)
.then((resp) => resp.text())
.then((text) => {
let xml = ProgressiveCat.parser.parseFromString(text, "text/xml")
self.fields = getFields(self, xml);
self._loadAllskyNewMethod();
})
.catch(err => self._loadAllskyOldMethod());
},
_loadAllskyNewMethod: function() {
var self = this;
Utils.fetch({
desc: "Loading allsky tiles of: " + self.name,
url: self.rootUrl + '/' + 'Norder1/Allsky.tsv',
method: 'GET',
success: function(tsv) {
let [sources, footprints] = getSources(self, tsv, self.fields);
self.order1Footprints = footprints;
self.order1Sources = sources;
if (self.order2Sources) {
self.isReady = true;
self._finishInitWhenReady();
}
},
error: function(err) {
console.log('Something went wrong: ' + err);
}
});
Utils.fetch({
desc: "Loading allsky order 2 tiles of: " + self.name,
url: self.rootUrl + '/' + 'Norder2/Allsky.tsv',
method: 'GET',
success: function(tsv) {
let [sources, footprints] = getSources(self, tsv, self.fields);
self.order2Footprints = footprints;
self.order2Sources = sources;
if (self.order1Sources) {
self.isReady = true;
self._finishInitWhenReady();
}
},
error: function(err) {
console.log('Something went wrong: ' + err);
}
});
},
_loadAllskyOldMethod: function() {
this.maxOrderAllsky = 3;
this._loadLevel2Sources();
this._loadLevel3Sources();
},
_loadLevel2Sources: function() {
var self = this;
Utils.fetch({
desc: "Loading level 2 sources of: " + self.name,
url: self.rootUrl + '/' + 'Norder2/Allsky.xml',
method: 'GET',
success: function(text) {
let xml = ProgressiveCat.parser.parseFromString(text, "text/xml")
self.fields = getFields(self, xml);
let [sources, footprints] = getSources(self, xml.querySelectorAll('CSV').innerText, self.fields);
self.order2Footprints = footprints
self.order2Sources = sources
if (self.order3Sources) {
self.isReady = true;
self._finishInitWhenReady();
}
},
error: function(err) {
console.log('Something went wrong: ' + err);
}
});
},
_loadLevel3Sources: function() {
var self = this;
Utils.fetch({
desc: "Loading level 3 sources of: " + self.name,
url: self.rootUrl + '/' + 'Norder3/Allsky.xml',
method: 'GET',
success: function(text) {
let xml = ProgressiveCat.parser.parseFromString(text, "text/xml")
let [sources, footprints] = getSources(self, xml.querySelectorAll('CSV').innerText, self.fields);
self.order3Footprints = footprints
self.order3Sources = sources
if (self.order2Sources) {
self.isReady = true;
self._finishInitWhenReady();
}
},
error: function(err) {
console.log('Something went wrong: ' + err);
}
});
},
_finishInitWhenReady: function() {
this.view.requestRedraw();
this.loadNeededTiles();
},
draw: function(ctx, width, height) {
if (! this.isShowing || ! this.isReady) {
return;
}
if (this._shapeIsFunction) {
ctx.save();
}
// Order must be >= 0
if (this.order1Footprints) {
this.order1Footprints.forEach((f) => {
f.draw(ctx, this.view);
f.source.tooSmallFootprint = f.isTooSmall();
});
}
if (this.order1Sources) {
this.drawSources(this.order1Sources, ctx, width, height);
}
if (this.view.realNorder >= 1) {
if (this.order2Footprints) {
this.order2Footprints.forEach((f) => {
f.draw(ctx, this.view)
f.source.tooSmallFootprint = f.isTooSmall();
});
}
if (this.order2Sources) {
this.drawSources(this.order2Sources, ctx, width, height);
}
}
// For old allsky, tilesInView refers to tiles at orders 4..
// For new allsky, tilesInView will contains order3 sources
if (this.maxOrderAllsky === 3) {
if (this.view.realNorder >= 2) {
if (this.order3Footprints) {
this.order3Footprints.forEach((f) => {
f.draw(ctx, this.view)
f.source.tooSmallFootprint = f.isTooSmall();
});
}
if (this.order3Sources) {
this.drawSources(this.order3Sources, ctx, width, height);
}
}
}
let key, sources, footprints;
this.tilesInView.forEach((tile) => {
key = tile[0] + '-' + tile[1];
sources = this.sourcesCache.get(key);
footprints = this.footprintsCache.get(key);
if (footprints) {
footprints.forEach((f) => {
f.draw(ctx, this.view)
f.source.tooSmallFootprint = f.isTooSmall();
});
}
if (sources) {
this.drawSources(sources, ctx, width, height);
}
});
if (this._shapeIsFunction) {
ctx.restore();
}
},
drawSources: function(sources, ctx, width, height) {
let ra = []
let dec = [];
sources.forEach((s) => {
ra.push(s.ra);
dec.push(s.dec);
});
let xy = this.view.wasm.worldToScreenVec(ra, dec);
let self = this;
sources.forEach(function(s, idx) {
if (xy[2*idx] && xy[2*idx + 1]) {
if (!self.filterFn || self.filterFn(s)) {
s.x = xy[2*idx];
s.y = xy[2*idx + 1];
self.drawSource(s, ctx, width, height)
}
}
});
},
drawSource: Catalog.prototype.drawSource,
getSources: function() {
var ret = [];
if (this.order1Sources) {
ret = ret.concat(this.order1Sources);
}
if (this.order2Sources) {
ret = ret.concat(this.order2Sources);
}
if (this.order3Sources) {
ret = ret.concat(this.order3Sources);
}
if (this.tilesInView) {
var sources, key, t;
for (var k=0; k<this.tilesInView.length; k++) {
t = this.tilesInView[k];
key = t[0] + '-' + t[1];
sources = this.sourcesCache.get(key);
if (sources) {
ret = ret.concat(sources);
}
}
}
return ret;
},
getFootprints: function() {
var ret = [];
if (this.order1Footprints) {
ret = ret.concat(this.order1Footprints);
}
if (this.order2Footprints) {
ret = ret.concat(this.order2Footprints);
}
if (this.order3Footprints) {
ret = ret.concat(this.order3Footprints);
}
if (this.tilesInView) {
var footprints, key, t;
for (var k=0; k < this.tilesInView.length; k++) {
t = this.tilesInView[k];
key = t[0] + '-' + t[1];
footprints = this.footprintsCache.get(key);
if (footprints) {
ret = ret.concat(footprints);
}
}
}
return ret;
},
deselectAll: function() {
if (this.order1Sources) {
for (var k=0; k<this.order1Sources.length; k++) {
this.order1Sources[k].deselect();
}
}
if (this.order2Sources) {
for (var k=0; k<this.order2Sources.length; k++) {
this.order2Sources[k].deselect();
}
}
if (this.order3Sources) {
for (var k=0; k<this.order3Sources.length; k++) {
this.order3Sources[k].deselect();
}
}
var keys = this.sourcesCache.keys();
for (key in keys) {
if ( ! this.sourcesCache[key]) {
continue;
}
var sources = this.sourcesCache[key];
for (var k=0; k<sources.length; k++) {
sources[k].deselect();
}
var footprints = this.footprintsCache[key];
for (var k=0; k<footprints.length; k++) {
footprints[k].deselect();
}
}
},
show: function() {
if (this.isShowing) {
return;
}
this.isShowing = true;
this.loadNeededTiles();
this.reportChange();
},
hide: function() {
if (! this.isShowing) {
return;
}
this.isShowing = false;
this.reportChange();
},
reportChange: function() {
this.view.requestRedraw();
},
getTileURL: function(norder, npix) {
var dirIdx = Math.floor(npix/10000)*10000;
return this.rootUrl + "/" + "Norder" + norder + "/Dir" + dirIdx + "/Npix" + npix + ".tsv";
},
loadNeededTiles: function() {
if ( ! this.isShowing) {
return;
}
this.tilesInView = [];
var norder = this.view.realNorder;
if (norder > this.maxOrder) {
norder = this.maxOrder;
}
var cells = this.view.getVisibleCells(norder);
// Limit the number of cells to fetch by looking for smaller orders
let customNorder = norder;
while (cells.length > 12 && customNorder > this.maxOrderAllsky) {
customNorder--;
cells = this.view.getVisibleCells(customNorder);
}
norder = customNorder;
if (norder<=this.maxOrderAllsky) {
return; // nothing to do, hurrayh !
}
var ipixList, ipix;
for (var curOrder=this.maxOrderAllsky+1; curOrder<=norder; curOrder++) {
ipixList = [];
for (var k=0; k<cells.length; k++) {
ipix = Math.floor(cells[k].ipix / Math.pow(4, norder - curOrder));
if (ipixList.indexOf(ipix)<0) {
ipixList.push(ipix);
}
}
// load needed tiles
for (var i=0; i<ipixList.length; i++) {
this.tilesInView.push([curOrder, ipixList[i]]);
}
}
var t, key;
for (var k=0; k<this.tilesInView.length; k++) {
t = this.tilesInView[k];
key = t[0] + '-' + t[1]; // t[0] is norder, t[1] is ipix
if (!this.sourcesCache.get(key)) {
(function(self, norder, ipix) { // wrapping function is needed to be able to retrieve norder and ipix in ajax success function
var key = norder + '-' + ipix;
Utils.fetch({
/*
url: Aladin.JSONP_PROXY,
data: {"url": self.getTileURL(norder, ipix)},
*/
// ATTENTION : je passe en JSON direct, car je n'arrive pas a choper les 404 en JSONP
url: self.getTileURL(norder, ipix),
desc: "Get tile .tsv " + norder + ' ' + ipix + ' of ' + self.name,
method: 'GET',
//dataType: 'jsonp',
success: function(tsv) {
let [sources, footprints] = getSources(self, tsv, self.fields);
self.sourcesCache.set(key, sources);
self.footprintsCache.set(key, footprints);
self.view.requestRedraw();
},
error: function() {
// on suppose qu'il s'agit d'une erreur 404
self.sourcesCache.set(key, []);
self.footprintsCache.set(key, []);
}
});
})(this, t[0], t[1]);
}
}
},
computeFootprints: Catalog.prototype.computeFootprints,
reportChange: function() { // TODO: to be shared with Catalog
this.view && this.view.requestRedraw();
}
}; // END OF .prototype functions
ProgressiveCat.parser = new DOMParser();
return ProgressiveCat;
})();