/**
# This file part of: VisiOmatic
* @file User Interface for catalog queries and catalog overlays.
* @requires util/VUtil.js
* @requires control/UI.js
* @requires catalog/catalogs.js
* @copyright (c) 2014-2024 CNRS/IAP/CFHT/SorbonneU/CEA/AIM/UParisSaclay
* @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
*/
import {
DomEvent,
DomUtil,
LayerGroup,
Util,
geoJson,
point
} from 'leaflet';
import {VUtil} from '../util';
import {UI} from './UI';
import {
gaiaDR3,
panstarrs1,
skybot,
sdss,
twomass,
unWISE
} from '../catalog/catalogs';
export const CatalogUI = UI.extend( /** @lends CatalogUI */ {
/**
Default array of catalogs.
* @type {Catalog[]}
* @default
*/
defaultCatalogs: [
gaiaDR3,
twomass,
sdss,
panstarrs1,
unWISE,
skybot
],
options: {
title: 'Catalog overlays',
color: '#FFFF00',
timeOut: 30, // seconds
authenticate: false, // Force authentication
collapsed: true,
position: 'topleft'
},
/**
* Create a VisiOmatic dialog for catalog queries and catalog overlays.
* @extends UI
* @memberof module:control/CatalogUI.js
* @constructs
* @param {Catalog[]} catalogs - Array of catalogs
* @param {object} [options] - Options.
* @param {string} [options.title='Catalog overlays']
Title of the dialog window or panel.
* @param {string} [options.color='#FFFF00']
Default catalog overlay color.
* @param {number} [options.timeOut=30]
Time out delay for catalog queries, in seconds.
* @param {boolean} [options.authenticate=false]
Force authentication for querying catalogs?
* @see {@link UI} for additional control options.
* @returns {CatalogUI} VisiOmatic CatalogUI instance.
*/
initialize: function (catalogs, options) {
Util.setOptions(this, options);
this._className = 'visiomatic-control';
this._id = 'visiomatic-catalog';
this._layers = {};
this._handlingClick = false;
this._sideClass = 'catalog';
this._catalogs = catalogs ? catalogs : this.defaultCatalogs;
},
/**
* Initialize the catalog query dialog.
* @private
*/
_initDialog: function () {
const className = this._className,
catalogs = this._catalogs,
box = this._addDialogBox(),
line = this._addDialogLine('', box),
elem = this._addDialogElement(line),
colpick = this._addColorPicker(
className + '-color',
elem,
'catalog',
this.options.color,
'visiomaticCatalog',
title='Click to set catalog color'
);
const catselect = this._addSelectMenu(
this._className + '-select',
elem,
catalogs.map(
function (catalog) {
return catalog.name;
}
),
undefined,
-1,
'Select Catalog',
function () {
let className = catalogs[catselect.selectedIndex - 1].className;
if (className === undefined) {
className = '';
}
DomUtil.setClass(
catselect,
this._className + '-select ' + className
);
return;
}
);
DomEvent.on(catselect, 'change keyup', function () {
const catalog = catalogs[catselect.selectedIndex - 1];
catselect.title = catalog.attribution + ' from ' + catalog.service;
}, this);
const elem2 = this._addDialogElement(line);
this._addButton(
className + '-button',
elem2,
'catalog',
'Query catalog',
function () {
// Ignore dummy 'Choose catalog' entry
const index = catselect.selectedIndex - 1;
if (index >= 0) {
const catalog = catalogs[index];
catalog.color = colpick.value;
catselect.selectedIndex = 0;
catselect.title = 'Select Catalog';
DomUtil.setClass(catselect, this._className + '-select ');
this._getCatalog(catalog, this.options.timeOut);
}
}
);
},
/**
* Reset the catalog query dialog.
* @private
*/
_resetDialog: function () {
// Do nothing: no need to reset with layer changes
},
/**
* Query catalog.
* @private
* @param {Catalog} catalog
Catalog.
* @param {number} [timeout]
Query time out delay, in seconds. Defaults to no time out.
*/
_getCatalog: async function (catalog, timeout) {
const _this = this,
map = this._map,
wcs = map.options.crs,
center = map.getCenter(),
b = map.getPixelBounds(),
z = map.getZoom(),
templayer = new LayerGroup(null);
// Add a temporary "dummy" layer to activate a spinner sign
templayer.notReady = true;
this.addLayer(templayer, catalog.name);
if (catalog.authenticate) {
this.options.authenticate = catalog.authenticate;
} else {
this.options.authenticate = false;
}
// Compute the search cone
const lngfac = Math.abs(Math.cos(center.lat * Math.PI / 180.0)),
c = [
map.unproject(b.min, z),
map.unproject(point(b.min.x, b.max.y), z),
map.unproject(b.max, z),
map.unproject(point(b.max.x, b.min.y), z)
];
var response;
// Mean Julian date during the exposure
const jdmean = 0.5 * (wcs.jd[0] + wcs.jd[1]),
observer = (wcs.obslatlng[0]==0. && wcs.obslatlng[1]==0.) ?
'500' :
wcs.obslatlng[0].toFixed(4) + ',' +
wcs.obslatlng[1].toFixed(4) + ',0';
if (catalog.regionType === 'box') {
// CDS box search
let dlng = (Math.max(wcs._deltaLng(c[0], center),
wcs._deltaLng(c[1], center),
wcs._deltaLng(c[2], center),
wcs._deltaLng(c[3], center)) -
Math.min(wcs._deltaLng(c[0], center),
wcs._deltaLng(c[1], center),
wcs._deltaLng(c[2], center),
wcs._deltaLng(c[3], center))) * lngfac,
dlat = Math.max(c[0].lat, c[1].lat, c[2].lat, c[3].lat) -
Math.min(c[0].lat, c[1].lat, c[2].lat, c[3].lat);
if (dlat < 0.0001) {
dlat = 0.0001;
}
if (dlng < 0.0001) {
dlng = 0.0001;
}
response = await fetch(
Util.template(catalog.url, Util.extend({
sys: 'J2000.0',
jd: jdmean,
observer: 568,
lng: center.lng.toFixed(6),
lat: center.lat.toFixed(6),
dlng: dlng.toFixed(4),
dlat: dlat.toFixed(4),
nmax: catalog.nmax + 1,
maglim: catalog.maglim
}))
);
} else {
// Regular cone search
const dr = Math.max(wcs.distance(c[0], center),
wcs.distance(c[0], center),
wcs.distance(c[0], center),
wcs.distance(c[0], center));
response = await fetch(
Util.template(catalog.url, Util.extend({
sys: 'J2000.0',
jd: jdmean,
observer: 568,
lng: center.lng.toFixed(6),
lat: center.lat.toFixed(6),
dr: dr.toFixed(4),
drm: (dr * 60.0).toFixed(4),
nmax: catalog.nmax + 1
})),
);
}
if (response.status == 200) {
this._loadCatalog(catalog, templayer, await response);
} else {
this.removeLayer(templayer);
alert('Error ' + response.status + ' while querying ' +
catalog.service + '.');
}
},
/**
* Load catalog data and display the overlay layer.
* @private
* @param {Catalog} catalog
Catalog.
* @param {leaflet.Layer} templayer
"Dummy" layer to activate a spinner sign.
* @param {object} response
Response object from the fetch() request.
*/
_loadCatalog: async function (catalog, templayer, response) {
const wcs = this._map.options.crs;
// Propagate Julian date interval
catalog.jd = wcs.jd;
const geo = catalog.toGeoJSON(
catalog.format == 'json' ?
await response.json() : await response.text()
),
geocatalog = geoJson(geo, {
onEachFeature: function (feature, layer) {
if (feature.properties && feature.properties.items) {
layer.bindPopup(catalog.popup(feature));
}
},
coordsToLatLng: function (coords) {
return new L.LatLng(coords[1],coords[0],coords[2]);
},
filter: function (feature) {
return catalog.filter(feature);
},
pointToLayer: function (feature, latlng) {
return catalog.draw(feature, latlng);
},
style: function (feature) {
return catalog.style(feature);
}
});
let excessflag = false;
geocatalog.nameColor = catalog.color;
geocatalog.addTo(this._map);
this.removeLayer(templayer);
if (geo.features.length > catalog.nmax) {
geo.features.pop();
excessflag = true;
}
this.addLayer(geocatalog, catalog.name +
' (' + geo.features.length.toString() +
(excessflag ? '+ entries)' : ' entries)'));
if (excessflag) {
alert(
'Selected area is too large: ' + catalog.name +
' sample has been truncated to the brightest ' +
catalog.nmax + ' sources.'
);
}
}
});
/**
* Instantiate a VisiOmatic dialog for catalog queries and catalog overlays.
* @function
* @param {Catalog[]} catalogs - Array of catalogs
* @param {object} [options] - Options: see {@link CatalogUI}
* @returns {CatalogUI} Instance of a VisiOmatic catalog interface.
*/
export const catalogUI = function (catalogs, options) {
return new CatalogUI(catalogs, options);
};
source