import { hashCode, f2v, shift, unshift, getLpLine, KToCm } from "./utils.js";
import { FITS_HEADER } from "./fitsheader.js";
import { DEFAULT_INTENSITY, SPECTRO_COLLECTIONS, REDSHIFT_SHIFT_TYPE, VELOCITY_SHIFT_TYPE, DEFAULT_SPECTROSCOPY_DATABASE } from "./constants.js";
import { URL_SPECTRO_SERVER } from './init.js';
/**
* An object providing methods to format spectroscopic data display
* @typedef {Object} SpectroscopyFormatter
*/
class SpectroscopyFormatter {
constructor() { }
/**
* Returns a string formatting a list of spectral lines to display
* @param {array} lines an array of line objects
* @param {number} shiftValue redshift value (can also be a velocity) (float)
* @param {string} shiftType velocity or redshift
* @param {number} freqMin minimum frequency (float)
* @param {number} freqMax maximum frequency (float)
* @returns {string}
*/
linesTofile(lines, shiftValue, shiftType, freqMin, freqMax) {
let result = lines[0].sourcefile + "\n\n";
result += " Rest Frequency (GHz)".padStart(20) + " " + "Obs Frequency (GHz)".padStart(20) +
" " + "Elow (cm-1)".padStart(15) + " " + "Eup (cm-1)".padStart(15) + " " +
"Lower state".padStart(40) + " " + "Upper state".padStart(40) + "\n";
for (const line of lines) {
const obsFreq = shift(line.frequency, shiftValue, shiftType).toFixed(4);
// mark the line if it is in the interval
if (obsFreq <= freqMax && obsFreq >= freqMin) {
result += "=>";
} else {
result += " ";
}
result += line.frequency.toString().padStart(20) + " ";
result += obsFreq.toString().padStart(20) + " ";
result += line.lower_state.energy.toString().padStart(15) + " ";
result += line.upper_state.energy.toString().padStart(15) + " ";
result += this.quantumNumbers(line.lower_state).padStart(40) + " ";
result += this.quantumNumbers(line.upper_state).padStart(40) + " ";
result += "\n";
}
return result;
}
/**
* Returns the quantum numbers of a level as a string
* @param {object} level
* @returns {string}
*/
quantumNumbers(level) {
let result = "";
for (const qn of level.qns) {
result += qn.name + "=" + qn.value + " ";
}
return result;
}
}
/**
* Object managing interaction with the spectroscopy user interface
* @typedef {Object} SpectroscopyUI
* @property {object} toggler checkbox to enable/disable the UI
* @property {object} spectroscopyForm the whole form
* @property {object} redshiftElement redshift input
* @property {object} dlElement luminosity distance input
* @property {object} databaseElement database choice checkboxes
* @property {object} energyUpElement energy up input
* @property {object} energyUpUnitElement energy up unit select
* @property {object} intensityRangeElement intensity range
* @property {object} intensityElement intensity value display
* @property {object} atomCountMinElement minimum number of atoms
* @property {object} atomCountMaxElement maximum number of atoms
* @property {object} autoCompleteElement species choice with autocompletion
* @property {object} selectedSpeciesElement list of selected species
* @property {object} linesTable html table of displayed lines
* @property {object} linesTableBody html table tbody
* @property {object} linesTableContainer html table div
* @property {object} currentGroupDisplay id of currently displayed group
* @property {object} lastGroupDisplay id of last displayable group
* @property {object} surfaceInSummedSpectrum display surface of selected area in spectrum
* @property {object} lineLuminosityElement display value of line luminosity
* @property {object} referenceLineElement reference line used for luminosity calculation
* @property {array} groupsInfos list of tuples of min/max energy value for each lines group
* @property {number} currentGroup index of currently displayed group (int)
* @property {number} referenceFreq reference frequency value for luminosity calculation (float)
*/
class SpectroscopyUI {
constructor() {
const linesRedshift = "lines-redshift";
const linesVelocity = "lines-velocity";
const linesTable = "lines-table";
this.toggler = document.getElementById("toggle-lines-search");
this.togglerArea = document.getElementById("toggler-area");
this.spectroscopyForm = document.getElementById("spectroscopy-menu");
this.redshiftElement = document.getElementById(linesRedshift);
this.velocityElement = document.getElementById(linesVelocity);
this.dlElement = document.getElementById("lines-dl");
this.databaseElement = document.getElementsByName("spectrodatasource");
this.energyUpElement = document.getElementById("energyup");
this.energyUpUnitElement = document.getElementById("energyupunit");
this.intensityRangeElement = document.getElementById("intensityrange");
this.intensityElement = document.getElementById("intensity");
this.atomCountMinElement = document.getElementById("atomcount-min");
this.atomCountMaxElement = document.getElementById("atomcount-max");
this.autoCompleteElement = "autoComplete";
this.selectedSpeciesElement = "selectedspecies";
//this.energyGroups = document.getElementById(parameterSources['energygroups']);
this.linesTable = document.getElementById(linesTable);
this.linesTableBody = document.getElementById(linesTable + "-tbody");
this.linesTableContainer = document.getElementById(linesTable + "-container");
this.currentGroupDisplay = document.getElementById("current-group");
this.lastGroupDisplay = document.getElementById("last-group");
this.surfaceInSummedSpectrum = "selected-surface";
this.lineLuminosityElement = document.getElementById("lineluminosity-value");
this.lineLuminosityWarningElement = document.getElementById("lineluminosity-warning")
this.referenceLineElement = document.getElementById("reference-line");
this.redshiftRecallElement = document.getElementById(linesRedshift + "-recall");
this.velocityRecallElement = document.getElementById(linesVelocity + "-recall");
this.groupsInfos = [];
this.currentGroup = 0;
this.referenceFreq = null;
this._selectedLine = "";
// currently selected line in table for luminosity calculation
this._selectedLine = "";
this.getVelocity = this.getVelocity.bind(this);
this.setVelocity = this.setVelocity.bind(this);
this.getRedshift = this.getRedshift.bind(this);
this.setRedshift = this.setRedshift.bind(this);
this.getDl = this.getDl.bind(this);
this.setDl = this.setDl.bind(this);
this.getDatabase = this.getDatabase.bind(this);
this.getEnergyUp = this.getEnergyUp.bind(this);
this.getIntensity = this.getIntensity.bind(this);
this.setIntensity = this.setIntensity.bind(this);
this.getAtomCountMin = this.getAtomCountMin.bind(this);
this.getAtomCountMax = this.getAtomCountMax.bind(this);
this.getSelectedSpecies = this.getSelectedSpecies.bind(this);
this.initSpeciesSelection = this.initSpeciesSelection.bind(this);
this.setEnergyUp = this.setEnergyUp.bind(this);
this.isLineConfigurationOk = this.isLineConfigurationOk.bind(this);
this.showEnergyGroupLines = this.showEnergyGroupLines.bind(this);
this.setCurrentGroup = this.setCurrentGroup.bind(this);
this.setLastGroup = this.setLastGroup.bind(this);
this.getSurfaceInSummedSpectrum = this.getSurfaceInSummedSpectrum.bind(this);
this.toggleDatabaseSelector = this.toggleDatabaseSelector.bind(this);
this.hideToggler = this.hideToggler.bind(this);
this.showToggler = this.showToggler.bind(this);
}
/**
* Toggles on/off the radio button to search lines in a spectro db
* @param {string} name name of spectro db
* @param {boolean} isEnabled status of spectro db
*/
toggleDatabaseSelector(name, isEnabled) {
let status = "inline";
if (!isEnabled) {
status = "none";
}else{
this.showToggler();
}
document.getElementById(name + "datasourcespan").style.display = status;
}
/**
* Displays line luminosity L'_line value in the dedicated DOM element
* @param {*} spectrum_surface surface selected in summed pixel spectrum in Jy.km/s
*/
setLineLuminosityValue(spectrum_surface) {
// alert message if luminosity value is not a number whereas velocity and redshift are defined
if(this.referenceFreq === null){
this.lineLuminosityElement.innerHTML = "";
}else{
if (isNaN(parseFloat(this.dlElement.value)) &&
!isNaN(parseFloat(this.redshiftElement.value)) &&
!isNaN(parseFloat(this.velocityElement.value))) {
this.lineLuminosityElement.innerHTML = "Value of DL field is not a number, luminosity can not be calculated";
} else {
const result = getLpLine(spectrum_surface, this.referenceFreq,
parseFloat(this.dlElement.value), parseFloat(this.redshiftElement.value));
let alert = '';
if(this.redshiftElement.value < 0.1){
alert += "(the velocity is low please check the DL.)";
}
this.lineLuminosityElement.innerHTML =
`<b>Line luminosity</b><br/> L'<sub>line</sub> =
${result.toExponential(2)} K.km/s.pc<sup>2</sup> for a DL of ${this.dlElement.value} Mpc`;
this.lineLuminosityWarningElement.innerText = alert;
}
}
}
/**
* Disables intensity and energy configuration elements
*/
disable() {
$("#intensityrange").prop("disabled", true);
$("#energyup").prop("disabled", true);
$("#energyupunit").prop("disabled", true);
document.getElementById("intensityrangelabel").className = "disabled";
document.getElementById("energyuplabel").className = "disabled";
document.getElementById("energyupunit").className = "disabled";
}
/**
* Enables intensity and energy configuration elements
*/
enable() {
$("#intensityrange").prop("disabled", false);
$("#energyup").prop("disabled", false);
$("#energyupunit").prop("disabled", false);
document.getElementById("intensityrangelabel").className = "";
document.getElementById("energyuplabel").className = "";
document.getElementById("energyupunit").className = "";
}
/**
*
* @returns Value of integral of selected area in summed pixels spectrum
*/
getSurfaceInSummedSpectrum() {
return parseFloat(document.getElementById(this.surfaceInSummedSpectrum).innerText);
}
/**
* Displays the index of the currently displayed lines group
* @param {number} groupIndex index of group to display (int)
*/
setCurrentGroup(groupIndex) {
this.currentGroupDisplay.textContent = groupIndex;
}
/**
* Displays the index of the last lines group
* @param {number} groupIndex index of group to display (int)
*/
setLastGroup(groupIndex) {
this.lastGroupDisplay.textContent = groupIndex;
}
/**
* Returns the visibility status of the spectroscopy user interface
* @returns {boolean}
*/
isEnabled() {
return this.toggler.checked;
}
hideToggler(){
this.togglerArea.style.visibility = "hidden";
}
showToggler(){
this.togglerArea.style.visibility = "";
}
/**
* Makes the spectroscopy user interface visible
*/
showSpectroscopyMenu() {
this.spectroscopyForm.className = "";
}
/**
* Hides the spectroscopy user interface visible
*/
hideSpectroscopyMenu() {
this.spectroscopyForm.className = "hidden";
}
/**
* Hides the table of lines group
*/
hideEnergyGroupLines() {
this.linesTableContainer.className = "hidden";
}
/**
* Returns the redshift value ( either redshift or velocity ) and the type of returnd value
* in an object
* @returns {object}
*/
getShift() {
const velocity = this.getVelocity();
const redshift = this.getRedshift();
let result = {}
if (!isNaN(velocity) && velocity < 30000) {
result["shiftValue"] = velocity;
result["shiftType"] = VELOCITY_SHIFT_TYPE;
} else if (!isNaN(redshift)) {
result["shiftValue"] = redshift;
result["shiftType"] = REDSHIFT_SHIFT_TYPE;
}
return result;
}
/**
* Returns true if configuration of spectral lines display is correct (redshift or velocity is set and
* checkbox is selected)
* @returns {boolean}
*/
isLineConfigurationOk() {
//menu is disabled
if (!this.isEnabled())
return true;
//menu is enabled and (velocity or redshift) is defined
if (this.isEnabled() && (this.getRedshift() !== undefined || this.getVelocity() !== undefined))
return true;
return false;
}
/**
* Display a table containing found spectral lines
* @param {*} lines
*/
showEnergyGroupLines(lines) {
let self = this;
if (lines.length > 0) {
this.linesTableContainer.className = "";
lines.sort(function (a, b) { return a.obsFrequency - b.obsFrequency > 0 });
while (this.linesTableBody.hasChildNodes()) {
//tr element
let element = this.linesTableBody.lastChild;
//removes all tds
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
this.linesTableBody.removeChild(element);
}
for (let i = 0; i < lines.length; i++) {
let tr = document.createElement("tr");
tr.dataset.id = i;
let td1 = document.createElement("td");
td1.textContent = lines[i].species.formula;
if (lines[i].sourcefile !== undefined)
td1.title = lines[i].sourcefile;
let td2 = document.createElement("td");
td2.textContent = lines[i].obsFrequency.toFixed(4);
// display in red the currently selected line
if((td1.textContent + td2.textContent + i) === this._selectedLine){
tr.style.color = "red";
}
if((td1.textContent + td2.textContent) === this._selectedLine){
tr.style.color = "red";
}
tr.appendChild(td1);
tr.appendChild(td2);
// clicked lines is selected as reference line
tr.addEventListener('click', (event) => {
let tr = event.target.parentNode;
// set currently selected line id
self._selectedLine = tr.childNodes[0].innerText + tr.childNodes[1].innerText + tr.dataset.id;
let allTrs = tr.parentNode.childNodes;
// all lines in default color
for(let i =0; i < allTrs.length; i++ ){
allTrs[i].style.color = "#212529";
}
//selected line in red
tr.style.color = "red";
self.referenceLineElement.innerHTML = "The line selected is <br/>" + tr.childNodes[0].innerText +
" " + tr.childNodes[1].innerText + " GHz";
self.referenceFreq = parseFloat(tr.childNodes[1].innerText);
// clear last calculated luminosity value
self.lineLuminosityElement.innerText = "";
});
this.linesTableBody.appendChild(tr);
}
}
}
/**
* Sets the list of group infos. Each element in the list is a tuple containing the minimum and maximum value
* of upper_energy for the lines in the group
* @param {*} groupsInfos
*/
setGroupInfos(groupsInfos) {
this.groupsInfos = groupsInfos;
}
/**
* Changes the currently displayed group
* @param {number} newGroupIndex index of newly displayed group (int)
* @param {function} callback callback function applied to this group
*/
changeGroupAction(newGroupIndex, callback) {
if (newGroupIndex >= 0 && newGroupIndex < this.groupsInfos.length) {
this.currentGroup = newGroupIndex;
let infos = "";
// if not local db
if (this.groupsInfos[this.currentGroup][0] !== null) {
infos = "Upper energy between " + this.groupsInfos[this.currentGroup][0].toFixed(3)
+ " and " + this.groupsInfos[this.currentGroup][1].toFixed(3) + " cm-1";
}
callback(this.currentGroup);
}
}
/**
* Returns the minimum number of atoms in searched species
* @returns {number} an integer
*/
getAtomCountMin() {
return parseInt(this.atomCountMinElement[this.atomCountMinElement.selectedIndex].value, 10);
}
/**
* Returns the maximum number of atoms in searched species
* @returns {number} an integer
*/
getAtomCountMax() {
return parseInt(this.atomCountMaxElement[this.atomCountMaxElement.selectedIndex].value, 10);
}
/**
* Returns velocity (undefined if empty)
* @returns {number} a float value
*/
getVelocity() {
let result = parseFloat(this.velocityElement.value);
if (!isNaN(result))
return result;
else
return undefined;
}
/**
* Sets velocity value
* @param {number} value (float)
*/
setVelocity(value) {
if (isNaN(parseFloat(value)) && value.trim() !== "") {
alert("Velocity value must be a float");
} else {
this.velocityElement.value = value;
}
}
/**
* Set empty content in velocity field
*/
clearVelocity() {
this.velocityElement.value = "";
}
/**
* Returns redshift (undefined if empty)
* @returns {number} a float value
*/
getRedshift() {
let result = parseFloat(this.redshiftElement.value);
if (!isNaN(result))
return result;
else
return undefined;
}
/**
* Sets redshift value
* @param {number} value (float)
*/
setRedshift(value) {
if (isNaN(parseFloat(value)) && value.trim() !== "") {
alert("Redshift value must be a float");
} else {
this.redshiftElement.value = value;
}
}
/**
* Sets empty content in redshift field
*/
clearRedshift() {
this.redshiftElement.value = "";
}
/**
* Returns luminosity distance (undefined if empty)
* @returns {number} a float value
*/
getDl() {
let result = parseFloat(this.dlElement.value);
if (!isNaN(result))
return result;
else
return undefined;
}
/**
* Sets luminosity distance value
* @param {number} value (float)
*/
setDl(value) {
if (value === null) {
this.dlElement.value = "";
} else {
const dl = parseFloat(value);
if (isNaN(dl) && value.trim() !== "") {
alert("DL value must be a float");
} else {
this.dlElement.value = dl;
}
}
}
/**
* Set empty content in luminosity distance field
*/
clearDl() {
this.dlElement.value = "";
}
/**
* Returns selected database
* @returns {string}
*/
getDatabase() {
let result = "";
for (let i = 0; i < this.databaseElement.length; i++) {
if (this.databaseElement[i].checked) {
result = this.databaseElement[i].value;
}
}
return result;
}
/**
* Selects a spectroscopy database by default
* (see DEFAULT_SPECTROSCOPY_DATABASE in constants.js file for configuration)
*/
setDefaultDatabase() {
for (let i = 0; i < this.databaseElement.length; i++) {
if (this.databaseElement[i].id === DEFAULT_SPECTROSCOPY_DATABASE) {
this.databaseElement[i].checked = true;
} else {
this.databaseElement[i].checked = false;
}
}
}
/**
* Returns energy of upper level in cm-1
* @returns {number} a float value
*/
getEnergyUp() {
let result = undefined;
const eup = parseFloat(this.energyUpElement.value);
if (!isNaN(eup)) {
if (this.energyUpUnitElement[this.energyUpUnitElement.selectedIndex].value === "K") {
result = KToCm(eup);
} else {
result = eup;
}
}
return result;
}
/**
* Sets maximum value for upper energy in user interface
* @param {number} value (float)
*/
setEnergyUp(value) {
if (isNaN(parseFloat(value))) {
alert("Energy value must be a float");
} else {
this.energyUpElement.value = value;
}
}
/**
* Returns intensity value
* @returns {number} a float value
*/
getIntensity() {
let result = undefined;
const intensity = parseFloat(this.intensityRangeElement.value);
if (!isNaN(intensity)) {
result = intensity;
}
return result;
}
/**
* Sets value of intensity in dedicated field and in range element
* @param {foat} value
*/
setIntensity(value) {
this.intensityRangeElement.value = value;
this.intensityElement.innerText = value;
}
/**
* Returns minimum intensity value
* @returns {number} a float value
*/
getIntensityMin() {
return parseFloat(this.intensityRangeElement.min);
}
/**
* Returns maximum intensity value
* @returns {number} a float value
*/
getIntensityMax() {
return parseFloat(this.intensityRangeElement.max);
}
/**
* Returns species selected by user
* @returns {array}
*/
getSelectedSpecies() {
let result = [];
let nodes = document.querySelector("#" + this.selectedSpeciesElement).childNodes;
for (let i = 0; i < nodes.length; i++) {
// first child is span element
result.push(nodes[i].childNodes[0].innerHTML);
}
return result;
}
/**
* Initializes the species selection list by filling the autocomplete list with possible
* values
* @param {string} source source database
* @param {object} metadata metadata from database (species list)
*/
initSpeciesSelection(source, metadata) {
let molecules = [];
document.querySelector("#" + this.autoCompleteElement).value = "";
let result_area = document.querySelector("#" + this.selectedSpeciesElement);
while (result_area.hasChildNodes()) {
result_area.removeChild(result_area.lastChild);
}
for (let i = 0; i < metadata.length; i++) {
if (metadata[i].source === source) {
for (let j = 0; j < metadata[i].species.length; j++) {
molecules.push(metadata[i].species[j]);
}
}
}
new autoComplete({
selector: "#" + this.autoCompleteElement,
placeHolder: "Search for species in " + source + " database",
searchEngine: "strict",
data: {
src: molecules
},
resultsList: {
maxResults: 200,
noResults: (list, query) => {
// Create "No Results" message list element
const message = document.createElement("li");
message.setAttribute("class", "no_result");
// Add message text content
message.innerHTML = `<span>Found No Results for "${query}"</span>`;
// Add message list element to the list
list.appendChild(message);
},
},
resultItem: {
highlight: {
render: true
}
},
onSelection: (feedback) => {
document.querySelector("#" + this.autoCompleteElement).blur();
// Prepare User's Selected Value
const selection = feedback.selection.value;
const selectedSpecies = this.getSelectedSpecies();
let self = this;
// Add species if not already selected
if (!selectedSpecies.includes(selection)) {
// Replace Input value with the selected value
document.querySelector("#" + this.autoCompleteElement).value = selection;
// Render selected choice to selection div
const elementId = "species-line" + hashCode(selection);
let li = document.createElement("li");
li.id = elementId;
// span containing species name
let span = document.createElement('span');
span.innerText = selection;
// button to remove the species
let removebutton = document.createElement("button");
removebutton.textContent = "Remove";
removebutton.className = "btn btn-secondary";
removebutton.onclick = () => {
result_area.removeChild(document.getElementById(elementId));
if (self.getSelectedSpecies().length === 0) {
self.setIntensity(DEFAULT_INTENSITY);
}
};
let datasetbutton = document.createElement("button");
datasetbutton.textContent = "Dataset";
datasetbutton.className = "btn btn-secondary";
datasetbutton.onclick = () => {
datasetbutton.dispatchEvent(new CustomEvent('getdataset', {
bubbles: true,
detail: {
dataset: selection,
db: SPECTRO_COLLECTIONS[self.getDatabase()]
}
}));
};
li.appendChild(span);
li.appendChild(removebutton);
li.appendChild(datasetbutton);
result_area.appendChild(li);
}
if (this.getSelectedSpecies().length > 0) {
this.setIntensity(this.getIntensityMin());
}
},
});
}
}
/**
* Object querying the spectroscopy service
*/
class SpectroscopyQuery {
constructor() {
// service endpoint
this.server = URL_SPECTRO_SERVER;
// will contain result of getMetadata for caching
this.metadata = null;
this.getTransitions = this.getTransitions.bind(this);
this.getMetadata = this.getMetadata.bind(this);
}
/**
* Get a list of transitions
* @param {string} db selected database
* @param {array} frequencies min/max frequencies
* @param {array} atomcount min/max number of atoms
* @param {number} energyup maximum value of upper energy (float)
* @param {array} species list of species
* @param {number} intensity maximum intensity value (float)
* @param {function} callback callback function called on found transitions
*/
getTransitions(db, frequencies, atomcount, energyup, species, intensity, callback) {
let criteria = {
"frequencies": frequencies,
"atomcount": atomcount,
"energyup": energyup,
"species": species,
"idealisedintensity": intensity,
"sourcefiles": undefined
};
$.ajax({
method: "POST",
url: this.server + "/spectroscopy/" + db + "/lines",
data: JSON.stringify(criteria),
contentType: "application/json",
crossDomain: true
})
.done(function (transitions) {
callback(transitions);
}).fail(function (jqXHR, textStatus) {
console.log("error");
console.log(jqXHR);
});
}
/**
* Get a complete dataset with all its transitions
* @param {string} db name of selected database
* @param {string} sourcefile data file name in source database
* @param {function} callback callback function called on returned transitions
*/
getDataset(db, sourcefile, callback) {
let criteria = {
"sourcefile": sourcefile
};
$.ajax({
method: "POST",
url: this.server + "/spectroscopy/" + db + "/dataset",
data: JSON.stringify(criteria),
contentType: "application/json",
/*crossDomain : true*/
})
.done(function (transitions) {
callback(transitions);
}).fail(function (jqXHR, textStatus) {
console.log("error");
console.log(jqXHR);
});
}
/**
* Get metadata from spectroscopy database ( species by source database )
* @param {function} callback function applied to returned data
*/
getMetadata(callback) {
let self = this;
// no ajax request if metadata already available
if (this.metadata === null) {
$.ajax({
method: "GET",
url: this.server + "/metadata",
contentType: "application/json",
/*crossDomain : true*/
})
.done(function (metadata) {
self.metadata = metadata;
callback(metadata);
}).fail(function (jqXHR, textStatus) {
console.log("error");
console.log(jqXHR);
});
}
// performs callback on cached data
else {
callback(this.metadata);
}
}
/**
* Get status of dbs from spectroscopy database
* @param {function} callback function applied to returned data
*/
getStatuses(callback) {
// no ajax request if metadata already available
if (this.metadata === null) {
$.ajax({
method: "GET",
url: this.server + "/spectroscopy/databases/status",
contentType: "application/json",
/*crossDomain : true*/
})
.done(function (results) {
for (let result of results) {
callback(result.db, !result.isempty);
}
}).fail(function (jqXHR, textStatus) {
console.log("error");
console.log(jqXHR);
});
}
// performs callback on cached data
else {
callback(this.metadata);
}
}
}
/**
* Object plotting lines on a spectrum
*
* @typedef {Object} LinePlotter
* @property {number} linesCount number of lines currently displayed (int)
* @property {array} transitions complete list of spectral lines in the searched band
* @property {array} targets spectra graphs where lines will be plotted
* @property {SpectroscopyUI} spectroUI
* @property {array} transitionGroups gorups of lines
* @property {string} shiftedLineIdPrefix prefix used when setting the id of a shifted line in the DOM
* @property {string} lineColor hexadecimal code defining the color of a line
* @property {number} obsFreqMin minimum frequency (float)
* @property {number} obsFreqMax maximum frequency (float)
*
*/
class LinePlotter {
/**
* @param {SpectroscopyUI} spectroUI object controlling the spectroscopy user interface
*/
constructor(spectroUI) {
this.linesCount = 0;
this.transitions = [];
//list of graphs where lines will be plotted
this.targets = null;
//access to user input
this.spectroUI = spectroUI;
// group of transitions
this.transitionGroups = [];
this.shiftedLineIdPrefix = "spectroline-shifted";
this.lineColor = "#ff9167";
// stores previous values of obs frequency
this.obsFreqMin = null;
this.obsFreqMax = null;
this._initEvents();
this.plotLines = this.plotLines.bind(this);
this.loadAndPlotLines = this.loadAndPlotLines.bind(this);
this.refresh = this.refresh.bind(this);
this.plotTransitionGroup = this.plotTransitionGroup.bind(this);
}
_initEvents() {
//ENTER on redshift or velocity input fields
document.getElementById("lines-redshift").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
this.refresh();
}
});
document.getElementById("lines-velocity").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
this.refresh();
}
});
}
/**
* Calculates the shifted value of all the lines in the given array
* returns a deep-copy of the array with shifted line values
* @param {array} transitions array of transitions
* @param {number} value value used to calculate shift (float)
* @param {string} type redshift or velocity
* @returns {array} shifted transitions
*/
shiftLines(transitions, value, type) {
let shifted_transitions = transitions.slice();
for (let line of shifted_transitions) {
line.obsFrequency = shift(line.frequency, value, type);
}
return shifted_transitions;
}
/**
* Called when listened object triggers a message
* @param {event} event
*/
call(event) {
this.refresh();
}
/**
* Refreshes displayed lines
*/
refresh() {
if (this.targets !== null) {
if (this.obsFreqMin != null && this.obsFreqMax != null) {
this.loadAndPlotLines(this.obsFreqMin, this.obsFreqMax, this.targets);
}
}
}
/**
* query the db to get spectral lines and plot them on the graph (calling plotLines)
* the query is done on a velociy interval
* @param {number} obsFreqMin min observed frequency in Ghz (float)
* @param {number} obsFreqMax max observed frequency in Ghz (float)
* @param {array} targets array of axes where lines will be displayed
* ( x axis of a spectrum )
*/
loadAndPlotLines(obsFreqMin, obsFreqMax, targets) {
// do the search on rest frequencies
const shift = this.spectroUI.getShift();
const shiftValue = shift["shiftValue"];
const shiftType = shift["shiftType"];
this.obsFreqMin = obsFreqMin;
this.obsFreqMax = obsFreqMax;
this.targets = targets;
const frequencies = [{
min: unshift(obsFreqMin, shiftValue, shiftType),
max: unshift(obsFreqMax, shiftValue, shiftType)
}];
const atomCount = [{
min: this.spectroUI.getAtomCountMin(),
max: this.spectroUI.getAtomCountMax()
}];
let query = new SpectroscopyQuery();
// query data and execute this.plotLines on found spectral lines
query.getTransitions(this.spectroUI.getDatabase(), frequencies, atomCount,
this.spectroUI.getEnergyUp(), this.spectroUI.getSelectedSpecies(),
this.spectroUI.getIntensity(), this.plotLines);
}
/**
* Plots transitions on all the graphs registered in this.targets
* @param {array} transitions
*/
plotLines(transitions) {
this.transitions = transitions;
let shift = this.spectroUI.getShift();
let plotted_transitions = transitions;
const shiftValue = shift["shiftValue"];
const shiftType = shift["shiftType"];
// number of lines in a line group
const linesInGroup = 6;
if (shiftValue !== undefined) {
if (shiftType != null) {
plotted_transitions = this.shiftLines(transitions, shiftValue, shiftType);
}
this.removeLines();
// groups lines by energy
transitions.sort((a, b) => { return a.upper_state.energy - b.upper_state.energy; });
let groups = [];
let groups_infos = [];
groups.push([]);
for (let transition of transitions) {
if (groups[groups.length - 1].length < linesInGroup) {
groups[groups.length - 1].push(transition);
} else {
const min_upper_energy = groups[groups.length - 1][0].upper_state.energy;
const max_upper_energy = groups[groups.length - 1][groups[groups.length - 1].length - 1].upper_state.energy;
groups_infos.push([min_upper_energy, max_upper_energy]);
groups.push([transition]);
}
}
// plot lines if any
if (groups[0].length > 0) {
const min_upper_energy = groups[groups.length - 1][0].upper_state.energy;
const max_upper_energy = groups[groups.length - 1][groups[groups.length - 1].length - 1].upper_state.energy;
groups_infos.push([min_upper_energy, max_upper_energy]);
this.spectroUI.setGroupInfos(groups_infos);
this.transitionGroups = groups;
this.linesCount = plotted_transitions.length;
this.plotTransitionGroup(0);
this.spectroUI.showEnergyGroupLines(this.transitionGroups[0]);
this.spectroUI.setLastGroup(this.transitionGroups.length);
}
}
}
/**
* Draws spectral lines on the target graph, if transitions is undefined, uses this.transitions
* This function is redundant with plotLines and its usefulness must be checked
* cf Issue 28 in gitlab
* @param {object} target graph on which lines will be plotted
* @param {array} transitions graph on which lines will be plotted
*/
plotSpectroscopicDataOnGraph(target, transitions) {
if (transitions === undefined)
transitions = this.transitions;
let axis = target.axis;
for (var i = 0; i < transitions.length; ++i) {
let value = transitions[i].obsFrequency;
// rescale to velocity
if (target.datatype == VELOCITY_SHIFT_TYPE) {
value = f2v(transitions[i].obsFrequency * 10 ** 9, FITS_HEADER.restfreq, 0) / 10 ** 3;
}
let id = this.shiftedLineIdPrefix + target.datatype + i;
this.plotLine(axis, value, id, transitions[i], this.lineColor);
}
}
/**
* Plot one transition group on all the graphs registered in this.targets
* @param {number} index index of group to plot (int)
*/
plotTransitionGroup(index) {
this.removeLines();
this.linesCount = this.transitionGroups[index].length;
let transitions = this.transitionGroups[index];
this.spectroUI.setCurrentGroup(index + 1);
for (let target of this.targets) {
for (let i = 0; i < transitions.length; ++i) {
let value = transitions[i].obsFrequency;
// rescale to velocity
if (target.datatype == VELOCITY_SHIFT_TYPE) {
value = f2v(transitions[i].obsFrequency * 10 ** 9, FITS_HEADER.restfreq, 0) / 10 ** 3;
}
let id = this.shiftedLineIdPrefix + target.datatype + i;
this.plotLine(target.axis, value, id, transitions[i], this.lineColor);
}
}
}
/**
* Returns text description of a spectral line
* @param {object} line
* @returns {string}
*/
lineDescription(line) {
let text = line.species.formula + " ";
/*text += "f_obs=" + line.obsFrequency.toFixed(3) + " GHz ";*/
if (line.frequency != null) {
text += "f_rest=" + line.frequency.toFixed(3) + " GHz ";
}
/*if (line.lower_state.energy != null && line.upper_state.energy != null) {
text += "Lower state energy : " + line.lower_state.energy + "; ";
text += "Upper state energy : " + line.upper_state.energy + "; ";
}*/
return text;
}
/**
* Draw one spectral line on the graph
* @param {object} axis plot axis
* @param {floar} xval x coordinate
* @param {string} id id of new dom element
* @param {object} line line object
*/
plotLine(axis, xval, id, line, color) {
var self = this;
axis.addPlotBand({
from: xval,
to: xval,
color: "#FFFFFF",
zIndex: 4,
id: id,
borderWidth: 0.5,
borderColor: color,
label: {
text: self.lineDescription(line),
rotation: 90,
verticalAlign: "top",
y: 90,
x: 5,
style: {
fontWeight: "lighter",
color: "#828282"
}
}
});
}
/**
* Removes all spectral lines from an axis
*/
removeLines() {
if (this.targets !== null) {
for (let target of this.targets) {
for (let i = 0; i < this.linesCount; i++) {
try {
target.axis.removePlotBand(this.shiftedLineIdPrefix + target.datatype + i);
} catch (e) {
console.log(e);
}
}
}
this.linesCount = 0;
}
}
}
export {
SpectroscopyUI,
SpectroscopyQuery,
LinePlotter,
SpectroscopyFormatter
}