Source: public/javascript/modules/olqv_spectro.js

import { hashCode, f2v, shift, getLpLineJ, getLpLineK, KToCm } from "./utils.js";
import { FITS_HEADER } from "./fitsheader.js";
import { Constants } from "./constants.js";
import { EventFactory } from './customevents.js';
import {DOMAccessor} from './domelements.js';
import {SpectroApi} from './serverApi.js';

function changeDatabase(spectroq, spectroUI, dbname) {
    spectroq.getMetadata(function(metadata) { spectroUI.initSpeciesSelection(dbname, metadata) });
    spectroUI.intensityElement.value = Constants.DEFAULT_INTENSITY;
    spectroUI.intensityRangeElement.value = Constants.DEFAULT_INTENSITY;
}

/**
 * Changes list of spectral lines displayed in table next to the spectrum.
 * Only a few lines are displayed at a time to avoid list becoming too long.
 *
 * @param {int} newGroupIndex index of displayed lines group
 */
function changeLinesGroup(linePlotter, spectroUI, newGroupIndex) {
    spectroUI.changeGroupAction(newGroupIndex, () => {
        linePlotter.plotTransitionGroup(newGroupIndex);
        spectroUI.showEnergyGroupLines(linePlotter.transitionGroups[newGroupIndex]);
    });
}


/**
 * 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} velocity redshift value (can also be a velocity) (float)
     * @param {number} freqMin minimum frequency (float)
     * @param {number} freqMax maximum frequency (float)
     * @returns {string}
     */
    linesTofile(lines, velocity, 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, velocity, FITS_HEADER.getVeloLsr()).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 linesTable = "lines-table";

        this.toggler = document.getElementById("toggle-lines-search");
        this.togglerArea = document.getElementById("toggler-area");
        this.spectroscopyForm = document.getElementById("spectroscopy-menu");
        this.redshiftElement = DOMAccessor.getRedshiftField();
        this.velocityElement = DOMAccessor.getVelocityField();
        this.dlElement = DOMAccessor.getDlField();
        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.groupsInfos = [];
        this.currentGroup = 0;
        this.referenceFreq = null;
        this._selectedLine = null;
        this._selectedSurface = null;
        this._lines = [];

        // true is database is enable
        this._databases = {};

        // currently selected line in table for luminosity calculation
        this._selectedLine = "";
        this.toggleDatabaseSelector = this.toggleDatabaseSelector.bind(this);
    }

    setSelectedSurface(value){
        this._selectedSurface = value;
    }

    addDatabase(name, enabled){
        this._databases[name] = enabled;
    }

    /**
     * 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";
        }
        document.getElementById(name + "datasourcespan").style.display = status;
    }

    /**
     * Displays line luminosity L'_line value in the dedicated DOM element
     */
    getLineLuminosityValue(lineFrequency) {
        // alert message if luminosity value is not a number whereas velocity and redshift are defined
        if(this._selectedSurface === null){
            return "";
        }else{
            if (isNaN(parseFloat(this.dlElement.value)) &&
            !isNaN(parseFloat(this.redshiftElement.value)) &&
            !isNaN(parseFloat(this.velocityElement.value))) {
            return "Value of DL field is not a number, luminosity can not be calculated";
            } else {
                let result = null;
                if(FITS_HEADER.isSpectrumInK()){
                    result = getLpLineK(this._selectedSurface, parseFloat(this.dlElement.value), 
                    parseFloat(this.redshiftElement.value),FITS_HEADER.cdelt1, FITS_HEADER.cdelt2, 
                               FITS_HEADER.bmin, FITS_HEADER.bmaj, DOMAccessor.getSummedChartCoordinatesArray());    
                }else{
                    result = getLpLineJ(this._selectedSurface, lineFrequency,
                    (this.dlElement.value), parseFloat(this.redshiftElement.value));
                }

                return `${result.toExponential(2)} K.km/s.pc<sup>2</sup>`;
            }
        }

    }

    /**
     * Disables intensity and energy configuration elements
     */
    disableExtraFields() {
        $("#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
     */
    enableExtraFields() {
        $("#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
     * @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 true;//this.toggler.checked;
    }

    /**
     * Hides checkbox to enable/disable spectrosqcopy menu
     */
    hideToggler(){
        this.togglerArea.style.visibility = "hidden";
    }

    /**
     * Shows checkbox to enable/disable spectrosqcopy menu
     */
    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.classList.add("hidden");
    }

    /**
     * Returns the redshift value ( either redshift or velocity ) and the type of returnd value
     * in an object
     * @returns {object}
     */
    /*getShift() {
        try{
            const velocity = this.getVelocity();
            const redshift = this.getRedshift();
            let result = {}
            if (!isNaN(velocity) && velocity < 30000) {
                result["shiftValue"] = velocity;
                result["shiftType"] = Constants.VELOCITY_SHIFT_TYPE;
            } else if (!isNaN(redshift)) {
                result["shiftValue"] = redshift;
                result["shiftType"] = Constants.REDSHIFT_SHIFT_TYPE;
            }
            return result;
        }catch(e){
           throw(e);
        }
    }*/

    /**
     * Display a table containing found spectral lines
     * @param {*} lines
     */
    showEnergyGroupLines(lines) {
        this._lines = lines;        
        this.refreshEnergyGroupLines();
    }

    refreshEnergyGroupLines(){
        const lines = this._lines;
        let self = this;

        // returns the luminosity button
        let getButton = function(){
            // action when button is clicked
            let buttonClick = function(event){
                let tr = event.target.parentNode.parentNode;
                let alltrs = tr.parentNode.children;
                for(let i=0; i < alltrs.length; i++){
                    if(alltrs[i] !== tr){
                        let buttonTd = alltrs[i].cells[2];
                        while (buttonTd.hasChildNodes()) {
                            buttonTd.removeChild(buttonTd.lastChild);
                        }
                        buttonTd.appendChild(getButton());
                    }
                }
                tr.cells[2].innerHTML = self.getLineLuminosityValue(tr.cells[1].textContent);
            };

            let button = document.createElement("button");
            button.textContent = "Compute";
            button.classList.add('btn', 'btn-secondary');
            button.addEventListener('click', buttonClick);
            return button;
        };

        if (lines.length > 0) {
            this.linesTableContainer.classList.remove("hidden");
            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");
                let lower_state = "";

                if(lines[i].lower_state.qns !== undefined){
                    for(let qn of lines[i].lower_state.qns){
                        lower_state = lower_state + qn.name + "=" + qn.value +  " ";
                    }
                    let upper_state = "";
                    for(let qn of lines[i].upper_state.qns){
                        upper_state = upper_state + qn.name + "=" + qn.value + " ";
                    }

                    tr.title = "lower state : " + lower_state + " -  upper state : " + upper_state;
                }

                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);

                let td3 = document.createElement("td");
                td3.appendChild(getButton());

                // line name
                tr.appendChild(td1);
                // line frequency
                tr.appendChild(td2);
                // line luminosity
                tr.appendChild(td3);

                // clicked lines is selected as reference line
                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] !== undefined) {
                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)
     * @param {string} unit unit of returned value, km/s or m/s
     * @returns {number} a float value
     */
    getVelocity(unit="km/s") {
        if(unit !== "km/s" && unit !== "m/s"){
            alert(`Unknown unit ${unit} for getVelocity`);
            return undefined;
        }else{
            if(this.velocityElement.value !== "-" && this.velocityElement.value !== "" ){
                let result = new Number(this.velocityElement.value);
                if (isNaN(result)){
                    alert("Velocity value is not a valid number.");
                    return undefined;
                } else{
                    if(unit === "km/s")
                        return result.valueOf();
                    else if(unit === "m/s")
                        return result.valueOf() * 10**3;
                }
            }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 {
            if(this.velocityElement !== null)
                this.velocityElement.value = value;
        }
    }

    /**
     * Set empty content in velocity field
     */
    clearVelocity() {
        if(this.velocityElement !== null)
            this.velocityElement.value = "";
    }

    /**
     * Returns redshift (undefined if empty)
     * @returns {number} a float value
     */
    getRedshift() {
        if(this.redshiftElement.value !== "-" && this.redshiftElement.value !== ""){
            let result = new Number(this.redshiftElement.value);
            if (isNaN(result)){
                alert("Redshift value is not a valid number.");
                return undefined;
            }else{
                return result.valueOf();
            }
        }else
            return undefined;
            //return this.redshiftElement.value;
    }

    /**
     * Sets redshift value
     * @param {number} value (float)
     */
     setRedshift(value) {
        if (isNaN(parseFloat(value)) && value.trim() !== "") {
            alert("Redshift value must be a float");
        } else {
            if(this.redshiftElement !== null)
                this.redshiftElement.value = value;
        }
    }

    /**
     * Sets empty content in redshift field
     */
    clearRedshift() {
        if(this.redshiftElement !== null)
            this.redshiftElement.value = "";
    }

    /**
     * Returns luminosity distance (undefined if empty)
     * @returns {number} a float value
     */
    getDl() {
        let result = new Number(this.dlElement.value);
        if (isNaN(result))
            throw("DL value is not a valid number.");
        else
            return result;
    }

    /**
     * Sets luminosity distance value
     * @param {number} value (float)
     */
    setDl(value) {
        if(this.dlElement !== null){
            if (value === null) {
                this.dlElement.value = "";
            } else {
                const dl = parseFloat(value);
                if (isNaN(dl)) {
                    //alert("DL value must be a float");
                    this.dlElement.value = "";
                } else {
                    this.dlElement.value = dl;
                }
            }
        }
    }

    /**
     * Set empty content in luminosity distance field
     */
    clearDl() {
        if(this.dlElement !== null)
            this.dlElement.value = "";
    }

    /**
     * Returns value of hubble constant (h0)
     * @returns hubble constant value
     */
    getHubbleCst(){
        let result = new Number(DOMAccessor.getHnotField().value);
        if (isNaN(result))
            throw("Hubble constant value is not a valid number.");
        else
            return result.valueOf();
    }

    /**
     * Returns value of mass density (omega m)
     * @returns hubble constant value
     */
    getMassDensity(){
        let result = new Number(DOMAccessor.getOmegaMField().value);
        if (isNaN(result))
            throw("Mass density value is not a valid number.");
        else
            return result.valueOf();
    }


    /**
     * Returns selected database
     * @returns {string}
     */
    getSelectedDatabase() {
        let result = "";
        for (let i = 0; i < this.databaseElement.length; i++) {
            if (this.databaseElement[i].checked) {
                result = this.databaseElement[i].value;
            }
        }
        return result;
    }

    /**
     * Select a default spectro database. Local database if velolsr is defined
     * or no database if it is undefined
     * @param {*} velolsr 
     */
    selectDefaultDatabase(){
        if(FITS_HEADER.isNENUFAR() && this._databases.rrl === true){
            document.getElementById("rrldatasource").checked = true;
        }else if(this._databases.local === true){
            document.getElementById("localdatasource").checked = true;
        }else{
            document.getElementById("nodatasource").checked = true;
        }

        // disables the extended search interface in all cases
        this.disableExtraFields();
    }

    /**
     * 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 right-menu-box-element";
                    removebutton.onclick = () => {
                        result_area.removeChild(document.getElementById(elementId));
                        if (self.getSelectedSpecies().length === 0) {
                            self.setIntensity(Constants.DEFAULT_INTENSITY);
                        }
                    };

                    let datasetbutton = document.createElement("button");
                    datasetbutton.textContent = "Dataset";
                    datasetbutton.className = "btn btn-secondary  right-menu-box-element";
                    datasetbutton.onclick = () => {
                        datasetbutton.dispatchEvent(EventFactory.getEvent(EventFactory.EVENT_TYPES.getDataset, {
                            bubbles: true,
                            detail: {
                                dataset: selection,
                                db: Constants.SPECTRO_COLLECTIONS[self.getSelectedDatabase()]
                            }
                        })
                    )};

                    let p = document.createElement("p");

                    li.appendChild(span);
                     p.appendChild(removebutton);
                    p.appendChild(datasetbutton);
                    li.appendChild(p);
                    result_area.appendChild(li);
                }

                if (this.getSelectedSpecies().length > 0) {
                    this.setIntensity(this.getIntensityMin());
                }
            },
        });
    }
}

/**
 * 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);

    }

    setTargets(targets){
        this.targets = targets;
    }

    _initEvents() {
        //ENTER on redshift or velocity input fields
        let self = this;
        DOMAccessor.getRedshiftField().addEventListener("keypress", function (event) {
            if (event.key === "Enter") {
                self.refresh();
            }
        });
        DOMAccessor.getVelocityField().addEventListener("keypress", function (event) {
            if (event.key === "Enter") {
                self.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, velocity) {
        let shifted_transitions = transitions.slice();
        for (let line of shifted_transitions) {
            line.obsFrequency = shift(line.frequency, velocity, FITS_HEADER.getVeloLsr());
        }
        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
        if(this.spectroUI.getSelectedDatabase() !== "off"){
            try{
                const velocity = this.spectroUI.getVelocity();
                this.obsFreqMin = obsFreqMin;
                this.obsFreqMax = obsFreqMax;
                this.targets = targets;

                const frequencies = [{
                    min: obsFreqMin, //unshift(obsFreqMin, velocity, FITS_HEADER.getVeloLsr()),
                    max: obsFreqMax //unshift(obsFreqMax, velocity, FITS_HEADER.getVeloLsr())
                }];

                const atomCount = [{
                    min: this.spectroUI.getAtomCountMin(),
                    max: this.spectroUI.getAtomCountMax()
                }];

                let query = new SpectroApi();

                // query data and execute this.plotLines on found spectral lines
                query.getTransitions(this.spectroUI.getSelectedDatabase(), frequencies, atomCount,
                this.spectroUI.getEnergyUp(), this.spectroUI.getSelectedSpecies(),
                this.spectroUI.getIntensity(), this.plotLines);
            }catch(e){
                alert("Error while loading spectral lines : " + e);
            }
        }
    }

    /**
     * Plots transitions on all the graphs registered in this.targets
     * @param {array} transitions 
     */
    plotLines(transitions) {
        this.transitions = transitions;

        try{
            let plotted_transitions = transitions;
            const velocity = this.spectroUI.getVelocity();
            plotted_transitions = this.shiftLines(transitions, velocity);
            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 < Constants.LINES_PER_GROUP) {
                    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);
            }
        }catch(e){
            throw(e);
        }
    }

    /**
     * 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].frequency;
            // rescale to velocity 
            if (target.datatype == Constants.VELOCITY_SHIFT_TYPE) {
                value = f2v(transitions[i].frequency * 10 ** 9, FITS_HEADER.restfreq, this.spectroUI.getVelocity()) / 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);
        const velocity = this.spectroUI.getVelocity("m/s");
        let factor = 1;
        if(velocity === 0 || velocity === undefined ){
            factor =  1 + this.spectroUI.getRedshift() ;
        }

        for (let target of this.targets) {
            for (let i = 0; i < transitions.length; ++i) {
                let value = transitions[i].frequency;

                // rescale to velocity 
                if (target.datatype == Constants.VELOCITY_SHIFT_TYPE) {
                    value = f2v(transitions[i].frequency * 10 ** 9, FITS_HEADER.restfreq * factor, velocity) / 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(4) + " 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,
    LinePlotter,
    SpectroscopyFormatter,
    changeDatabase,
    changeLinesGroup
}