import { Constants } from "./constants.js";
import { DecDeg2HMS, DecDeg2DMS, redshift2Velocity } from "./utils.js";
import { EventFactory } from "./customevents.js";
import { DOMAccessor } from "./domelements.js";
/**
* Removes everything from the DOM element containing the results
*/
function cleanContainer(domElement) {
if(domElement !== null){
while (domElement.firstChild) {
domElement.firstChild.remove()
}
}
}
/**
*
* All NED response fields :
*
* Associations, DEC, Diameter Points, Magnitude and Filter, No.
* Notes, Object Name, Photometry Points, Positions, RA, Redshift
* Redshift Flag, Redshift Points, References, Separation, Type, Velocity
*
*/
/**
* Returns separation in arcsec ( for lisibility )
* @param {*} RA1 in degrees
* @param {*} DEC1 in degrees
* @param {*} RA2 in degrees
* @param {*} DEC2 in degrees
*/
function separation(RA1, DEC1, RA2, DEC2) {
const alpha_line = parseFloat(RA1) * Math.PI / 180;
const delta_line = parseFloat(DEC1) * Math.PI / 180;
const alpha_glob = parseFloat(RA2) * Math.PI / 180;
const delta_glob = parseFloat(DEC2) * Math.PI / 180;
const step1 = Math.sin(delta_line) * Math.sin(delta_glob) + Math.cos(delta_line) * Math.cos(delta_glob) * Math.cos(alpha_line - alpha_glob);
return (Math.acos(step1) * 180 / Math.PI * 60) * 60;
}
/**
* Returns tangential comoving distance
* @params {float} WK
* @params {float} DCMR
*
* @returns {float}
*/
function dcmt(wk, dcmr) {
let ratio = 1.00;
const x = Math.sqrt(Math.abs(wk)) * dcmr;
if (x > 0.1) {
if (wk > 0) {
ratio = 0.5 * (Math.exp(x) - Math.exp(-x)) / x;
} else {
ratio = Math.sin(x) / x;
}
return ratio * dcmr;
}
let y = x * x
if (wk < 0) {
y = -y;
}
ratio = 1 + y / 6 + y * y / 120;
y = ratio * dcmr;
return y;
}
/**
* Returns an object containing the distant in Mpc, the scale in kpc,
* the luminosity distance in Mpc and the z value in Gyr
*
* @param {number} Hnot Hubble constant value
* @param {number} OmegaM mass density value
* @param {number} z redshift value
* @returns {object}
*/
function computeDistanceQuantities(Hnot, OmegaM, z) {
//number of points in integrals
const n = 1000;
const H0 = Hnot;
const WM = OmegaM;
const WV = 1. - OmegaM;
const h = H0 / 100;
// velocity of light in km/sec
const c = Constants.SPEED_OF_LIGHT / 1000;
// coefficent for converting 1/H into Gyr
const Tyr = 977.8;
// value of age in Gyr
let age_Gyr = 0.0;
let DCMR_Mpc = 0.0;
let DCMR_Gyr = 0.0;
// angular size distance
let DA = 0.0;
let DA_Mpc = 0.0;
let DA_Gyr = 0.0;
//let kpc_DA = 0.0;
// luminosity distance
let DL = 0.0;
let DL_Mpc = 0.0;
let DL_Gyr = 0.0; // DL in units of billions of light years
//const a = 1.0; // 1/(1+z), the scale factor of the Universe
// entry point for the input form to pass values back to this script
// includes 3 massless neutrino species, T0 = 2.72528
const WR = 4.165E-5 / (h * h);
// Omega curvaturve = 1-Omega(total)
const WK = 1 - WM - WR - WV;
const az = 1.0 / (1 + 1.0 * z);
let age = 0;
for (let i = 0; i < n; i++) {
const a = az * (i + 0.5) / n;
const adot = Math.sqrt(WK + (WM / a) + (WR / (a * a)) + (WV * a * a));
age = age + 1 / adot;
}
// age of Universe at redshift z in units of 1/H0
let zage = az * age / n;
/* correction for annihilations of particles not present now like e+/e-
added 13-Aug-03 based on T_vs_t.f */
const lpz = Math.log((1 + 1.0 * z)) / Math.log(10.0);
let dzage = 0
if (lpz > 7.500)
dzage = 0.002 * (lpz - 7.500);
if (lpz > 8.000)
dzage = 0.014 * (lpz - 8.000) + 0.001;
if (lpz > 8.500)
dzage = 0.040 * (lpz - 8.500) + 0.008;
if (lpz > 9.000)
dzage = 0.020 * (lpz - 9.000) + 0.028;
if (lpz > 9.500)
dzage = 0.019 * (lpz - 9.500) + 0.039;
if (lpz > 10.000)
dzage = 0.048;
if (lpz > 10.775)
dzage = 0.035 * (lpz - 10.775) + 0.048;
if (lpz > 11.851)
dzage = 0.069 * (lpz - 11.851) + 0.086;
if (lpz > 12.258)
dzage = 0.461 * (lpz - 12.258) + 0.114;
if (lpz > 12.382)
dzage = 0.024 * (lpz - 12.382) + 0.171;
if (lpz > 13.055)
dzage = 0.013 * (lpz - 13.055) + 0.188;
if (lpz > 14.081)
dzage = 0.013 * (lpz - 14.081) + 0.201;
if (lpz > 15.107)
dzage = 0.214;
zage = zage * Math.pow(10.0, dzage);
const zage_Gyr = (Tyr / H0) * zage;
let DTT = 0.0;
let DCMR = 0.0;
// do integral over a=1/(1+z) from az to 1 in n steps, midpoint rule
for (let i = 0; i < n; i++) {
const a = az + (1. - az) * (i + 0.5) / n;
const adot = Math.sqrt(WK + (WM / a) + (WR / (a * a)) + (WV * a * a));
DTT = DTT + 1. / adot;
DCMR = DCMR + 1. / (a * adot);
}
DTT = (1. - az) * DTT / n;
DCMR = (1. - az) * DCMR / n;
age = DTT + zage;
age_Gyr = age * (Tyr / H0);
DCMR_Gyr = (Tyr / H0) * DCMR;
DCMR_Mpc = (c / H0) * DCMR;
DA = az * dcmt(WK, DCMR);
DA_Mpc = (c / H0) * DA;
const scale_DA = DA_Mpc / 206.264806;
DA_Gyr = (Tyr / H0) * DA;
DL = DA / (az * az);
DL_Mpc = (c / H0) * DL;
DL_Gyr = (Tyr / H0) * DL;
return {
"distance_Mpc": DA_Mpc,
"scale_kpc": scale_DA,
"luminosity_distance_Mpc": DL_Mpc,
"z_age_Gyr": zage_Gyr
};
}
/**
* Returns luminosity distance in Mpc calculated by computeDistanceQuantities
* @param {number} Hnot Hubble constant value
* @param {number} OmegaM mass density value
* @param {number} z redshift value
* @returns luminosity distance in Mpc
*/
function computeDl(Hnot, OmegaM, z){
return computeDistanceQuantities(Hnot, OmegaM, z)["luminosity_distance_Mpc"];
}
/**
* An object representing a list of sources in the sky
*/
class SourceTable {
/**
*
* @param {string} table_target DOM element containing the table
* @param {string} table_title Title of the table
* @param {object} spectroUI spectroscopy user interface
*/
constructor(table_target, table_title, spectroUI) {
this.nedResult = [];
this.tableTarget = table_target;
this.tableTitle = table_title;
// purely internal table id
this._tableId = "ned-table";
// name of area displaying a warning message to user when DL value is not calculated
this.dlWarning = "dl-warning";
this._sourceInfo = "ned-source-info";
this._dlInfo = "ned-dl-info";
this.spectroUI = spectroUI;
this.previousSelection = null;
this.radius = null;
this.Hnot = 69.6;
this.OmegaM = 0.286;
this._lastZ = null;
this.cache = {};
this.data = [];
// a list a listeners that are called when a new value is selected in table
this.selectionListeners = [];
this._getHyperlinkCell = this._getHyperlinkCell.bind(this);
this._getTableHeader = this._getTableHeader.bind(this);
this._getTable = this._getTable.bind(this);
this._showResult = this._showResult.bind(this);
this._onClick = this._onClick.bind(this);
this._getDataTable = this._getDataTable.bind(this);
this._writeInfoMessage = this._writeInfoMessage.bind(this);
this._showSpinner = this._showSpinner.bind(this);
this.getSources = this.getSources.bind(this);
this._setTitle = this._setTitle.bind(this);
this._getNedTapUrl = this._getNedTapUrl.bind(this);
this._executeListeners = this._executeListeners.bind(this);
this.addListener = this.addListener.bind(this);
this.removeListener = this.removeListener.bind(this);
}
get lastZ(){
return this._lastZ;
}
/**
* Returns a TAP request to NED service
* @param {float} RA
* @param {float} DEC
* @returns {string} query url
*/
_getNedTapUrl(RA, DEC, radius) {
const url = "https://ned.ipac.caltech.edu/tap/sync?query=SELECT+prefname,ra,dec,z,pretype,n_crosref+FROM+objdir+WHERE+CONTAINS(POINT('',ra,dec),CIRCLE(''," + RA + "," + DEC + "," + radius + "))=1+AND+z+IS+NOT+null+ORDER+BY+n_crosref+DESC&LANG=ADQL&REQUEST=doQuery&FORMAT=votable%2ftd";
return url;
}
/**
* Returns a cone search request to NED service
* @param {float} RA
* @param {float} DEC
* @returns {string} query url
*/
_getNedWebUrl(RA, DEC, radius) {
// ned cone search radius unit is arcmin
const search_radius = radius * 60;
const url = "https://ned.ipac.caltech.edu/conesearch?in_csys=Equatorial&in_equinox=J2000&coordinates=" + RA + "d%20" + DEC + "d&radius=" + search_radius + "&hconst=67.8&omegam=0.308&omegav=0.692&wmap=4&corr_z=1&z_constraint=Available&z_unit=z&ot_include=ANY&nmp_op=ANY&search_type=Near%20Position%20Search&out_csys=Equatorial&out_equinox=Same%20as%20Input&obj_sort=Distance%20to%20search%20center";
return url;
}
/** Returns a link to the NED page listing distances for a source
*
* @param {object} source source name
* @returns url
*/
_getDistanceLink(source){
return "http://ned.ipac.caltech.edu/cgi-bin/nDistance?name="+encodeURI(source);
}
/**
* Builds the content of the container displaying the source infos
* @param {object} source a source name
* @returns DOMElement
*/
_buildSourceInfo(source){
let span = document.createElement('span');
span.innerText = "Selected source : " + source;
return span;
}
/**
* Builds the content of the container displaying link to NED DL page
* for this source
* @param {object} source a source name
* @returns DOMElement
*/
_buildDLInfo(source){
let link = document.createElement("a");
link.innerText = "(Redshift-independant DL)";
link.href = this._getDistanceLink(source);
link.target = "_blank";
return link;
}
/**
* Fills the title area of the sources container
* @param {float} RA
* @param {float} DEC
* @param {float} radius
*/
_setTitle(RA, DEC, radius) {
let domElement = document.getElementById(this.tableTitle);
let div = document.createElement("div");
div.className = "title";
while (domElement.firstChild) {
domElement.firstChild.remove();
}
let p1 = document.createElement("p");
let p2 = document.createElement("p");
let p3 = document.createElement("p");
p3.id = this.dlWarning;
p3.className = this.dlWarning;
let a = document.createElement("a");
a.href = this._getNedWebUrl(RA, DEC, radius);
a.target = "_blank";
a.innerHTML = "Data from NED";
p1.appendChild(a);
p2.innerHTML = `Flat Universe with <span title="Hubble constant">H<sub>0</sub></span>=${this.Hnot},
<span title="Mass density">Ω<sub>M</sub></span>=${this.OmegaM}`;
div.appendChild(p1);
div.appendChild(p2);
div.appendChild(p3);
domElement.appendChild(div);
}
/**
* Displays a message in the dedicated area in title div
* @param {string} message displayed message
*/
_setDlWarning(message){
document.getElementById(this.dlWarning).innerText = message;
}
/**
* Returns a TD DOM element
* @param {string} data
*/
_getDataCell(data) {
let td = document.createElement("td");
td.innerHTML = data;
return td;
}
/**
* Returns a TD DOM element containing a hyperlink
* @param {string} link
* @param {string} text
*/
_getHyperlinkCell(link, text) {
let a = document.createElement("a");
a.innerHTML = text;
a.href = link;
a.target = "_blank";
let td = document.createElement("td");
td.appendChild(a);
return td;
}
/**
* Returns a TH DOM element
* @param {string} content
*/
_getTableHeader(content) {
let th = document.createElement("th");
th.innerHTML = content;
return th;
}
/**
* Add a SPAN element with a text message inside the DOM element containing the results
* @param {string} text
*/
_writeInfoMessage(text) {
let domElement = document.getElementById(this.tableTarget);
let span = document.createElement("span");
span.textContent = text;
domElement.appendChild(span);
}
/**
* Shows a spinner image indicating that the UI is busy
*
*/
_showSpinner() {
let domElement = document.getElementById(this.tableTarget);
let div = document.createElement("div");
div.classList.add("spinner-border");
div.role = "status";
let span = document.createElement("span");
span.classList.add("sr-only");
div.appendChild(span);
domElement.appendChild(div);
}
/**
* Calls all the registered listeners
* @param event contains values of redshift, velocity, ra, dec
*/
_executeListeners(event) {
for (let l of this.selectionListeners) {
l.sourceTableCall(event);
}
}
/**
* Adds a new listener to the list
* @param {object} listener
*/
addListener(listener) {
this.selectionListeners.push(listener);
}
/**
* Removes a listener
* @param {object} listener
*/
removeListener(listener) {
for (let i = 0; i < this.selectionListeners.length; i++) {
if (this.selectionListeners[i] === listener) {
this.selectionListeners.splice(i, 1);
//return true;
}
}
}
/**
* Event triggered when a TR element is clicked
* It is propagated to all entries in this.selectionListeners
*
* @param {*} event
*/
_onClick(event) {
let row = event.target.parentNode.getAttribute("data-row");
// if row is null, user clicked on link to ned page
if(row !== null){
if (this.previousSelection != null) {
this.previousSelection.className = '';
}
event.target.parentNode.className = "table-active";
//const velocity = redshift2Velocity(this.data[row]['redshift']);
this.previousSelection = event.target.parentNode;
this.spectroUI.setRedshift(this.data[row]['redshift']);
this.spectroUI.setVelocity(0);
if(document.getElementById(this._dlInfo) !== null){
this.spectroUI.setDl(this.data[row]['dl']);
cleanContainer(document.getElementById(this._dlInfo));
if(this.data[row]['redshift'] < Constants.MIN_REDSHIFT_FOR_CALC ){
this._setDlWarning(`z < ${Constants.MIN_REDSHIFT_FOR_CALC}, DL value must be set manually to compute a Line Luminosity`);
document.getElementById(this._dlInfo).appendChild(this._buildDLInfo(this.data[row]['object']));
}else{
this._setDlWarning("");
}
}
//last selected source
if(document.getElementById(this._sourceInfo) !== null){
cleanContainer(document.getElementById(this._sourceInfo));
document.getElementById(this._sourceInfo).appendChild(this._buildSourceInfo(this.data[row]['object']));
//reset redshift to last selected source value
DOMAccessor.getResetZButton().classList.remove("hidden");
}
let clickevent = EventFactory.getEvent(EventFactory.EVENT_TYPES.custom, {
detail: {
redshift: this.data[row]['redshift'],
//velocity: velocity,
ra: this.data[row]['ra'],
dec: this.data[row]['dec'],
object: this.data[row]['object']
}
})
this._lastZ = this.data[row]['redshift'];
this._executeListeners(clickevent);
}
}
/**
* Returns a HTML table element with all its rows in tbody
*
* @param {*} rows
* @returns {domElement}
*/
_getTable(rows) {
let table = document.createElement("table");
table.id = this._tableId;
table.className = "table table-hover ned-table";
let thead = document.createElement("thead");
let tr = document.createElement("tr");
tr.appendChild(this._getTableHeader("Object Name"));
tr.appendChild(this._getTableHeader("RA"));
tr.appendChild(this._getTableHeader("Dec"));
tr.appendChild(this._getTableHeader("Type"));
tr.appendChild(this._getTableHeader("Redshift"));
tr.appendChild(this._getTableHeader("Separation (arcsec)"));
tr.appendChild(this._getTableHeader("Separation (kpc)"));
tr.appendChild(this._getTableHeader("Scale (kpc/‘’)"));
tr.appendChild(this._getTableHeader("DA (Mpc)"));
tr.appendChild(this._getTableHeader("Z age (Gyr)"));
tr.appendChild(this._getTableHeader("DL (Mpc)"));
tr.appendChild(this._getTableHeader("References"));
tr.appendChild(this._getTableHeader("Link to NED"));
thead.appendChild(tr);
table.appendChild(thead);
let tbody = document.createElement("tbody");
for (let row of rows) {
tbody.appendChild(row);
}
table.appendChild(tbody);
return table;
}
/**
* Displays the result table or a warning message if no result has been found
*
* @param {float} RA
* @param {float} DEC
*/
_showResult(RA, DEC) {
// clear content of component containing the table
cleanContainer(document.getElementById(this.tableTarget));
let table = this._getDataTable(RA, DEC, this.radius);
let rows = table["table"];
this.data = table["data"];
// build results table
if (rows.length > 0) {
let domElement = document.getElementById(this.tableTarget);
domElement.appendChild(this._getTable(rows));
//table sortable and searchable
$('#' + this._tableId).DataTable({
order: [[11, 'desc']]
});
}
// no redshift data available, show message
else {
console.log("no redshift found, writeMessage");
this._writeInfoMessage("No redshift information available");
}
}
/**
* Returns rows of HTML table and the metadata associated to each row.
* Additional metadata are calculated if z > Constants.MIN_REDSHIFT_FOR_CALC
* (scale in kpc, distance in Mpc, * z age in Gyr, luminosity distance in Mpc)
*
* @returns {object} {"table" : [], "data" : []}
*/
_getDataTable(RA, DEC) {
let rows = [];
let rows_data = [];
for (let source of this.nedResult) {
let data = source.data;
const sep = separation(data["ra"], data["dec"], RA, DEC);
const distances = computeDistanceQuantities(this.Hnot, this.OmegaM, data["z"]);
let row = document.createElement("tr");
row.setAttribute("data-row", rows.length);
let link = this._getHyperlinkCell(Constants.NED_SERVICE_URL + encodeURIComponent(data["prefname"]), data["prefname"]);
row.appendChild(this._getDataCell("<strong>"+data["prefname"]+"</strong>"));
row.appendChild(this._getDataCell(DecDeg2HMS(parseFloat(data["ra"]))));
row.appendChild(this._getDataCell(DecDeg2DMS(parseFloat(data["dec"]))));
row.appendChild(this._getDataCell(data["pretype"]));
row.appendChild(this._getDataCell(data["z"]));
row.appendChild(this._getDataCell(sep.toFixed(3)));
if (data["z"] > Constants.MIN_REDSHIFT_FOR_CALC) {
row.appendChild(this._getDataCell((sep * distances["scale_kpc"]).toFixed(3)));
row.appendChild(this._getDataCell(distances["scale_kpc"].toFixed(3)));
row.appendChild(this._getDataCell(distances["distance_Mpc"].toFixed(3)));
row.appendChild(this._getDataCell(distances["z_age_Gyr"].toFixed(3)));
row.appendChild(this._getDataCell(distances["luminosity_distance_Mpc"].toFixed(3)));
} else {
row.appendChild(this._getDataCell("N/A"));
row.appendChild(this._getDataCell("N/A"));
row.appendChild(this._getDataCell("N/A"));
row.appendChild(this._getDataCell("N/A"));
row.appendChild(this._getDataCell("N/A"));
}
let dl = parseFloat(distances["luminosity_distance_Mpc"].toFixed(3));
if(data["z"] <= Constants.MIN_REDSHIFT_FOR_CALC){
dl = null;
}
row.appendChild(this._getDataCell(data["n_crosref"]));
row.addEventListener('click', this._onClick);
row.appendChild(this._getHyperlinkCell(Constants.NED_SERVICE_URL + encodeURIComponent(data["prefname"]), "Link"));
rows_data.push({
"object": data["prefname"].trim(),
"redshift": parseFloat(data["z"]),
"velocity": "",
"dl": dl,
"ra": parseFloat(data["ra"]),
"dec": parseFloat(data["dec"])
});
rows.push(row);
}
return { "table": rows, "data": rows_data };
}
/**
* Gets sources at RA/DEC/radius from NED catalog
* @param {*} RA
* @param {*} DEC
* @param {*} radius in degrees
*/
getSources(RA, DEC, radius, Hnot, OmegaM) {
cleanContainer(document.getElementById(this.tableTarget));
this.radius = radius;
this.Hnot = Hnot;
this.OmegaM = OmegaM;
this._setTitle(RA, DEC, radius);
//this._writeInfoMessage("Please wait, NED request in progress.");
this._showSpinner();
this.previousRadius = radius;
const url = this._getNedTapUrl(RA, DEC, radius);
// NED request through Aladin
// This is a vulnerability of our service, if Aladin is down NED is unreachable
A.catalogFromURL(url, { onClick: 'showTable' }, (result) => {
if (result.length > 0) {
this.nedResult = result;
this._showResult(RA, DEC);
} else {
cleanContainer(document.getElementById(this.tableTarget));
this._writeInfoMessage("Nothing found, take a larger radius.");
}
});
}
}
export {
SourceTable, computeDistanceQuantities, computeDl
}