Source: public/javascript/modules/spectrum/olqv_spectrum.js

import {
    shiftFrequencyByZ,
    unshiftFrequencyByZ,
    shiftFrequencyByV,
    unshiftFrequencyByV,
    v2f,
    f2v,
    sumArr,
    unitRescale,
    standardDeviation,
    jyperk
} from "../utils.js";
import { LinePlotter } from '../olqv_spectro.js';
import { dataPaths, URL_ROOT, URL_3D_TO_1D, yafitsTarget, testMode } from '../init.js';
import { ServerApi } from '../serverApi.js'
import { FITS_HEADER } from '../fitsheader.js';
import { Constants } from "../constants.js";
import { sAMPPublisher, setOnHubAvailability } from "../samp_utils.js";
import { DOMAccessor } from "../domelements.js";
import {Slice} from "../olqv_slice.js";
import {
    XDataCompute, FrequencyXData, VelocityXData, VeloLSRXData, WaveXData, WavnXData, AWavXData
} from "./xdata.js";
import {
    YDataCompute, FrequencyYData, VelocityYData, VeloLSRYData, WaveYData, WavnYData, AWavYData
} from "./ydata.js";


import{
    ChartLegend, SitelleLegend, CasaLegend, GildasLegend, MuseLegend, MiriadLegend, MeerkatLegend, NenufarLegend
} from './chartlegend.js';

/**
 * Triggers Highcharts selection event on the given chart
 * @param {Highcharts.chart} chart event target
 * @param {number} xMin minimum selected value on xAxis
 * @param {number} xMax maximum selected value on xAxis
 */
function fireChartSelectionEvent(chart, xMin, xMax){
    Highcharts.fireEvent(chart, 'selection', {
        xAxis: [{
            min: xMin,
            max: xMax
        }],
    });
}

/**
 * Returns a formatted string containing a displayable unit name
 * @param {string} unit the source unit
 * @returns {string} a formatted unit
 */
function displayableBUnit(unit) {
    if(unit === undefined)
            return "";
    else{
        switch (unit) {
            case "Jy/beam":
                return "Jy";

            case "erg/s/cm^2/A/arcsec^2":
                return "erg/s/cm^2/A";

            default:
                return "";
        }
    }
}

/**
 * Returns an object containing the configuration of a point when it will be displayed
 * in Highcharts graph (plot type, colors, line width ...)
 *
 * Radius (5) and color (red) are hard coded for now
 *
 * @param {number} x coordinate on x axis (float)
 * @param {number} y coordinate on y axis (float)
 * @param {boolean} visible  visibility
 * @returns {object} a point object to plot in Hightcharts
 */
function getPoint(x, y, visible) {
    return {
        type: 'scatter',
        name: '',
        showInLegend: false,
        visible: visible,
        zIndex: 1,
        enableMouseTracking: false,
        marker: {
            radius: 8
        },
        data: [{
            x: x,
            y: y,
            color: '#BF0B23'
        }]
    }
}

/**
 * Returns an object containing the configuration of the X axis when it will be displayed
 * in Highcharts graph (plot type, colors, line width ...)
 * @returns {Object} configuration of x axis for Highcharts
 */
function getXAxisConfiguration() {
    let axis = {
        type: 'scatter',
        marker: {
            color: '#1f77b4',
            size: 0,
            radius: 0
        },
        line: {
            color: '#1f77b4',
            width: 1
        },
        connectgaps: 'true',
        hoverinfo: 'x+y',
        xaxis: 'x'
    };
    return axis;
    // commented for now, does not seem useful
    //return JSON.parse(JSON.stringify(axis))
}

/**
 * Returns an event signaling that a slice has been modified
 * data packed inside the event are used in testing context
 * @param {string} type type of the modified slice ( single or summed)
 * @param {object} data spectrum data
 * @returns Event
 */
function get3DSpectrumUpdateEvent(type, data, meta){
    let event = new Event(type);
    if (type === "single"){
        event.freqMin = meta.spectrumValues.freqMin;
        event.freqMax = meta.spectrumValues.freqMax;
        event.jyperk = jyperk(FITS_HEADER.restfreq, FITS_HEADER.bmin, FITS_HEADER.bmaj);
        event.iRA = meta.iRA;
        event.iDEC = meta.iDEC;
        if(meta.ytitle !== undefined){
            let parts = meta.ytitle.split('(');
            if(parts.length > 1){
                event.unit = parts[1].replace(')', '');
            }else{
                event.unit = parts[0];
            }
        }

        event.rmsValue = standardDeviation(data.y);
        event.minValue = Math.min(...data.y);
        event.maxValue = Math.max(...data.y);
    }else if(type === "summed"){
        if(meta.lowerIntegratedValues.flux !== undefined){
            event.fluxValue = meta.lowerIntegratedValues.flux.value;
            event.fluxUnit = meta.lowerIntegratedValues.flux.unit;
        }
    
        if(meta.lowerIntegratedValues.vmin !== undefined){
            event.vminValue = meta.lowerIntegratedValues.vmin.value;
            event.vminUnit = meta.lowerIntegratedValues.vmin.unit;
        }
    
        if(meta.lowerIntegratedValues.vmax !== undefined){
            event.vmaxValue = meta.lowerIntegratedValues.vmax.value;
            event.vmaxUnit = meta.lowerIntegratedValues.vmax.unit;
        }
        event.imin = meta.lowerIntegratedValues.imin;
        event.imax = meta.lowerIntegratedValues.imax;
        if(meta.ytitle !== undefined){
            let parts = meta.ytitle.split('(');
            if(parts.length > 1){
                event.unit = parts[1].replace(')', '');
            }else{
                event.unit = parts[0];
            }
        }
        event.rmsValue = standardDeviation(data.y);

        event.minValue = Math.min(...data.y);
        event.maxValue = Math.max(...data.y);
    }else if(type === "bounds"){
        event.iFreqMin = meta.iFreqMin;
        event.iFreqMax = meta.iFreqMax;
        /*event.freqMin = meta.freqMin;
        event.freqMax = meta.freqMax;*/
        event.velMin = meta.velMin;
        event.velMax = meta.velMax;
    }
    return event;
}

/**
 * Returns an event signaling that a slice has been modified
 * data packed inside the event are used in testing context
 * @param {object} data spectrum data
 * @returns Event
 */
function get1DSpectrumUpdateEvent(data, meta){
    let event = new Event("1d");
    if(meta.ytitle !== undefined){
        let parts = meta.ytitle.split('(');
        if(parts.length > 1){
            event.unit = parts[1].replace(')', '');
        }else{
            event.unit = parts[0];
        }
    }

    event.rmsValue = standardDeviation(data.y);

    event.minValue = meta.minVal;
    event.maxValue = meta.maxVal;
    event.freqMin = meta.freqMin;
    event.freqMax = meta.freqMax;
    event.iRA = 0;
    event.iDEC = 0;
 
    return event;
}


class SpectrumViewer {

    static objectType = "SPECTRUM";

    /**
     * @constructor
     * @param {Object} paths dataPaths object
     * @param {string} containerId id of graph container
     */
    constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
        this.spectrumChart = null;
        this._containerId = containerId;
        this._viewLinker = null;
        this._spectroUI = spectroUI;
        this.linePlotter = null;
        this.spectrumLoadedlisteners = [];

        this._spectrumUnit = displayableBUnit(FITS_HEADER.bunit);

        this._width = width;
        this._heightWidthRatio = heightWidthRatio;
        this._xtitle = "undefined";
        this._ytitle = "undefined";
        this._xMinZoom = null;
        this._xMaxZoom = null;
        this._relFITSFilePath = paths.relFITSFilePath;
        this._sampButton = undefined;
        this._datatype = "";

        this._legendObject = new ChartLegend();
        this._xDataComputer = new XDataCompute()
        this._yDataComputer = new YDataCompute()

        this._initLegendObject();
        this. _initDataComputers();
    }

    _initLegendObject(){
        let strategy = null;
        if (FITS_HEADER.isSITELLE()) {
            strategy = new SitelleLegend();
        } else if (FITS_HEADER.isCASA()) {
            strategy =  new CasaLegend();
        } else if (FITS_HEADER.isGILDAS()) {
            strategy =  new GildasLegend();
        } else if (FITS_HEADER.isMUSE()) {
            strategy =  new MuseLegend();
        } else if (FITS_HEADER.isMIRIAD()) {
            strategy =  new MiriadLegend();
        } else if (FITS_HEADER.isMEERKAT()) {
            strategy =  new MeerkatLegend();
        } else if (FITS_HEADER.isNENUFAR()) {
            strategy =  new NenufarLegend();
        } else{
            throw("Strategy is unknown");
        }

        this._legendObject.setStrategy(strategy);
    }

    _initDataComputers(){
        switch (FITS_HEADER.ctype3) {
            case 'FREQ': 
                this._xDataComputer.setStrategy(new FrequencyXData());
                this._yDataComputer.setStrategy(new FrequencyYData());
                break;

            case 'VRAD':
                this._xDataComputer.setStrategy(new VelocityXData());
                this._yDataComputer.setStrategy(new VelocityYData());
                break;

            case 'VELO-LSR':
                this._xDataComputer.setStrategy(new VeloLSRXData());
                this._yDataComputer.setStrategy(new VeloLSRYData());
                break;

            case 'WAVE':
                this._xDataComputer.setStrategy(new WaveXData());
                this._yDataComputer.setStrategy(new WaveYData());
                break;

            case 'WAVN':
                this._xDataComputer.setStrategy(new WavnXData());
                this._yDataComputer.setStrategy(new WavnYData());
                break;

            case 'AWAV':
                this._xDataComputer.setStrategy(new AWavXData());
                this._yDataComputer.setStrategy(new AWavYData());
                break;

            default:
                throw("ctype3 case not recognized : " + FITS_HEADER.ctype3);
        }        
    }

    /**
     * Returns Y value in spectrum data for a given X. 
     * Values are sorted in descending ordre in spectrumData,
     * Y value is returned as soon as X > spectrumData[i]
     * @param {number} x  X value (float)
     * @param {array} spectrumData an array of [x,y] tuples
     * @returns 
     */
    getYValueAtX(x, spectrumData){
        if(x <= spectrumData[0][0] && x >= spectrumData[spectrumData.length -1][0]){
            for(let i=0; i < spectrumData.length; i++){
                if(x >= spectrumData[i][0])
                    return spectrumData[i][1];
            }
        } else
            throw RangeError(x + " value not found in spectrum data");
    }

    setSpectrumSize(width, ratio){
        this.spectrumChart.setSize(width, ratio);
        this._width = width;
        this._heightWidthRatio = ratio;
    }

    addSpectrumLoadedListener(listener){
        this.spectrumLoadedlisteners.push(listener);
    }

    removeSpectrumLoadedListener(listener){
        for(let i=0; i < this.spectrumLoadedlisteners.length; i++){
            if(this.spectrumLoadedlisteners[i] === listener){
                this.spectrumLoadedlisteners.splice(i, 1);
            }
        }
    }

    _executeSpectrumLoadedListener(event) {
        for (let l of this.spectrumLoadedlisteners) {
            l.spectrumLoaded(event);
        }
    }

    /**
     * @param {ViewLinker} viewLinker ViewLinker object managing interactions between elements
     */
    setViewLinker(viewLinker) {
        this._viewLinker = viewLinker;
    }

    /**
     * Returns the xaxis of the chart and its datatype
     * @returns {Object} an object containing the xaxis and a datatype
     */
    getSpectrumChartXAxis() {
        return {
            axis: this.spectrumChart.xAxis[0],
            datatype: this._datatype
        };
    }

    /**
     * Called when NED table object triggers an event, refreshes lines display
     * 
     * @param {Event} event event that triggered the call
     */
    sourceTableCall(event) {
        if (this.linePlotter != undefined) {
            this.linePlotter.refresh();
        }
    }

    refreshChartLegend() {
        throw new Error("This method must be implemented");
    }

    getExportMenuItems(mode, sampConnected){
        let result = ['downloadPNG'];

        if(mode === "OBSPM"){
            result.push('downloadFits');
        }

        result.push("sendTo1D");

        if(sampConnected){
            result.push("sendWithSamp");
        }

        return result;
    }


    /**
     * Toggles samp button visibility
     * @param {boolean} state status of button visibility
     */
    setSampButtonVisible(state) {
        let menu = { exporting: { buttons: { contextButton: { menuItems: this.getExportMenuItems(yafitsTarget, state) } } } };
        //this can occur before chart is initialized
        if(this.spectrumChart !== null){
            this.spectrumChart.update(menu);
        }
    }

    _showCoordinates(coords){
        throw new Error("This method must be implemented");
    }

    /**
     * Returns integral value of selected area in the spectrum
     * One case for a graph in radial velocity, one for all other cases
     * @param {*} yData
     * @param {*} imin 
     * @param {*} imax 
     * @returns 
     */
    _getSelectedSpectrumValue(yData, imin, imax) {
        let result = 0;
        if (FITS_HEADER.ctype3 === 'VRAD') {
            let copy = (x) => x;
            let arraycopy = yData.map(copy);
            result = sumArr(arraycopy.reverse(), imin, imax, FITS_HEADER.cdelt3prim);
        } else {
            result = sumArr(yData, imin, imax, FITS_HEADER.cdelt3prim);
        }
        return result / unitRescale(this._spectrumUnit);
    }

    /**
     * Returns the channel corresponding to the given input value (frequency or velocity)
     * @param {number} value (float)
     * @returns {float}
     */
    _getCalculatedIndex(value) {
        let result = 0;
        if (FITS_HEADER.ctype3 === 'VRAD') {
            let step1 = (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]) / FITS_HEADER.cdelt3;
            let crval3 = FITS_HEADER.crval3 / (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]);
            result = (value - crval3) * step1 + FITS_HEADER.crpix3 - 1;
        } else if (FITS_HEADER.ctype3 === 'FREQ') {
            // if ctype is FREQ we have to read Constants.DEFAULT_OUTPUT_UNIT['VRAD']*
    
            let vcenter = 0; //SPEED_OF_LIGHT * (FITS_HEADER.crval3 - FITS_HEADER.restfreq) / FITS_HEADER.restfreq;
            let step1 = v2f(value * Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT['VRAD']], FITS_HEADER.restfreq, vcenter);
            let step2 = (step1 - FITS_HEADER.crval3) / FITS_HEADER.cdelt3;
            result = step2 + FITS_HEADER.crpix3 - 1 /*+ FITS_HEADER.naxis3 / 2*/;
        }
    
        if (FITS_HEADER.cdelt3 >= 0) {
            result = FITS_HEADER.naxis3 - result - 1;
        }
    
        if(Math.round(result) < 0){
            console.error("Calculated index can not be a negative value : " + result);
        } else
            return Math.round(result);
    }

    /**
     * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
     * The formula changes according to the type of data on x axis (CTYPE3)
     *
     * @param {number} rlen number of points on x axis (int)
     * @returns {array} an array of x values
     */
    _getXData(rlen) {
        throw new Error("This method must be implemented");
    }

    /**
     * Returns an array of ydata from the data passed in parameter.
     * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
     * and if CDELT3 < 0 in case of a radial velocity
     * It is rescaled in case of Sitelle data.
     * It is returned unchanged in any other case
     *
     * @param {array} data 
     * @returns {array}
     */
    _getYData(data) {
        throw new Error("This method must be implemented");
    }


}

/**
 * A class displaying a spectrum, using the Highcharts library
 * 
 * @property {Object} spectrumChart a highchart chart object
 */
class SingleSpectrumViewer extends SpectrumViewer {
    /**
     * @constructor
     * @param {Object} paths dataPaths object
     * @param {string} containerId id of graph container
     */
    constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
        super(paths, containerId, width, heightWidthRatio, spectroUI);
        this.iRA = undefined;
        this._iDEC = undefined;
        this._ifrequencyMarker = 0;
        this.toptitle = "undefined";
        this._datatype = "frequency";
        this._computeSliceIndex = this._computeSliceIndex.bind(this);
        this.refreshChartLegend();
    }


    /**
    Returns index of slice to be displayed
    plotData : spectrum
    x : x position clicked on graph
    */
    /**
     * Returns index of slice to be displayed when spectrum is clicked
     * @param {Object} plotData object containing arrays of x and y values of the graph
     * @param {number} x x position clicked on graph (float)
     * @returns 
     */
    _computeSliceIndex(plotData, x) {
        var rlen = plotData.x.length;
        switch (FITS_HEADER.ctype3) {
            case 'FREQ':
                if (FITS_HEADER.cdelt3 > 0) {
                    var forigin = plotData.x[rlen - 1];
                    var deltaf = plotData.x[0] - plotData.x[1];
                } else {
                    var forigin = plotData.x[0];
                    var deltaf = plotData.x[1] - plotData.x[0];
                }
                break;

            case 'VRAD':
            case 'VELO-LSR':
            case 'WAVE':
            case 'WAVN':
            case 'AWAV':
                if (FITS_HEADER.cdelt3 > 0) {
                    var forigin = plotData.x[0];
                    var deltaf = plotData.x[1] - plotData.x[0];
                } else {
                    var forigin = plotData.x[rlen - 1];
                    var deltaf = plotData.x[0] - plotData.x[1];
                }
                break;

            default:
                console.log("This should not happen");
        }

        // phys2Index
        return Math.round((x - forigin) / deltaf);
    }

    /**
     * Called when NED table object triggers an event, refreshes lines display
     * 
     * @param {Event} event event that triggered the call
     */
    sourceTableCall(event) {
        this.refresh();
    }

    /**
     * Sets the title of the graph, x and y axis
     * the format of the title depends on the type o displayed data.
     * currently considered are : sitelle, casa, muse, gildas, miriad
     * 
     * An alert is displayed in any other case
     */
    refreshChartLegend() {
        this._legendObject.defineSpectrumLegend(this);
        if(this.spectrumChart !== null){
            this.spectrumChart.xAxis[0].axisTitle.attr({
                text: this._xtitle
            });
        }
    }

    _showCoordinates(coords){
        DOMAccessor.getSingleChartCoordinates().innerText = "@Pixel x=" + Math.round(coords[0]) + " y=" + Math.round(coords[1]);
    }

    /**
     * Sets the index of selected frequency value on graph
     * @param {number} i index of selected frequency on graph (int)
     */
    setFrequencyMarker(i) {
        console.log("setFrequencyMarker: entering.");
        switch (FITS_HEADER.ctype3) {
            case 'FREQ':
                if (FITS_HEADER.cdelt3 > 0) {
                    this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
                } else {
                    this._ifrequencyMarker = i;
                }
                break;

            case 'VRAD':
                if (FITS_HEADER.cdelt3 > 0) {
                    this._ifrequencyMarker = i;
                } else {
                    this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
                }
                break;

            // equivalent to VRAD
            case 'VELO-LSR':
                if (FITS_HEADER.cdelt3 > 0) {
                    this._ifrequencyMarker = i;
                } else {
                    this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
                }
                break;

            case 'WAVE':
            case 'WAVN':
            case 'AWAV':
                this._ifrequencyMarker = i;
                break;

            default:
                console.log("This should not happen");
                break;
        }
        console.log("setFrequencyMarker: exiting.");
    }


    /**
     * Creates and returns a Highcharts chart
     * @param {Object} plotData Data plotted in graph
     * @param {string} xtitle x axis title
     * @param {string} ytitle  y axis title
     * @returns {chart} 
     */
    _getChart(plotData, xtitle, ytitle) {
        let self = this;
        let kpj = FITS_HEADER.kelvinPerJansky();
        let spectrumData = [];
        for (let i = 0; i < plotData.x.length; i++) {
            spectrumData.push([plotData.x[i], plotData.y[i]]);
        }
        let container = document.getElementById(this._containerId);

        return Highcharts.chart(container, {
            title: {
                text: ''
            },
            chart: {
                type: 'line',
                width: self._width,
                height: self._heightWidthRatio,
                animation: false,
                zoomType: 'xz',
                panning: true,
                panKey: 'shift',
                responsive: {  
                    rules: [{  
                      condition: {  
                        maxWidth: self._width,
                        maxHeight: self._heightWidthRatio
                      },  
                      chartOptions: {
                        xAxis: {
                            labels: {
                                formatter: function () {
                                    return this.value.charAt(0);
                                }
                            }
                        },
                        yAxis: {
                            labels: {
                                align: 'left',
                                x: 0,
                                y: -5
                            },
                            title: {
                                text: null
                            }
                        }
                    }
                    }]  
                },
                events: {
                    render: function (event) {
                        // hides zoom button when it is displayed
                        // we only use zoom button defined in yafits UI
                        if (this.resetZoomButton !== undefined) {
                            this.resetZoomButton.hide();
                        }
                    },
                    load: function (event) {
                        // graph is loaded
                        DOMAccessor.showLoaderAction(false);
                    },

                    click: function (event) {
                        let sliceIndex = self._computeSliceIndex(plotData, event.xAxis[0].value);
                        // Display slice at index sliceIndex
                        if (self._viewLinker !== null) {
                            self._viewLinker.getAndPlotSingleSlice(sliceIndex);
                            self._viewLinker.setFluxDensityInPopup(event.yAxis[0].value, SpectrumViewer.objectType);
                        }
                        const yvalue = self.getYValueAtX(event.xAxis[0].value, spectrumData);
                        this.series[1].update(getPoint(event.xAxis[0].value, yvalue, true));
                    }
                }

            },
            boost: {
                useGPUTranslations: true,
                usePreAllocated: true
            },
            xAxis: {
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                title: {
                    text: xtitle
                },
                crosshair: true,
                reversed: true,
                maxPadding : 0, 
                endOnTick : false,
                minPadding : 0, 
                startOnTick : false,
                events: {
                    // called when boudaries of spectrum are modified
                    setExtremes: function (event) {
                        if ((event.min === undefined || event.max === undefined) && self._viewLinker !== null) {
                            self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(
                            self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMin,
                            self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMax);
                        } else {
                            let restfreq = FITS_HEADER.restfreq
                            if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s")  === undefined ){
                                if(self._spectroUI.getRedshift() !== undefined){
                                    restfreq = restfreq * (1 + self._spectroUI.getRedshift());
                                }                                    
                            }

                            let minval, maxval;

                            //velocity is undefined if a z value has been entered
                            if( self._spectroUI.getVelocity("m/s")  === undefined ){
                                minval = Math.round(f2v(event.min * 1e9, restfreq, 0) / 1e3);
                                maxval = Math.round(f2v(event.max * 1e9, restfreq, 0) / 1e3); 
                            }else{
                                minval = Math.round(f2v(event.min * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);
                                maxval = Math.round(f2v(event.max * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);   
                            }

                            //exchange min/max if min > max
                            if (minval > maxval) {
                                let tmp = minval;
                                minval = maxval;
                                maxval = tmp;
                            }

                            if (self._viewLinker !== null) {
                                self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(minval, maxval);
                            }
                        }
                    },
                }
            },
            yAxis: {
                lineColor: '#FFFFFF',
                gridLineWidth: 1,
                lineWidth: 1,
                opposite: true,
                title: {
                    text: ytitle
                },
                labels: {
                    // returns ticks to be displayed on Y axis
                    formatter: function () {
                        let label = '';
                        // value already in K
                        if (FITS_HEADER.isSpectrumInK()) {
                            label = 'K';
                        }
                        // result can be NaN if _bmin/_bmaj not available
                        // then nothing to display, else value is converted in K
                        else if (!isNaN(kpj)) {
                            label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
                        }

                        return Number(this.value).toExponential(2) + label;
                    }
                }
            },
            plotOptions: {
                series: {
                    cursor: 'pointer',
                    step: 'center',
                    color: Constants.PLOT_DEFAULT_COLOR,
                    animation: {
                        duration: 0
                    },
                    lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
                    events: {
                        click: function (event) {
                            //console.clear();
                            let sliceIndex = self._computeSliceIndex(plotData, event.point.x);
                            DOMAccessor.setSliceChannel("Chan#" + sliceIndex);
                            const yvalue = self.getYValueAtX(event.point.x, spectrumData);
                            this.chart.series[1].update(getPoint(event.point.x, yvalue, true));
                            if (this._viewLinker !== null) {
                                // Display slice at index sliceIndex
                                self._viewLinker.setFluxDensityInPopup(event.point.y, SpectrumViewer.objectType);
                                self._viewLinker.getAndPlotSingleSlice(sliceIndex);
                            }
                        }
                    }
                },
                marker: {
                    radius: 0
                }
            },
            exporting: {
                menuItemDefinitions: {
                    // Custom definition
                    downloadFits: {
                        onclick: function () {
                            window.open(URL_ROOT + dataPaths.spectrum, '_blank');
                        },
                        text: 'Download FITS file'
                    },
                    sendWithSamp: {
                        onclick: function (event) {
                            sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
                            event.stopPropagation();
                        },
                        text: 'Send with SAMP'
                    },
                    sendTo1D: {
                        onclick: function (event) {
                            window.open(URL_3D_TO_1D + dataPaths.spectrum, '_blank');
                        },
                        text: 'Open in 1D viewer'
                    }
                },
                buttons: {
                    contextButton: {
                        menuItems: self.getExportMenuItems(yafitsTarget, false)
                    }
                }
            },
            tooltip: {
                // displayed when the mouse if above the graph
                formatter: function () {
                    // get channel number
                    let sliceIndex = self._computeSliceIndex(plotData, this.x);
                    let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
                    if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
                        label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
                    }
                    return " Chan#" + sliceIndex + " " + label;
                }
            },
            series: [{
                // unlimited number of points when zooming
                cropThreshold: Infinity,
                showInLegend: false,
                data: spectrumData,
                zIndex: 0,
                marker: {
                    radius: 0
                },
            },
            //series of frequency markers, must not be empty to create it, this point is hidden
            getPoint(0, 0, false)],
        });
    }

    /**
     * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
     * The formula changes according to the type of data on x axis (CTYPE3)
     *
     * @param {number} rlen number of points on x axis (int)
     * @returns {array} an array of x values
     */
    _getXData(rlen) {
        return this._xDataComputer.computeSpectrum(rlen, this._spectroUI.getVelocity("m/s"), this._spectroUI.getRedshift());
    }

    /**
     * Returns an array of ydata from the data passed in parameter.
     * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
     * and if CDELT3 < 0 in case of a radial velocity
     * It is rescaled in case of Sitelle data.
     * It is returned unchanged in any other case
     *
     * @param {array} data 
     * @returns {array}
     */
    _getYData(data) {
        let result = this._yDataComputer.computeSpectrum(data);

        if (FITS_HEADER.isSITELLE()) {
            if (FITS_HEADER.cdelt1 && FITS_HEADER.cdelt2) {
                let pixel2arcsec = Math.abs(3600 * 3600 * FITS_HEADER.cdelt1 * FITS_HEADER.cdelt2);
                let temparr = result.map(function (x) {
                    return x * unitRescale(FITS_HEADER.bunit) / pixel2arcsec
                });
                result = temparr;
            }
        }

        return result;
    }

    /**
     * Calls createFits function of server to create a fits file corresponding to iRa/iDec
     * Path of created file is stored in dataPaths.spectrum
     *
     * createFits parameters are relFITSFilePath, iRA, iDEC
     *
     * @param {number} iRA  index of selected RA value (int)
     * @param {number} iDEC index of selected DEC value (int)
     */
     _createFitsFile(iRA, iDEC) {
        if (FITS_HEADER.ctype3 === "FREQ" || FITS_HEADER.ctype3 === "VRAD") {
            let apiQuery = new ServerApi();
            apiQuery.createFitsFile(iRA, iDEC, this._relFITSFilePath, (resp)=>{
                let response = JSON.parse(resp);
                dataPaths.spectrum = response.result;
            });
        }
    }

    refresh(){
        this.plot(this._iRA, this._iDEC, undefined);
        this.refreshChartLegend();
    }

    /**
     * Calls the getSpectrum function of the server to get the spectrum data and plot them
     *
     * getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
     *
     * Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
     * if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
     *
     * @param {number} iRA
     * @param {number} iDEC
     * @param {callbackFucntion} cb a function called when data have been returned from server
     */
    plot(iRA, iDEC, cb) {
        this._iRA = iRA;
        this._iDEC = iDEC;
        let self = this;
        if (typeof iRA === 'undefined') {
            iRA = Math.floor(FITS_HEADER.naxis1 / 2);
        }
        if (typeof iDEC === 'undefined') {
            iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
        }

        let queryApi = new ServerApi();
        queryApi.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
            let plotData = getXAxisConfiguration();
            let jsonTextContent =  JSON.stringify(resp);
            let data;
            // NaN values are replaced by null if they exist
            if(jsonTextContent.includes("NaN")){
                let newResp = JSON.stringify(resp).replace(/\bNaN\b/g, "null");
                let test = JSON.parse(newResp);
                data = JSON.parse(test.data);
            }else{
                data = resp.data;
            }

            plotData.x = self._getXData(data.result.length); // abscissa ( frequency, wavelength, velocity, ...);
            plotData.xaxis = "x";

            plotData.y = self._getYData(data.result);
            self.spectrumChart = self._getChart(plotData, self._xtitle, self._ytitle);

            // pixel coordinates above chart
            self._showCoordinates([iRA, iDEC]);
            self._createFitsFile(iRA, iDEC);

            if(testMode){
                let meta = {iRA : iRA, 
                    iDEC : iDEC, 
                    ytitle : self._ytitle, 
                    spectrumValues : {
                        freqMin : Math.min(...plotData.x), 
                        freqMax : Math.max(...plotData.x),
                        freqcenter : plotData.x[Math.floor(FITS_HEADER.naxis3 / 2)],
                    }
                };
                self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("single",plotData, meta));
            }


            // callback function called at the end of loading process
            // typical use is restore previous graph limits
            if (cb !== undefined) {
                cb();
            }

            if (self._viewLinker !== null) {
                // set value of flux density in popup, should me moved out of this function
                self._viewLinker.setFluxDensityInPopup(plotData.y[self._ifrequencyMarker], Slice.objectType);
                if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
                    self._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
                        self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
                        [self._viewLinker.summedPixelsSpectrumViewer.getSpectrumChartXAxis(), self.getSpectrumChartXAxis()]);
                }
            }
        }); 
    };
}


/**
 * A class displaying a spectrum, using the Highcharts library
 * 
 * @property {Object} spectrumChart a highchart chart object
 */
class SingleSpectrumViewer1D extends SingleSpectrumViewer {
    constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
        super(paths, containerId, width, heightWidthRatio, spectroUI);
        this.detailChart;
        this._detailData = [];

        // min x value in detail chart
        this._xDetailMin = null;
        // max x value in detail chart
        this._xDetailMax = null;

        this._shiftMode = null;

        //xMin for manual user selection, redshift is applied
        this._xSelectionMin = null;
        //xMax for manual user selection, redshift is applied
        this._xSelectionMax = null;
        this.plottedData;
        // draw lines on master chart
        this.linePlotter = null;
        // draw lines on detailed chart
        this.detailLinePlotter = null;
        this._masterYData = [];
        // reference to last lines drawer called
        // this is used by the navigation buttons in 
        // the web UI
        this.activePlotter = null;
        //this.initialized = false;
        this.isRefreshable = true;
        this._isInit = true;

    }

    /**
     * Resets zoom of detail chart to its initial value by triggering a selection event on the chart
     * with this._xDetailMin and this._xDetailMax values
     * Master chart is fixed and can not be zoomed
     */
    resetZoom(){
        Highcharts.fireEvent(this.detailChart, 'selection', {
            xAxis: [{
                min: this._xDetailMin,
                max: this._xDetailMax
            }],
        });
    }

    /**
     * Called when NED table object triggers an event, refreshes lines display
     * 
     * @param {Event} event event that triggered the call
     */
    sourceTableCall(event) {
        this._shiftMode = "z";
        this.refresh("z");
    }

    /**
     * Returns data in the min/max interval from this._detailData
     * @param {number} min minimum value in interval (float)
     * @param {number} max maximum value in interval (float)
     * @returns array
     */
    _getIntervalData(dataArray, min, max){
        let result = [];
        dataArray.forEach(point => {
            if (point[0] > min && point[0] < max) {
                result.push([point[0], point[1]]);
            }
        });
        return result;
    }

    setDetailedSpectrumSize(width, ratio){
        this.detailChart.setSize(width, ratio);
        this._width = width;
        this._heightWidthRatio = ratio;
    }

    /**
     * Creates the detailed chart zooming content from master chart
     * @param {string} xtitle X axis title
     * @param {string} ytitle Y axis title
     */
    _getDetailChart(srcData){
        let self = this;
        let kpj = FITS_HEADER.kelvinPerJansky();
        this.detailChart = Highcharts.chart('detail-container', {
            chart: {
                type : "line",
                animation : false,
                width: self._width,
                height: self._heightWidthRatio,
                zoomType: 'x',
                panning: true,
                panKey: 'shift',
                /*responsive: {  
                    rules: [{  
                      condition: {  
                        maxWidth: self._width,
                        maxHeight: self._heightWidthRatio
                      },  
                      chartOptions: {
                        xAxis: {
                            labels: {
                                formatter: function () {
                                    return this.value.charAt(0);
                                }
                            }
                        },
                        yAxis: {
                            labels: {
                                align: 'left',
                                x: 0,
                                y: -5
                            },
                            title: {
                                text: null
                            }
                        }
                    }
                    }]  
                },*/
                events : {
                    selection: function (event) {
                        if(self.isRefreshable){
                            var extremesObject = event.xAxis[0],
                            min = extremesObject.min,
                            max = extremesObject.max;

                            this.xAxis[0].setExtremes(min, max);
                            self.refreshMasterBands(min, max);
                            // spectral lines
                            self.detailChartLines();
                            self.activePlotter = self.detailLinePlotter;
                        }else{
                            alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
                        }

                        return false;
                    },
                    pan : function(event){
                        self.refreshMasterBands(event.target.xAxis[0].min, event.target.xAxis[0].max);
                    }
                },
            },
            credits: {
                enabled: false
            },
            title: {
                text: '',

            },
            tooltip: {
                // displayed when the mouse if above the graph
                formatter: function () {
                    // get channel number
                    let sliceIndex = self._computeSliceIndex(srcData, this.x);
                    let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
                    return " Chan#" + sliceIndex + " " + label;
                }
            },
            xAxis: {
                gridLineWidth: 1,
                type : "line",
                lineColor: '#FFFFFF',
                title: {
                    text: self._xtitle
                },
                crosshair: true,
                reversed: false,
                maxPadding : 0, 
                endOnTick : false,
                minPadding : 0, 
                startOnTick : false
            },
            yAxis: {
                lineColor: '#FFFFFF',
                gridLineWidth: 1,
                lineWidth: 1,
                opposite: true,
                title: {
                    text: self._ytitle
                },
                labels: {
                    // returns ticks to be displayed on Y axis
                    formatter: function () {
                        let label = '';
                        // value already in K
                        if (FITS_HEADER.isSpectrumInK()) {
                            label = 'K';
                        }
                        // result can be NaN if _bmin/_bmaj not available
                        // then nothing to display, else value is converted in K
                        else if (!isNaN(kpj)) {
                            label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
                        }

                        return Number(this.value).toExponential(2) + label;
                    }
                }
            },
            legend: {
                enabled: false
            },
            exporting: {
                menuItemDefinitions: {
                },
                buttons: {
                    contextButton: {
                        menuItems: ['downloadPNG']
                    }
                }
            },
            plotOptions: {
                series: {
                    cursor: 'pointer',
                    step: 'center',
                    color: Constants.PLOT_DEFAULT_COLOR,
                    lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
                    animation: {
                        duration: 0
                      }
                },
                marker: {
                    radius: 0
                }
            },
            series: [{
                marker: {
                    radius: 0
                }
            }],
        });
    }


    /**
     * Shows the selected interval on the master graph. 
     * Area is coloured between [xAxis[0], xMin] and [xMax, xAxis[xAxis.length -1]]
     * @param {number} xMin minimum value seelcted by user on X axis
     * @param {number} xMax maximum value seelcted by user on X axis
     */
    refreshMasterBands(xMin, xMax){
        if(this.spectrumChart !== null){
            if(this._shiftMode === 'z'){
                this._xSelectionMin = unshiftFrequencyByZ(xMin, this._spectroUI.getRedshift());
                this._xSelectionMax = unshiftFrequencyByZ(xMax, this._spectroUI.getRedshift());
            }else if(this._shiftMode === 'v'){
                this._xSelectionMin = unshiftFrequencyByV(xMin, this._spectroUI.getVelocity());
                this._xSelectionMax = unshiftFrequencyByV(xMax, this._spectroUI.getVelocity());
            }

            // move the plot bands to reflect the new detail span
            this.spectrumChart.xAxis[0].removePlotBand('mask');
            this.spectrumChart.xAxis[0].addPlotBand({
                id: 'mask',
                from: xMin,
                to: xMax,
                color: 'rgba(51, 153, 255, 0.2)'
            });
        }
    }

    /**
     * Returns the xaxis of the chart and its datatype
     * @returns {Object} an object containing the xaxis and a datatype
     */
     getDetailedSpectrumChartXAxis() {
        return {
            axis: this.detailChart.xAxis[0],
            datatype: this._datatype
        };
    }

    _getAxes(){
        return [{
            axis: this.spectrumChart.xAxis[0],
            datatype: this._datatype
        },{
            axis: this.detailChart.xAxis[0],
            datatype: this._datatype
        }
        ]; 
    }

    masterChartLines(){ 
        // if not disabled
        if (this._spectroUI.getSelectedDatabase() !== "off") {
            if (this.linePlotter === null) {
                this.linePlotter = new LinePlotter(this._spectroUI);
            }
    
            const graphs = this._getAxes();

            try {
                this.linePlotter.loadAndPlotLines(this._xDetailMin,
                                                  this._xDetailMax,
                                                  graphs);
            } catch (e) {
                console.log(e);
                alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
            }
        }

        //}
    }

    detailChartLines(){
        // spectral lines
        //if (this._spectroUI.isEnabled()) {
        if (this.detailLinePlotter === null) {
            this.detailLinePlotter = new LinePlotter(this._spectroUI);
        }

        const graphs = this._getAxes();

        try {
            this.detailLinePlotter.loadAndPlotLines(this.detailChart.xAxis[0].getExtremes().min,
            this.detailChart.xAxis[0].getExtremes().max,
            graphs);
        } catch (e) {
            console.log(e);
            alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
        }
        //}
    }

    /**
      * Creates and returns a Highcharts chart
      * @param {Object} spectrumData Data plotted in graph
      * @param {string} xtitle x axis title
      * @param {string} ytitle  y axis title
      * @returns {chart} 
      */
    _getChart(srcData, spectrumData) {
        let self = this;
        let kpj = FITS_HEADER.kelvinPerJansky();
        let result = Highcharts.chart("master-container", {
            title: {
                text: ''
            },
            chart: {
                type: 'line',
                width: self._width,
                height: self._heightWidthRatio,
                animation: false,
                zoomType: 'x',
                panning: true,
                panKey: 'shift',
                /*responsive: {  
                    rules: [{  
                      condition: {  
                        maxWidth: self._width,
                        maxHeight: self._heightWidthRatio
                      },  
                      chartOptions: {
                        xAxis: {
                            labels: {
                                formatter: function () {
                                    return this.value.charAt(0);
                                }
                            }
                        },
                        yAxis: {
                            labels: {
                                align: 'left',
                                x: 0,
                                y: -5
                            },
                            title: {
                                text: null
                            }
                        }
                    }
                    }]  
                },*/
                events: {
                    load: function (event) {
                        // graph is loaded
                        //result.series[0].setVisible(false);
                        let meta = {
                            yTitle: self._ytitle,
                            freqMin : this.xAxis[0].min,
                            freqMax : this.xAxis[0].max,
                            minVal : this.series[0].dataMin,
                            maxVal : this.series[0].dataMax,
                        }
                        let t = get1DSpectrumUpdateEvent(srcData, meta);
                        self._executeSpectrumLoadedListener(get1DSpectrumUpdateEvent(srcData, meta));
                        //DOMAccessor.showLoaderAction(true);
                        DOMAccessor.markLoadingDone();
                    },
                    selection: function (event) {
                        if(self.isRefreshable) {
                            var extremesObject = event.xAxis[0],
                            min = extremesObject.min,
                            max = extremesObject.max,
                            detailData = self._getIntervalData(spectrumData, min, max);

                            // show selected interval
                            self.refreshMasterBands(min ,max);

                            // set data on detailed chart and refresh x axis limits
                            self.detailChart.series[0].setData(detailData, true, false, false);
                            self.detailChart.xAxis[0].setExtremes(min, max);

                            // save global selection
                            self._detailData = detailData;
                            self._xDetailMin = min;
                            self._xDetailMax = max;

                            self._xSelectionMin = min;
                            self._xSelectionMax = max;
                            
                            // spectral lines
                            self.masterChartLines();
                            self.activePlotter = self.linePlotter;
                        }else{
                            alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
                        }

                        return false;
                    }
                }

            },
            boost: {
                useGPUTranslations: true,
                usePreAllocated: true
            },
            xAxis: {
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                title: {
                    text: self._xtitle
                },
                crosshair: true,
                reversed: false,
                maxPadding : 0, 
                endOnTick : false,
                minPadding : 0, 
                startOnTick : false
            },
            yAxis: {
                lineColor: '#FFFFFF',
                gridLineWidth: 1,
                lineWidth: 1,
                opposite: true,
                title: {
                    text: self._ytitle
                },
                labels: {
                    // returns ticks to be displayed on Y axis
                    formatter: function () {
                        let label = '';
                        // value already in K
                        if (FITS_HEADER.isSpectrumInK()) {
                            label = 'K';
                        }
                        // result can be NaN if _bmin/_bmaj not available
                        // then nothing to display, else value is converted in K
                        else if (!isNaN(kpj)) {
                            label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
                        }

                        return Number(this.value).toExponential(2) + label;
                    }
                }
            },
            plotOptions: {
                series: {
                    cursor: 'pointer',
                    step: 'center',
                    color: Constants.PLOT_DEFAULT_COLOR,
                    animation: {
                        duration: 0
                    },
                    lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
                },
                marker: {
                    radius: 0
                }
            },
            tooltip: {
                // displayed when the mouse if above the graph
                formatter: function () {
                    // get channel number
                    let sliceIndex = self._computeSliceIndex(srcData, this.x);
                    let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
                    if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
                        label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
                    }
                    return " Chan#" + sliceIndex + " " + label;
                }
            },
            exporting: {
                menuItemDefinitions: {
                    // Custom definition
                    downloadFits: {
                        onclick: function () {
                            window.open(URL_ROOT + dataPaths.spectrum, '_blank');
                        },
                        text: 'Download FITS file'
                    },
                    sendWithSamp: {
                        onclick: function (event) {
                            sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
                            event.stopPropagation();
                        },
                        text: 'Send with SAMP'
                    },
                },
                buttons: {
                    contextButton: {
                        menuItems:  self.getExportMenuItems(yafitsTarget, false)
                    }
                }
            },
            series: [{
                // unlimited number of points when zooming
                cropThreshold: Infinity,
                showInLegend: false,
                data: self.plottedData,
                zIndex: 0,
                marker: {
                    radius: 0
                }

            },
            //series of frequency markers, must not be empty to create it, this point is hidden
            getPoint(0, 0, false)],
        }, function (chart) { 
            // this is called when spectrum is exported as an image
            // the detailed chart must not be recreated then
            //if(!self.initialized){
            self._getDetailChart(srcData);
            //}                
            //self.initialized = true;
        });
        return result;
    }

    /**
     * Refresh both charts display
     */
    refresh(mode = null){
        this._shiftMode = mode;
        // set data on detailed chart and refresh x axis limits
        this.plot(this._iRA, this._iDEC, ()=>{
            let xMin = this._xSelectionMin;
            let xMax = this._xSelectionMax;
            this._xDetailMin = xMin;
            this._xDetailMax = xMax;
            if(this._shiftMode === "z"){
                xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
                xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
            }else if(this._shiftMode === "v"){
                xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
                xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
            }

            this.refreshChartLegend();
            this.detailChart.series[0].setData(this.plottedData, true, false, false);            
            this.detailChart.xAxis[0].setExtremes(xMin, xMax);
            //this.detailChart.xAxis[0].setExtremes(this.detailChart.xAxis[0].getExtremes().min, this.detailChart.xAxis[0].getExtremes().max);
            
            if (this.linePlotter !== null) {
                this.linePlotter.setTargets(this._getAxes());
                this.masterChartLines();
                this.detailChartLines();
            }
            this.detailChart.xAxis[0].axisTitle.attr({
                text: this._xtitle
            });
        });        
    }

    /**
     * Calls the getSpectrum function of the server to get the spectrum data and plot them
     * 
     * getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
     * 
     * Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
     * if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
     * 
     * @param {number} iRA 
     * @param {number} iDEC 
     * @param {callbackFucntion} cb a function called when data have been returned from server
     */
     plot(iRA, iDEC, cb) {
        let self = this;
        this._iRA = iRA;
        this._iDEC = iDEC;
        if (typeof iRA === 'undefined') {
            iRA = Math.floor(FITS_HEADER.naxis1 / 2);
        }
        if (typeof iDEC === 'undefined') {
            iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
        }

        this.refreshChartLegend();

        DOMAccessor.showLoaderAction(true);
        let qp = new ServerApi();
        qp.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
            let content = resp;
            let test = JSON.stringify(resp);
            let data;
            // fix for cubes containing NaN
            if(test.includes("NaN")){
                let newResp = test.replace(/\bNaN\b/g, "null");
                content = JSON.parse(newResp);
                //for some reason the data element is still
                //a string after data have been parsed above
                data = JSON.parse(content.data);
            }else{
                data = resp.data;
            }
            let plotData = getXAxisConfiguration();
            // in a 1D spectra, getXData will always return data sorted in reverse order
            plotData.x = self._getXData(data["result"].length).reverse();
            plotData.xaxis = "x";
            plotData.y = self._getYData(data["result"]).reverse();
            self._masterYData = plotData.y;
            let spectrumData = [];
            for (let i = 0; i < plotData.x.length; i++) {
                spectrumData.push([plotData.x[i], plotData.y[i]]);
            }
    
            this.plottedData = spectrumData;

            self.spectrumChart = self._getChart(plotData, spectrumData);
            if(self._xSelectionMin !== null){
                let xMin = this._xSelectionMin;
                let xMax= this._xSelectionMax;
                if(this._shiftMode === "z"){                    
                    xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
                    xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
                }else if(this._shiftMode === "v"){
                    xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
                    xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
                }
                self.refreshMasterBands(xMin, xMax);
            }            

            self._createFitsFile(iRA, iDEC);
            if(testMode && self._isInit){
                self._isInit = false;
            }

            // callback function called at the end of loading process
            // typical use is restore previous graph limits
            if (cb !== undefined) {
                cb();
            }
            DOMAccessor.showLoaderAction(false);
        });
    };
}


/**
 * A class displaying an averaged spectrum, using the Highcharts library
 * Initial interval selection is defined in _getInitialSelectionRange
 * 
 * @property {Object} summedPixelsSpectrumChart a highchart chart object
 * @property {LinePlotter} linePlotter an object drawing spectral lines on the chart
 */
class SummedPixelsSpectrumViewer extends SpectrumViewer {
    /**
     * 
     * @param {*} paths 
     * @param {*} containerId 
     * @param {*} viewLinker 
     */
    constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
        super(paths, containerId, width, heightWidthRatio, spectroUI);
        this._iRA0 = null;
        this._iRA1 = null;
        this._iDEC0 = null;
        this._iDEC1 = null;

        this._toptitle_unit = "";
        this._flux_unit = "";
        this._averageSpectrum = null;        

        this._vmin = null;
        this._vmax = null;

        this._isInit = true;

        // Interval of values selected by default of the graph
        // Those are the indexes of the values to select not values themselves
        // If they are not null they will bypass the values coming from the selection event
        this.defaultIndexMax = null;

        const freqs = this._getInitialSelectionRange();
        this.defaultIndexMin = freqs[0];
        this.defaultIndexMax = freqs[1];

        this._summedData = {
            x: [],
        };

        this._datatype = "velocity";

        // surface under the chart in area selected by user, Jy/km/s
        //this._selectedSurface = 0;
        this.refreshChartLegend();
    }

    get vmin() {
        return this._vmin;
    }

    get vmax() {
        return this._vmax;
    }

    set vmin(vmin) {
        this._vmin = vmin;
    }

    set vmax(vmax) {
        this._vmax = vmax;
    }

    get summedData() {
        return this._summedData;
    }


    /**
     * Initial frequency range selected on the spectrum
     * @returns array
     */
    _getInitialSelectionRange() {
        const naxis3Index = FITS_HEADER.naxis3 -1;
        const iFREQ0 = Math.round(naxis3Index / 2 - naxis3Index  / 8);
        const iFREQ1 = Math.round(naxis3Index / 2 + naxis3Index / 8);
        /*const iFREQ0 = 0;
        const iFREQ1 = naxis3Index;*/
        return [iFREQ0, iFREQ1];
    }


    _showCoordinates(coords){
        DOMAccessor.getSummedChartCoordinates().innerText = "@Box xmin=" + Math.round(coords[0]) +
                                                            " xmax=" + Math.round(coords[1]) +
                                                            " ymin=" + Math.round(coords[2]) +
                                                            " ymax=" + Math.round(coords[3]);
    }

    /**
     * Sets title of spectrum, x and y axis
     * 
     * Title of the spectrum depends on its type (Sitelle, Casa, Gildas, Muse)
     */
    refreshChartLegend() {
        // prefix and suffix for GILDAS and ALMA title 
        this._legendObject.defineSummedSpectrumLegend(this);
        if(this.spectrumChart !== null){
            this.spectrumChart.xAxis[0].axisTitle.attr({
                text: this._xtitle
            });
            
        }
    }

    /**
     * Returns the title displayed above the graph
     * @param {number} value integral of selected interval on the graph (float)
     * @param {string} unit  unit of integral value
     * @param {number} vmin minimum selected velocity value (float)
     * @param {number} vmax maximum selected velocity value (float)
     * @param {number} imin minimum selected channel index (int)
     * @param {number} imax maximum selected channel index (int)
     * @returns {string} 
     */
    _getTopTitle(value, unit, vmin, vmax, imin, imax) {
        // do not display unit (Jy.km/s) if bmin/bmax are not defined
        // because it has no meaning in that case
        let result_unit = "";
        if (FITS_HEADER.bmin !== undefined) {
            result_unit = unit;
        }

        return '<span id="selected-surface">' + value.toExponential(2) + "</span> " +
            result_unit + ", vmin=" +
            vmin.toFixed(2) + " " + "km/s" + "  , vmax=" +
            vmax.toFixed(2) + " " + "km/s, imin=" + imin + ", imax=" + imax;
    }

    /**
     * Sets the title diplayed above the graph
     * @param {string} title 
     */
    setChartTitle(title) {
        DOMAccessor.getAverageSpectrumTitle().innerHTML = title;
    }

    refresh(){
        this.plot(this._iRA0, this._iRA1, this._iDEC0, this._iDEC1, undefined);
        this.refreshChartLegend();
    }

    /**
     * Updates the displayed averaged slice image, with respect to selected interval in graph
     * Note : this function should be removed from this class and place in ViewLinker
     * 
     * @param {number} min minimum selected value (float)
     * @param {number} max maximum selected value (float)
     */
    _updateSummedSlices(min, max) {
        if (this._viewLinker !== null) {
            const imin = Math.round((min - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
            const imax = Math.round((max - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
            if (FITS_HEADER.cunit3 in Constants.UNIT_FACTOR) {
                switch (FITS_HEADER.ctype3) {
                    case 'FREQ':
                        if (FITS_HEADER.cdelt3 > 0) {
                            this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
                        } else {
                            this._viewLinker.getAndPlotSummedSlices(imin, imax);
                        }
                        break;
                    case 'VRAD':
                        if (FITS_HEADER.cdelt3 > 0) {
                            this._viewLinker.getAndPlotSummedSlices(imin, imax);
                        } else {
                            this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
                        }
                        break;
                    //equivalent to VRAD
                    case 'VELO-LSR':
                        if (FITS_HEADER.cdelt3 > 0) {
                            this._viewLinker.getAndPlotSummedSlices(imin, imax);
                        } else {
                            this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
                        }
                        break;
                    case 'WAVE':
                        break
                    case 'WAVN':
                        break
                    case 'AWAV':
                        if (FITS_HEADER.cdelt3 > 0) {
                            this._viewLinker.getAndPlotSummedSlices(imin, imax);
                        } else {
                            this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
                        }
                        break;
                    default:
                        alert('Unknown value for ctype3');
                        console.log("This should not happen");
                }
            }
        }

    }

    /**
     * Returns a Highchart chart
     * @returns {chart}
     */
    _getChart() {
        let self = this;
        let target = document.getElementById(this._containerId);
        return Highcharts.chart(target, {
            title: {
                text: ''
            },
            chart: {
                type: 'line',
                animation: false,
                width: self._width,
                height: self._heightWidthRatio,
                zoomType: 'x',
                panning: true,
                panKey: 'shift',
                //marginLeft: 0, // Set the left margin to 0
               // marginRight: 0, // Set the right margin to 0
                responsive: {  
                    rules: [{  
                      condition: {  
                        maxWidth: self._width,
                        maxHeight: self._heightWidthRatio
                      },  
                      chartOptions: {
                        xAxis: {
                            labels: {
                                formatter: function () {
                                    return this.value.charAt(0);
                                }
                            }
                        },
                        yAxis: {
                            labels: {
                                align: 'left',
                                x: 0,
                                y: -5
                            },
                            title: {
                                text: null
                            }
                        }
                    }
                    }]  
                },
                events: {
                    click: function (event) {
                        console.log("A click occurred on the spectrum : enter");
                    },
                    redraw: function(event){
                        // make sure chart title are always up to date
                        self.refreshChartLegend();
                    },
                    selection: function (event) {
                        if(self._viewLinker.isRefreshable){                          
                            let velMin, velMax = null;
                            // if true, refresh displayed summmed slice after the selection
                            let refreshSlice = true;
   
                            // if default values exists for the interval use them then they are reseted
                            if (self.defaultIndexMin !== null && self.defaultIndexMax !== null) {
                                velMin = self.spectrumChart.series[0].xData[self.defaultIndexMin];
                                velMax = self.spectrumChart.series[0].xData[self.defaultIndexMax];
                                // slice is not refreshed
                                refreshSlice = false;
                                self.defaultIndexMin = null;
                                self.defaultIndexMax = null;
                            }
                            // values selected by the user
                            else {
                                velMin = event.xAxis[0].min;
                                velMax = event.xAxis[0].max;
                            }
    
                            // overplot selected area  in blue                            
                            this.xAxis[0].update({
                                plotBands: [{
                                    from: velMin,
                                    to: velMax,
                                    color: 'rgba(68, 170, 213, .2)'
                                }]
                            });
                            
                            self._vmin = velMin;
                            self._vmax = velMax;
    
                            //toggle-lines-search
                            if (self._viewLinker !== null) {
                                self._spectroUI.hideEnergyGroupLines();
                                if (self.linePlotter === null) {
                                    self.linePlotter = new LinePlotter(self._spectroUI);
                                }
                            }
    
                            // refreshes summed slice display according to selection
                            if(refreshSlice === true){
                                self._updateSummedSlices(velMin, velMax);
                            }
        
                            let ivalues = [self._getCalculatedIndex(velMin), self._getCalculatedIndex(velMax)].sort();
                            const imin = ivalues[0];
                            const imax = ivalues[1];
    
                            let selectedSurface = 0;
                            if (self._viewLinker !== null) {
                                selectedSurface = self._getSelectedSpectrumValue(self._averageSpectrum, imin, imax);
                                self._spectroUI.setSelectedSurface(selectedSurface);    
                            }
    
                            self.setChartTitle(self._getTopTitle(selectedSurface, self._toptitle_unit,
                                self._vmin, self._vmax, imin, imax));
    
                            let factor = 1;
                            if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s")  === undefined ){
                                factor = 1 + self._spectroUI.getRedshift();
                            }
                            const obsFreqMin = v2f(velMax * 10 ** 3, FITS_HEADER.restfreq * factor, self._spectroUI.getVelocity("m/s")) / 10 ** 9;
                            const obsFreqMax = v2f(velMin * 10 ** 3, FITS_HEADER.restfreq * factor,  self._spectroUI.getVelocity("m/s")) / 10 ** 9;

    
                            if (self._viewLinker !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
                                const graphs = [self._viewLinker.spectrumViewer.getSpectrumChartXAxis(),
                                self.getSpectrumChartXAxis()
                                ];
    
                                self._viewLinker.updateSummedSlicesFreqIndexes(imin, imax);
    
                                if((self._isInit && FITS_HEADER.velolsr != undefined) || (!self._isInit)){
                                    try {
                                        self.linePlotter.loadAndPlotLines(obsFreqMin, 
                                                                          obsFreqMax, 
                                                                        graphs);
                                    } catch (e) {
                                        console.log(e);
                                        alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
                                    }
                                }
    
                            }
    
                            if(testMode && self._isInit){
                                let meta = {
                                    iFreqMin : imin,
                                    iFreqMax : imax,
                                    freqMin: obsFreqMin,
                                    freqMax: obsFreqMax, 
                                    velMin : velMin,
                                    velMax : velMax
                                };
                                //this._meta = meta;
                                self._executeSpectrumLoadedListener(
                                    get3DSpectrumUpdateEvent("bounds",self._summedData, meta));
                                
                                DOMAccessor.markLoadingDone();
                            }
                            self._isInit = false;
                            
                        }else{
                            alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
                        }
                        return false;
                    }
                }

            },
            boost: {
                useGPUTranslations: true
            },
            xAxis: {
                title: {
                    text: self._xtitle
                },
                crosshair: true,
                reversed: false,
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                maxPadding : 0, 
                endOnTick : false,
                minPadding : 0, 
                startOnTick : false
            },
            yAxis: {
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                lineWidth: 1,
                opposite: true,
                title: {
                    text: self._ytitle
                },
                labels:{
                    formatter: function(){
                        return Number(this.value).toExponential(2);
                    }
                }
            },
            plotOptions: {
                series: {
                    step: 'center',
                    zoneAxis: 'x',
                    animation: {
                        duration: 0
                    },
                    lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
                    events: {
                        click: function (event) {
                            console.log("A click occurred on the LINE : enter");
                        }
                    }
                },
                marker: {
                    radius: 0
                }
            },
            exporting: {
                menuItemDefinitions: {
                    // Custom definition
                    downloadFits: {
                        onclick: function () {
                            window.open(URL_ROOT + dataPaths.averageSpectrum, '_blank');
                        },
                        text: 'Download FITS file'
                    },
                    sendWithSamp: {
                        onclick: function (event) {
                            sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.averageSpectrum, "Artemix");
                            event.stopPropagation();
                        },
                        text: 'Send with SAMP'
                    },
                    sendTo1D: {
                        onclick: function (event) {
                            window.open(URL_3D_TO_1D + dataPaths.averageSpectrum, '_blank');
                        },
                        text: 'Open in 1D viewer'
                    }  
                },
                buttons: {
                    contextButton: {
                        menuItems:  self.getExportMenuItems(yafitsTarget, false)
                    }
                }
            },
            tooltip: {
                formatter: function () {
                    const index = self._getCalculatedIndex(this.x);
                    return 'Chan# ' + index + ' ( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ')';
                }
            },
            series: [{
                color: Constants.PLOT_DEFAULT_COLOR
            }]
        });
    }

    /**
     * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
     * The formula changes according to the type of data on x axis (CTYPE3)
     *
     * @param {number} rlen number of points on x axis (int)
     * @returns {array} an array of x values
     */
    _getXData(rlen) {
        return this._xDataComputer.computeSummedSpectrum(rlen, this._spectroUI.getVelocity("m/s"));
    }

    /**
     * Returns an array of ydata from the data passed in parameter.
     * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
     * and if CDELT3 < 0 in case of a radial velocity
     * It is rescaled in case of Sitelle data.
     * It is returned unchanged in any other case
     *
     * @param {array} averageSpectrum 
     * @returns {array}
     */
    _getYData(averageSpectrum) {
        return this._yDataComputer.computeSummedSpectrum(averageSpectrum, this._spectrumUnit);        
    }

    /**
     * Calls the getAverageSpectrum function of the server to get the spectrum data and plot them
     * 
     * getAverageSpectrum parameters are : relFITSFilePath, iRA0, iDEC0, iRA1, iDEC1
     * 
     * 
     * @param {number} iRA0     minimum selected index value on RA axis (int)
     * @param {number} iDEC0    minimum selected index value on DEC axis (int)
     * @param {number} iRA1     maximum selected index value on RA axis (int)
     * @param {number} iDEC1    maximum selected index value on DEC axis (int)
     */
    plot(iRA0, iRA1, iDEC0, iDEC1, callback) {
        this._iRA0 = iRA0;
        this._iRA1 = iRA1;
        this._iDEC0 = iDEC0;
        this._iDEC1 = iDEC1;

        if(this.spectrumChart !== null){
            this._xMinZoom = this.spectrumChart.xAxis[0].getExtremes().min;
            this._xMaxZoom = this.spectrumChart.xAxis[0].getExtremes().max;
        }

        this.spectrumChart = this._getChart();
        let self = this;
        let queryApi = new ServerApi(); 
        queryApi.getSummedSpectrum(iRA0, iRA1, iDEC0, iDEC1, self._relFITSFilePath, (resp)=>{
            let x = JSON.parse(resp);
            if (x.result.averageSpectrum == null) {
                alert("No data for average spectrum");
                throw ("No data for average spectrum");
            }

            // Let's inform the SAMP hub
            if ("absFITSFilePath" in x["result"]) {
                dataPaths.averageSpectrum = x["result"]["absFITSFilePath"];
            } else {
                console.log("We should have found a key 'absFITSFilePath'");
            }
            let averageSpectrum = x["result"]["averageSpectrum"];

            // Draw x-axis in Velocities (plot on bottom right)
            self._summedData.x = self._getXData(averageSpectrum.length);


            // only at initialization
            if (self._vmin === null && self._vmax === null) {
                self._vmin = self._summedData.x[0];
                self._vmax = self._summedData.x[self._summedData.x.length - 1];
            }

            // box coordinates above chart
            self._showCoordinates([iRA0, iRA1, iDEC0, iDEC1]);

            // change name of function
            averageSpectrum = self._getYData(averageSpectrum);
            self._summedData.y = averageSpectrum;

            let chartData = [];
            for (let i = 0; i < self._summedData.x.length; i++) {
                chartData.push([self._summedData.x[i], self._summedData.y[i]]);
            }

            self._averageSpectrum = averageSpectrum;
            self.spectrumChart.series[0].update({
                name: '',
                // unlimited number of points when zooming
                cropThreshold: Infinity,
                showInLegend: false,
                marker: {
                    radius: 0
                },
                data: chartData,
            });

            /**
            Add a series where Y=0 in the given chart
            */
            let addYAxisSeries = function (chart) {
                chart.addSeries({
                    lineWidth: 1,
                    enableMouseTracking: false,
                    showInLegend: false,
                    color: "#000000",
                    marker: {
                        enabled: false
                    },
                    data: [
                        [chart.xAxis[0].dataMin, 0],
                        [chart.xAxis[0].dataMax, 0]
                    ]
                });
            }

            addYAxisSeries(self.spectrumChart);

            if (self._viewLinker !== null) {
                addYAxisSeries(self._viewLinker.spectrumViewer.spectrumChart);

                if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
                    self.linePlotter.loadAndPlotLines(self.linePlotter.obsFreqMin,
                        self.linePlotter.obsFreqMax,
                        [self.getSpectrumChartXAxis(), self._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
                }
            }

            if(self._xMinZoom !== null && self._xMaxZoom !== null){
                self.spectrumChart.xAxis[0].setExtremes(self._xMinZoom, self._xMaxZoom);
            }             

            if(testMode){
                let meta = {
                    iRA : Math.min(iRA0, iRA1), 
                    iDEC : Math.max(iDEC0, iDEC1), 
                    ytitle : self._ytitle, 
                    xbox : Math.abs(iRA1-iRA0), 
                    ybox : Math.abs(iDEC1-iDEC0),
                    lowerIntegratedValues : {
                        flux : {value: self._getSelectedSpectrumValue(self._averageSpectrum, 0, self._summedData.x.length -1), unit : self._toptitle_unit},
                        vmin : {value : self._vmin, unit : "km/s"},
                        vmax : {value : self._vmax, unit : "km/s"},
                        imin : 0,
                        imax : self._summedData.x.length -1,
                    }
                };
                //this._meta = meta;
                self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("summed",self._summedData, meta));
            }

            if (callback !== undefined)
                callback();
        })

    }

    /**
     * Replot the spectrum according to the received parameters
     * The plot is done through a call to this.plot()
     * 
     * The displayed spectral lines will be updated if they exist
     * 
     * @param {number} xMin start x position (float)
     * @param {number} xMax end x position (float)
     * @param {number} yMin start y position (float)
     * @param {number} yMax end y position (float)
     */
     replot(xMin, xMax, yMin, yMax) {
        // replot the spectrum and keep the selection interval if it exists
        this.plot(xMin, xMax, yMin, yMax, () => {
            if (this.spectrumChart !== null && this._vmin !== null && this._vmax !== null) {
                fireChartSelectionEvent(this.spectrumChart, this._vmin, this._vmax);
            }            
        });
    }

    /**
     * Called when NED table object triggers an event, refreshes graph title
     * 
     * @param {Event} event event that triggered the call
     */
    sourceTableCall(event) {
        this.refreshChartLegend();
    }
}

/**
 * Initializes and return the spectrum
 * @param {*} spectroUI 
 * @param {*} sourceTable 
 * @param {*} withSAMP true if SAMP is enabled
 * @returns 
 */
function getSingleSpectrum1D(spectroUI, sourceTable, withSAMP) {
    let spectrumViewer = new SingleSpectrumViewer1D(dataPaths, 'spectrum', 
                                                    Constants.PLOT_WIDTH_1D_LARGE, 
                                                    Constants.PLOT_HEIGHT_RATIO_1D_LARGE, 
                                                    spectroUI);
    //spectrumViewer.setSpectroUI(spectroUI);
    sourceTable.addListener(spectrumViewer);
    spectrumViewer.plot(Math.floor(FITS_HEADER.naxis1 / 2), Math.floor(FITS_HEADER.naxis2 / 2), ()=>{
        // select all the spectrum by default is velolsr is defined
        // this plot all the lines
        //if(FITS_HEADER.velolsr !== undefined){
        let icenter = Math.round(spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData.length/2);
        let imin = icenter - Math.round(FITS_HEADER.naxis3 / 8);
        let imax = icenter + Math.round(FITS_HEADER.naxis3 / 8);

        fireChartSelectionEvent(spectrumViewer.spectrumChart, 
            spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imin], 
            spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imax] );
        //}
    });
    
    if (withSAMP) {
        // all implement setSampButtonVisible
        setOnHubAvailability([spectrumViewer]);
    }
    
    return spectrumViewer;
}


export{
    fireChartSelectionEvent, SingleSpectrumViewer, 
    SingleSpectrumViewer1D, SummedPixelsSpectrumViewer,
    getSingleSpectrum1D
}