Source: public/javascript/modules/olqv_spectro.js

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
}