Source: public/javascript/modules/olqv_linkedviews.js

import {
    RaLabelFormatter,
    DecLabelFormatter,
    linearTabulator,
    round,
    revertArray,
    v2f,
    f2v,
    sumArr,
    unitRescale,
    DecDeg2HMS,
    degToRad,
    AbsToPixelConverter
} from "./utils.js";
import { LinePlotter } from './olqv_spectro.js';
import { withSAMP, dataPaths, URL_ROOT, header } from './init.js';
import { FITS_HEADER } from './fitsheader.js';
import { UNIT_FACTOR, DEFAULT_OUTPUT_UNIT, PLOT_WIDTH, PLOT_HEIGHT_RATIO, 
         PLOT_DEFAULT_COLOR, PLOT_DEFAULT_LINE_WIDTH} from "./constants.js";
import { MarkerManager } from "./olqv_markers.js";
import { sAMPPublisher, setOnHubAvailability } from "./samp_utils.js";
import { getProjection } from "./olqv_projections.js";
import { getAverageSpectrumTitle,
         getSingleSliceMousePosition, getSummedSliceMousePosition, 
         showLoaderAction, getConfiguration, setSliceChannel, setSliceRMS, 
         setSummedSliceRMS } from "./domelements.js";


/**
 * Returns the channel corresponding to the given input value (frequency or velocity)
 * @param {number} value (float)
 * @returns {float}
 */
function getCalculatedIndex(value) {
    let result = 0;
    if (FITS_HEADER.ctype3 === 'VRAD') {
        let step1 = (UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / UNIT_FACTOR[FITS_HEADER.cunit3]) / FITS_HEADER.cdelt3;
        let crval3 = FITS_HEADER.crval3 / (UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / 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 DEFAULT_OUTPUT_UNIT['VRAD']*

        let vcenter = 0; //SPEED_OF_LIGHT * (FITS_HEADER.crval3 - FITS_HEADER.restfreq) / FITS_HEADER.restfreq;
        let step1 = v2f(value * UNIT_FACTOR[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;
    }
    return Math.round(result);
}

/**
 * An object to mark informations related to a position
 * in a popup bow located close to the position passed
 * as a parameter.
 * @typedef {Object} LastClickMarker
 * @param {Map} map open layer map where the popup will be displayed
 * @param {string} elementId id of DOM element containing the text
 */
function LastClickMarker(map, elementId) {
    let _map = map;
    let _overlay = null;
    let _container = document.getElementById(elementId);
    let _content = document.getElementById(elementId + '-content');
    let _closer = document.getElementById(elementId + '-closer');

    let _lastChanIndex = null;
    let _lastCoordinate = null;
    let _lastRADEC = null;
    let _lastFluxDensity = null;

    /**
     * Popup creation and addition to an ol overlay to the map passed
     * as a parameter.
     */
    let _popupLastClickInfos = function() {
        if (_overlay == null) {
            /**
             * Create an overlay to anchor the popup to the map.
             */
            _overlay = new ol.Overlay({
                element: _container,
                autoPan: true,
                autoPanAnimation: {
                    duration: 250
                }
            });

            /**
             * Adds a click handler to hide the popup.
             * @return {boolean} Don't follow the href.
             */
            _closer.onclick = function() {
                _overlay.setPosition(undefined);
                _closer.blur();
                return false;
            };

            _map.addOverlay(_overlay);
        }
    };

    /**
     * Updates the content of the popup by using the informations
     * stored in _lastCoordinate, _lastRADEC and _lastFluxDensity
     */
    let _updateLastClickInfos = function() {
        if (_lastCoordinate == null) return;
        _content.innerHTML = 'Chan#' + _lastChanIndex + '<br>' + 'x = ' +
            _lastCoordinate[0].toFixed(0) +
            ', y = ' +
            _lastCoordinate[1].toFixed(0) + '<br>' +
            'RA=' + _lastRADEC['RA'] + '<br>' +
            'DEC=' + _lastRADEC['DEC'] + '<br>' +
            'Value=' + Number(_lastFluxDensity).toExponential(4);
            

        _overlay.setPosition(_lastCoordinate);
    }

    /**
     * Public method to register the values to be displayed
     * in the popup and update its content.
     * @param {object} data an object containing displayed information
     */
    this.setPositionAndFluxDensity = function(data) {
        console.log("this.setPosition = function(coordinate) { : entering");

        _lastChanIndex = data["chanIndex"]
        _lastCoordinate = data["coordinate"];
        _lastRADEC = data["RADEC"];
        _lastFluxDensity = data["fluxDensity"];

        _updateLastClickInfos();

        console.log("this.setPosition = function(coordinate) { : exiting");
    };

    /**
     * Public method to register the fluxDensity value passed as a parameter
     * and update the popup content accordingly.
     * @param {number} fluxDensity flux density (float)
     * @param {number} sliceIndex  index of displayed slice in cube (int)
     */
    this.setFluxDensity = function(fluxDensity, sliceIndex) {
        console.log("this.setFluxDensity = function(fluxDensity) { : entering");
        _lastFluxDensity = fluxDensity;
        _lastChanIndex = sliceIndex;
        _updateLastClickInfos();
        console.log("this.setFluxDensity = function(fluxDensity) { : exiting");
    };

    // create our popup.
    _popupLastClickInfos();
};

/**
 * A class creating buttons appearing on a an open layer map
 * @typedef {Object} ImgControlBuilder
 * @property {string} _buttonInnerHTML HTML code in the button element
 * @property {string} _buttonCss button style
 * @property {number} _topPosition vertical position of the button in the map (0 is on top)
 * @property {string} _title title displayed as a tooltiptext
 */
class ImgControlBuilder {
    constructor() {
        this._buttonInnerHTML = '';
        this._buttonCss = '';
        this._topPosition = '';
        this._title = '';
        this._build = this.build.bind(this);
    }

    set buttonInnerHTML(innerHTML) {
        this._buttonInnerHTML = innerHTML;
    }

    set buttonCss(buttonCss) {
        this._buttonCss = buttonCss;
    }

    set topPosition(topPosition) {
        this._topPosition = topPosition;
    }

    set title(title) {
        this._title = title;
    }

    /**
     * 
     * @returns {IMGControl} returns a button element
     */
    build() {
        let self = this;
        let IMGControl = (function(Control) {
            function IMGControl(opt_options) {
                var options = opt_options || {};
                var button = document.createElement('button');
                button.innerHTML = self._buttonInnerHTML;

                var element = document.createElement('div');
                element.className = self._buttonCss + ' ol-unselectable ol-control';
                element.title = self._title;
                element.style.top = self._topPosition;
                element.appendChild(button);

                Control.call(this, {
                    element: element,
                    target: options.target
                });

                //this.handleImageSlice = function() {;}
                //button.addEventListener('click', this.handleImageSlice.bind(this), false);
                this.setHandler = function(handler) {
                    console.log("Changing handler");
                    console.log(handler);
                    button.addEventListener('click', handler.bind(this), false)
                };
            }

            if (Control) IMGControl.__proto__ = Control;
            IMGControl.prototype = Object.create(Control && Control.prototype);
            IMGControl.prototype.constructor = IMGControl;

            return IMGControl;
        }(ol.control.Control));
        return new IMGControl();
    }
}

/**
 * Base class for Slice display
 * @typedef {Object} Slice
 */
class Slice {
    /**
     * @constructor
     * @param {ViewLinker} viewLinker Object linking spectra and slices
     * @param {string} sliceDivId id of the DOM element containing the slice
     * @param {string} canvasId id of the canvas
     * @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
     * @param {number} width slice width  in pixels (int)
     * @param {number} height slice height in pixels (int)
     */
    constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {

        this._im_layer = null;
        this._data_steps = null;

        this._width = width;
        this._height = height;

        this._RADECRangeInDegrees = RADECRangeInDegrees;
        this._RMS = null;
        this._viewLinker = viewLinker;
        this._map_controls = null;
        this._sampButton = null;

        this._initControl();

        this._hidden_canvas = document.getElementById(canvasId);
        this._hidden_canvas.height = height;
        this._hidden_canvas.width = width;

        this._projection = getProjection(FITS_HEADER.projectionType);
        this._absToPix = new AbsToPixelConverter(FITS_HEADER.crpix1, FITS_HEADER.cdelt1, 
                                                 FITS_HEADER.crpix2, FITS_HEADER.cdelt2, 
                                                 this._projection); 

        this._shapesLayerGroup = new ol.layer.Group({
            layers: []
        });
        this._shapesLayerGroup.set('title', 'shapes');

        this._map = this._getMap(sliceDivId);

        // initial image resolution, for later reset
        this._defaultResolution = this._map.getView().getResolution();
        
        //this._graticule = this._getGraticule();
        //this._graticule.setMap(this._map);
        this._markerManager = new MarkerManager(this._map, true);

        this._initControl = this._initControl.bind(this);
        this.updateSlice = this.updateSlice.bind(this);
        this._imageLoadFunction = this._imageLoadFunction.bind(this);
        this._getMap = this._getMap.bind(this);
        //this._getGraticule = this._getGraticule.bind(this);

    }

    /**
     * Sets the string RMS value with its unit
     * @param {number} value (float)
     * @param {string} unit 
     */
     setRMS(value, unit){       
        this._RMS = "rms=" + Number.parseFloat(value).toExponential(2)+' '+unit;
        this._setRMSLabel(this._RMS);
    }

    /**
     * Called when markers list triggers an event
     */
     markerListClear() {
        this._markerManager.clearMarkers();
    }

    /**
     * Reset image position and resolution
     */
    reset() {
        this._map.getView().setCenter(ol.extent.getCenter(this._viewLinker.extent));
        this._map.getView().setResolution(this._defaultResolution);
    }

    /**
     * Tests if a pixel has a value, returns false if pixel has no value, i.e RGB value is 255,0,0
     * returns true in any other case
     * @param {number} x (int) 
     * @param {number} y (int)
     * @returns {boolean}
     */
    pixelHasValue(x, y){
        let ctx = this._hidden_canvas.getContext('2d');
        let pixelAtPosition = ctx.getImageData(x,  this._height - y, 1, 1).data;
        if(pixelAtPosition[0] == 255 && pixelAtPosition[1] == 0 && pixelAtPosition[2] == 0){
            return false;
        }else{
            return true;
        }
    }

    /**
     * Toggles display of the samp button on the slice image
     * Button will be displayed if state is true, hidden if it is false
     * @param {boolean} state 
     */
    setSampButtonVisible(state){
        if(this._sampButton !== null){
            if(state){
                this._sampButton.element.style.display = "block";
            }else{
                this._sampButton.element.style.display = "none";
            }
        }
    }

    /**
     * Called when NED table object triggers an event, adds a marker on the slice pointing on an object in the sky
     * @param {Event} event event received from NED table, containing ra/dec and the name of an object
     */
    sourceTableCall(event) {
        let res = this._absToPix.convert(degToRad(event.detail.ra), degToRad(event.detail.dec));
        try {
            this._markerManager.addMarker(res[0], res[1], event.detail["object"]);
        } catch (error) {
            console.log(error);
        }
    }

    /**
     * Called when markers list triggers an event
     * @param {Event} event event containing a list of markers to plot on the slice
     */
    markerListUpdate(event) {
        this._markerManager.clearMarkers();
        let raDec = this._projection.absToRel(degToRad(this._RADECRangeInDegrees[0][0]), degToRad(this._RADECRangeInDegrees[1][0]));
        //let res = this._absToPix.convert(degToRad(this._RADECRangeInDegrees[0][0]), degToRad(event.detail.dec));

        //let ra_to_pix = new RADDtoPixelConverter(this._RADECRangeInDegrees[0][0], this._RADECRangeInDegrees[1][0], 0, this._width - 1);
        //let dec_to_pix = new DECDDtoPixelConverter(this._RADECRangeInDegrees[0][1], this._RADECRangeInDegrees[1][1], 0, this._height - 1);
        for (const marker of event.detail.markers) {            
            console.log(marker);
            try {
                // convert RA/DEC in degrees
                //const ra = HMS2DecDeg(marker["ra"]);
                //const dec = DMS2DecDeg(marker["dec"]);
                this._markerManager.addMarker(raDec["ra"], raDec["dec"], marker["label"]);
            } catch (error) {
                console.log(error);
            }
        }

    }

    /**
     * What happens when the image to be displayed in 'image'
     * is loaded.
     * @param {Image} image an open layer image (ol.Image) that will contain src
     * @param {string} src image url
     */
    _imageLoadFunction(image, src) {
        console.log(" _imageLoadFunction : entering");
        showLoaderAction(true);
        image.getImage().addEventListener('load', () => {
            showLoaderAction(false);
            this._hidden_canvas.getContext('2d').drawImage(image.getImage(), 0, 0);
        });

        image.getImage().src = src;
        image.getImage().crossOrigin = "Anonymous";

        console.log("_imageLoadFunction : exiting");
    }

    /**
     * Returns an ol.Graticule open layer object, showing a grid for a coordinate system
     * This is an abstract function that must be implemented in a derived class
     */
    _getGraticule() {
        throw new Error("This method must be implemented");
    }

    /**
     * Returns the open layer map of the slice viewer
     * This is an abstract function that must be implemented in a derived class
     * @param {number} id of displayed slice in cube (int)
     */
    _getMap(sliceDivId) {
        throw new Error("This method must be implemented");
    }

    /**
     * Returns the coordinates at cursor position
     * This is an abstract function that must be implemented in a derived class     * 
     * @param {Array} olc open layers coordinates
     */
    coordinateFormat(olc) {
        throw new Error("This method must be implemented");
    }

    /**
     * Creates buttons on the slice viewer
     * This is an abstract function that must be implemented in a derived class
     */
    _initControl() {
        throw new Error("This method must be implemented");
    }

    /**
     * Updates the displayed image. Data object is generally obtained from a request to yafitss
     * This is an abstract function that must be implemented in a derived class
     * @param {object} data
     */
    updateSlice(data) {
        throw new Error("This method must be implemented");
    }

    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {string} text a function
     */
     _setRMSLabel(text) {
        throw new Error("This method must be implemented");
    }
}


/**
 * Class displaying the content of a single slice in an image ( a channel map extracted from a channel (frequency))
 * @extends Slice
 */
class SingleSlice extends Slice {

    /**
    * @constructor
    * @param {ViewLinker} viewLinker Object linking spectra and slices
    * @param {string} sliceDivId id of the DOM element containing the slice
    * @param {string} canvasId id of the canvas
    * @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
    * @param {number} width slice width  in pixels (int)
    * @param {number} height slice height in pixels (int)
     */
    constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {
        super(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height)
        this._lastClickMarker = '';
        this._map_controls = this._getMapControl();
        setSliceChannel("* Chan#0");
        //click in slice viewer
        this._map.on("click", (event) => {
            // keep old x axis limits before replot
            if(this.pixelHasValue(Math.round(event.coordinate[0]), Math.round(event.coordinate[1]))){
                const minVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].min;
                const maxVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].max;
                this._viewLinker.markLastClickInSlice(event.coordinate);
                this._viewLinker.spectrumViewer.setFrequencyMarker(this._viewLinker.sliceIndex);
                showLoaderAction(true);
                this._viewLinker.spectrumViewer.plot(Math.floor(event.coordinate[0]),
                    Math.floor(event.coordinate[1]),
                    () => {
                        // keep the old min/max values on x axis when
                        // refreshing graph
                        // does not seem useful, commented for now
                        // replot already selected spectral lines
                        if (this._viewLinker.summedPixelsSpectrumViewer.linePlotter != null && this._viewLinker.spectroUI.isEnabled()) {
                            this._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines( this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin, 
                                                                                                      this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax, 
                                                                                                      [this._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis(), this._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
                        }
                        // set old x axis limits
                        this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
                    });
            }
        });
        this._getMapControl = this._getMapControl.bind(this);
    }

    /**
     * Returns the open layer map of the slice viewer
     * @param {number} id of displayed slice in cube (int)
     */
    _getMap(sliceDivId) {
        return new ol.Map({
            target: sliceDivId,
            view: new ol.View({
                projection: this._viewLinker.olProjection,
                center: ol.extent.getCenter(this._viewLinker.extent),
                resolution: this._hidden_canvas.width / 512
            }),
            controls: this._viewLinker.controls
        });
    }

    /**
     * Returns an ol.Graticule open layer object, showing a grid for a coordinate system
     * This is an abstract function that must be implemented in a derived class
     * @returns {Graticule} an ol.Graticule object
     */
    _getGraticule() {
        return new ol.Graticule({
            showLabels: true,
            strokeStyle: new ol.style.Stroke({
                color: 'rgba(0,0,0,0.9)',
                width: 1,
                lineDash: [0.5, 4]
            }),
            //targetSize: 75,
            lonLabelFormatter: this._viewLinker.raLabelFormatter.format,
            lonLabelStyle: new ol.style.Text({
                font: '12px Calibri,sans-serif',
                textBaseline: 'bottom',
                fill: new ol.style.Fill({
                    color: 'rgba(0,0,0,1)'
                }),
                stroke: new ol.style.Stroke({
                    color: 'rgba(255,255,255,1)',
                    width: 3
                })
            }),
            latLabelFormatter: this._viewLinker.decLabelFormatter.format
        });
    }

    /**
     * Returns the coordinates at cursor position
     * @param {Array} olc open layers coordinates
     * @returns {string}
     */
    coordinateFormat(olc) {
        let result;
        let ctx = this._hidden_canvas.getContext('2d');
        let pixelAtPosition = ctx.getImageData(olc[0], this._height - olc[1], 1, 1).data;
        let raDec = this._projection.iRaiDecToHMSDMS(olc[0], olc[1]);
        if (pixelAtPosition) {
            let data_steps_index = pixelAtPosition.slice(0, 3).join('_');
            if (data_steps_index !== "0_0_0") {
                result = raDec['ra'] + ', ' + raDec["dec"]  ;
            }
        } else {
            result = "???";
        }
        return result;
    }

    /**
     * Defines the action triggered when the mouse moves on the slice
     * This creates a link between the SingleSlice and SummedSlice through the ViewLinker
     */
    _getMapControl() {
        return [
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: getSingleSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this.coordinateFormat(olc) }
            }),
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: getSummedSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this._viewLinker.summedSlicesImage.coordinateFormat(olc) }
            }),
            new ol.control.FullScreen()
        ];
    }

    /**
     * Creates buttons on the slice viewer
     */
    _initControl() {
        let builder = new ImgControlBuilder();
        let controls = [];

        // Reset image position
        builder.buttonCss = "btn-slice-png fa fa-home";
        builder.buttonInnerHTML = "R";
        builder.topPosition = "480px";
        builder.title = "Reset zoom and position";
        let imgReset = builder.build();
        imgReset.setHandler(() => {
            this.reset();
        });


        // open image in 2D viewer
        builder.buttonCss = "btn-slice-png";
        builder.buttonInnerHTML = "&#9673;";
        builder.topPosition = "60px";
        builder.title = "Open in 2D viewer";
        let imgView = builder.build();

        imgView.setHandler(() => {
            let getPath = this._viewLinker._getFITSSliceImage();
            getPath.then(x => {
                let res = JSON.parse(x);
                let url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + res.result;
                window.open(url);
            });
        });

        //download image
        builder.buttonInnerHTML = "&#8681;";
        builder.topPosition = "83px";
        builder.title = "Download the 2D image in FITS on disk";
        let imgDownload = builder.build();
        imgDownload.setHandler(() => {
            let getPath = this._viewLinker._getFITSSliceImage();
            getPath.then(x => {
                let res = JSON.parse(x);
                let url = URL_ROOT + res.result;
                window.open(url, '_blank');
            });
            /*var url = URL_ROOT + this._viewLinker.getFitsSliceImagePath();
            window.open(url, '_blank');*/
        });

        controls.push(imgReset);
        controls.push(imgView);
        controls.push(imgDownload);

        // samp button
        builder.buttonCss = "samp-publish-png";
        builder.buttonInnerHTML = "&#8663";
        builder.topPosition = "107px";
        builder.title = "Send with SAMP";

        this._sampButton = builder.build();
        if(withSAMP){
            this._sampButton.setHandler(sAMPPublisher.sendPNGSlice);
            controls.push(this._sampButton);
        }

        this._viewLinker.controls = ol.control.defaults().extend(controls);
    }

    /**
     * Updates the displayed image. Data object is generally obtained from a request to yafitss
     * @param {object} data
     */
    updateSlice(data) {
        this._data_steps = data["data_steps"];
        if (FITS_HEADER.isSITELLE()) {
            for (var k in this._data_steps) {
                if (this._data_steps.hasOwnProperty(k)) {
                    this._data_steps[k] /= Math.abs(3600. * 3600. * FITS_HEADER.cdelt1 * FITS_HEADER.cdelt2);
                }
            }
        }

        if (this._im_layer) {
            this._map.removeLayer(this._im_layer);
        }

        this._im_layer = new ol.layer.Image({
            source: new ol.source.ImageStatic({
                url: URL_ROOT + "/" + data["path_to_png"],
                projection: this._viewLinker.olProjection,
                imageExtent: this._viewLinker.extent,
                imageLoadFunction: this._imageLoadFunction
            })
        });
        this._map.getLayers().insertAt(0, this._im_layer);
    }


    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {object} fn a function
     */
     _setRMSLabel(text) {
        setSliceRMS(text);
    }
}

/**
 * Class displaying the content of a map averaged from a frequency-range (velocity-range) 
 * selected in the averaged spectrum
 * @extends Slice
 * @property {number} sliceIndex0 selected start index in averaged spectrum (int)
 * @property {number} sliceIndex1 selected end index in averaged spectrum (int)
 * @property {number} selectedBox box selected by the user in the image
 * @property {number} regionOfInterest boundaries of the cube as found in the header
 */
class SummedSlice extends Slice {
    /**
    * @constructor
    * 
    * @param {ViewLinker} viewLinker Object linking spectra and slices
    * @param {string} sliceDivId id of the DOM element containing the slice
    * @param {string} canvasId id of the canvas
    * @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
    * @param {number} width slice width  in pixels (int)
    * @param {number} height slice height in pixels (int)
     */
    constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {
        super(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height);
        // public attributes
        this.sliceIndex0 = null;
        this.slideIndex1 = null;
        this.selectedBox = null;
        this.regionOfInterest = {
            iRA0: 0,
            iRA1: FITS_HEADER.naxis1 - 1,
            iDEC0: 0,
            iDEC1: FITS_HEADER.naxis2 - 1,
            iFREQ0: 0,
            iFREQ1: FITS_HEADER.naxis3 - 1
        };

        //private attributes
        this._select = this._getSelect();
        this._dragBox = this._getDragBox();
        this._map_controls = this._getMapControl();

        this._map.addInteraction(this._select);
        this._map.addInteraction(this._dragBox);
        //this._graticule = this._getGraticule();
        //this._graticule.setMap(this._map);

        // Here we have all the stuff to create boxes on summedslices
        // and trigger the update of the spectrum of sums of pixels per slice
        this._box_source = new ol.source.Vector({
            wrapX: false
        });

        this._box_layer = new ol.layer.Vector({
            source: this._box_source
        });

        //click in summed slice viewer
        this._map.on("click", (event) => {
            if(this.pixelHasValue(Math.round(event.coordinate[0]), Math.round(event.coordinate[1]))){
                this._viewLinker.markLastClickInSlice(event.coordinate);
                this._viewLinker.markLastClickInSummedSlice(event.coordinate);
                $.post("", {
                    "method": "getAverage",
                    "relFITSFilePath": this._viewLinker._relFITSFilePath,
                    "iRA0": event.coordinate[0],
                    "iRA1": event.coordinate[0],
                    "iDEC0": event.coordinate[1],
                    "iDEC1": event.coordinate[1],
                    "iFREQ0": this.regionOfInterest.iFREQ0,
                    "iFREQ1": this.regionOfInterest.iFREQ1,
                    "retFITS": false
                }).done(
                    (resp) => {
                        console.log("getAverage callback entering");
                        // keep old x axis limits before replot
                        const minVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].min;
                        const maxVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].max;
                        let result = resp["data"];
                        if (result["status"])
                            this._viewLinker.setFluxDensityInSummedPopup(result["result"][0][0]);
                        this._viewLinker.spectrumViewer.setFrequencyMarker(this._viewLinker.sliceIndex);
                        let self = this;
                        this._viewLinker.spectrumViewer.plot(Math.floor(event.coordinate[0]), Math.floor(event.coordinate[1]), () => {
                            // set old x axis limits
                            self._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
                        });
                        console.log("getAverage callback exiting");
                    }
                ).fail(
                    function(err) {
                        var msg = "POST failed" + JSON.stringify(err, 0, 4);
                        console.log(msg);
                        alert(msg);
                    });
            }

        });

        this._imageLoadFunction = this._imageLoadFunction.bind(this);
        this._getSelect = this._getSelect.bind(this);
        this._getDragBox = this._getDragBox.bind(this);
        this._getMapControl = this._getMapControl.bind(this);
        this.forgetSelectedBox = this.forgetSelectedBox.bind(this);
    }

    /**
     * Adds a Layer the shapesLayerGroup
     * @param {Layer} layer an open layer Layer object (ol.layer.Layer)
     */
    addShapesLayer(layer) {
        this._shapesLayerGroup.getLayers().getArray().push(layer);
    }

    /**
     * Defines the action triggered when the mouse moves on the slice
     * This creates a link between the SingleSlice and SummedSlice through the ViewLinker
     */
    _getMapControl() {
        return [
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: getSingleSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this._viewLinker.singleSliceImage.coordinateFormat(olc) }
            }),
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: getSummedSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this.coordinateFormat(olc) }
            }),
            new ol.control.FullScreen()
        ];
    }

    /**
     * Returns the open layer map of the slice viewer
     * @param {number} id of displayed slice in cube (int)
     */
    _getMap(sliceDivId) {
        return new ol.Map({
            target: sliceDivId,
            view: new ol.View({
                projection: this._viewLinker.olProjection,
                center: ol.extent.getCenter(this._viewLinker.extent),
                resolution: this._hidden_canvas.width / 512
            }),
            controls: this.controls,
            layers: [this._shapesLayerGroup]
        });
    }

    /**
     * Returns an ol.Graticule open layer object, showing a grid for a coordinate system
     */
    _getGraticule() {
        return new ol.Graticule({
            showLabels: true,
            strokeStyle: new ol.style.Stroke({
                color: 'rgba(0,0,0,0.9)',
                width: 1,
                lineDash: [0.5, 4]
            }),
            targetSize: 100,
            lonLabelFormatter: this._viewLinker.raLabelFormatter.format,
            latLabelFormatter: this._viewLinker.decLabelFormatter.format,
            decLabelPosition: 0.92
        });
    }

    /**
     * Returns the coordinates at cursor position
     * @param {Array} olc open layers coordinates
     */
    coordinateFormat(olc) {
        let result;
        let ctx = this._hidden_canvas.getContext('2d');

        let pixelAtPosition = ctx.getImageData(olc[0], this._height - olc[1], 1, 1).data;
        let raDec = this._projection.iRaiDecToHMSDMS(olc[0], olc[1]);
        if (pixelAtPosition) {
            let data_steps_index = pixelAtPosition.slice(0, 3).join('_');
            if (data_steps_index !== "0_0_0") {
                result = raDec["ra"] + ', ' + raDec["dec"];
            }
        } else {
            result = "???";
        }
        return result;
    }

    /**
     * Creates buttons on the slice viewer
     */
    _initControl() {
        let controls = [];

        // object building buttons
        let builder = new ImgControlBuilder()

        // open image 2D viewer
        builder.buttonCss = "btn-slice-png";
        builder.buttonInnerHTML = "&#9673;";
        builder.topPosition = "60px";
        builder.title = "Open in 2D viewer";
        let imgView = builder.build();

        imgView.setHandler(() => {
            var getPath = this._viewLinker._getFITSSumSliceImage();
            getPath.then(x => {
                var res = JSON.parse(x);
                var url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + res.result;
                window.open(url);
            });
            //var url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + this._viewLinker.getFitsSumSliceImagePath();
            //window.open(url);
        });

        //download image
        builder.buttonInnerHTML = "&#8681;";
        builder.topPosition = "83px";
        builder.title = "Download the 2D image in FITS on disk";
        let imgDownload = builder.build();
        imgDownload.setHandler(() => {
            var getPath = this._viewLinker._getFITSSumSliceImage();
            getPath.then(result => {
                    var res = JSON.parse(result);
                    var url = URL_ROOT + res.result;
                    window.open(url, '_blank');;
                })
                //var url = URL_ROOT + this._viewLinker.getFitsSumSliceImagePath();
                //window.open(url, '_blank');
        });

        controls.push(imgView);
        controls.push(imgDownload);

        // open image in Aladin
        builder.buttonCss = "samp-publish-png";
        builder.buttonInnerHTML = "&#8663";
        builder.topPosition = "107px";
        builder.title = "Send with SAMP";

        this._sampButton = builder.build();

        if(withSAMP){
            this._sampButton.setHandler(sAMPPublisher.sendPNGSummedSlices);
            controls.push(this._sampButton);
        }


        //add all controls to list
        this.controls = ol.control.defaults().extend(controls);
    }


    /**
     * Returns a Select object, defining what happens when user clicks on a box in the slice 
     * @returns {Select} ol.interaction.Select
     */
    _getSelect() {
        let select = new ol.interaction.Select({
            condition: ol.events.pointerMove
        });

        select.on('select', (e) => {
            if (e.selected.length) {
                this.selectedBox = e.selected[0];
                var extent = e.selected[0].getGeometry().getExtent();
                // keep current limits for x axis when refreshing the plot
                this._viewLinker.summedPixelsSpectrumViewer.replot(extent[0], extent[2], extent[1], extent[3]);
                /*let minVal = this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].min;
                let maxVal = this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].max;
                this._viewLinker.summedPixelsSpectrumViewer.plot(extent[0], extent[2], extent[1], extent[3]);

                // replot already selected spectral lines
                if (this._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null) {
                    this._viewLinker.summedPixelsSpectrumViewer.linePlotter.plotSpectroscopicDataOnGraph(this._viewLinker.summedPixelsSpectrumViewer.linePlotter.transitions, this._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis());
                    this._viewLinker.summedPixelsSpectrumViewer.linePlotter.plotSpectroscopicDataOnGraph(this._viewLinker.summedPixelsSpectrumViewer.linePlotter.transitions, this._viewLinker.spectrumViewer.getSpectrumChartXAxis());
                }

                // and apply again
                this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);*/

                //summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);
                this.regionOfInterest.iRA0 = Math.round(extent[0]);
                this.regionOfInterest.iRA1 = Math.round(extent[2]);
                this.regionOfInterest.iDEC0 = Math.round(extent[1]);
                this.regionOfInterest.iDEC1 = Math.round(extent[3]);
            }
        });
        return select;
    }

    /**
     * Returns a DragBox object, defining what happens when user creates a box in the slice 
     * @returns {DragBox} ol.interaction.DragBox
     */
    _getDragBox() {
        let dragBox = new ol.interaction.DragBox();
        dragBox.on('boxend', () => {
            var extent = this._dragBox.getGeometry().getExtent();
            var tl = ol.extent.getTopLeft(extent);
            var tr = ol.extent.getTopRight(extent);
            var br = ol.extent.getBottomRight(extent);
            var bl = ol.extent.getBottomLeft(extent);
            if( this.pixelHasValue(tl[0], tl[1]) &&
                this.pixelHasValue(tr[0], tr[1]) &&
                this.pixelHasValue(br[0], br[1]) &&
                this.pixelHasValue(bl[0], bl[1])
            ){
                var corners = []
                corners.push(tl, tr, br, bl, tl);
                /*
                var ln = new ol.geom.LinearRing(corners);
                var style = {
                    strokeColor: "#00FF00",
                    strokeOpacity: 1,
                    strokewidth: 3,
                    fillColor: "#00FF00",
                    fillOpacity: 0.8,
                };*/
                var pf = new ol.Feature({
                    geometry: new ol.geom.Polygon([corners])
                });
                this._box_source.addFeature(pf);
            }else{
                alert("At least one of selected points have no value.");
            }

        });
        return dragBox;
    }

    /**
     * Updates the displayed image. Data object is generally obtained from a request to yafitss
     * This is an abstract function that must be implemented in a derived class
     * @param {object} data
     */
    updateSlice(data) {
        this._data_steps = data["data_steps"];
        let path_to_png = data["path_to_png"];

        if (this._im_layer) {
            this._map.removeLayer(this._im_layer);
            this._map.removeLayer(this._box_layer);
        }

        this._im_layer = new ol.layer.Image({
            source: new ol.source.ImageStatic({
                url: URL_ROOT + "/" + path_to_png,
                projection: this._viewLinker.olProjection,
                imageExtent: this._viewLinker.extent,
                imageLoadFunction: this._imageLoadFunction
            })
        });
        this._map.getLayers().insertAt(0, this._im_layer);
        this._map.getLayers().insertAt(1, this._box_layer);
    }

    /**
     * Deletes the currently selected box, if it exists
     */
    forgetSelectedBox() {

        console.log('this.forgetSelectedBox = function() {: entering');

        var styleForget = function() {
            return [new ol.style.Style({
                stroke: new ol.style.Stroke({
                    color: [255, 0, 0, 1]
                })
            })];
        };

        if (this.selectedBox) {
            this.selectedBox.setStyle(styleForget);
            this._box_source.removeFeature(this.selectedBox);
            this._box_source.refresh();
            this._viewLinker.summedPixelsSpectrumViewer.replot(0, this._width - 1, 0, this._height - 1)

            this.regionOfInterest.iRA0 = Math.round(0);
            this.regionOfInterest.iRA1 = Math.round(this._width - 1);
            this.regionOfInterest.iDEC0 = Math.round(0);
            this.regionOfInterest.iDEC1 = Math.round(this._height - 1);

        }
        console.log('this.forgetSelectedBox = function() {: exiting');
    }

    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {object} fn a function
     */
     _setRMSLabel(text) {
        setSummedSliceRMS('* ' + text);
    }
}

/**
 * Class creating link between slices and spectra
 * @typedef {Object} ViewLinker
 * 
 * @property {SpectrumViewer} spectrumViewer
 * @property {SummedPixelsSpectrumViewer} summedPixelsSpectrumViewer
 * @property {array} extent
 * @property {RaLabelFormatter} raLabelFormatter
 * @property {SpectruDecLabelFormattermViewer} decLabelFormatter
 * @property {SliceViewer} singleSliceImage
 * @property {SummedSliceViewer} summedSlicesImage
 * @property {Projection} coordsProjection
 * @property {SpectroscopyUI} spectroUI
 * 
 */
class ViewLinker {
    constructor(paths, width, height,
        RADECRangeInDegrees, divSlice, divSummedSlices, spectroUI) {

        // public attributes
        this.spectrumViewer = null;
        this.summedPixelsSpectrumViewer = null;
        this.spectroUI = spectroUI;

        this.extent = [0, 0, width - 1, height - 1];
        this.raLabelFormatter = new RaLabelFormatter(this.extent[0], this.extent[2],
            RADECRangeInDegrees[0][0], RADECRangeInDegrees[1][0]);
        this.decLabelFormatter = new DecLabelFormatter(this.extent[1], this.extent[3],
            RADECRangeInDegrees[0][1], RADECRangeInDegrees[1][1]);

        this.singleSliceImage = new SingleSlice(this, divSlice, "hidden-" + divSlice, RADECRangeInDegrees, width, height);
        this.summedSlicesImage = new SummedSlice(this, divSummedSlices, "hidden-" + divSummedSlices, RADECRangeInDegrees, width, height);
        //this.singleSliceImage._getMapControl(this.summedSlicesImage);

        // open layers projection for image display
        /*this.olProjection = new ol.proj.Projection({
            code: 'local_image',
            units: 'pixels',
            extent: this.extent,
            worldExtent: [...this.extent]
        });*/

        // projection object for coordinates calculation
        try{
            this.coordsProjection = getProjection(FITS_HEADER.projectionType);
        }catch(e){
            alert(e);
        }
        

        this.sliceIndex = null;

        // private attributes
        this._relFITSFilePath = paths.relFITSFilePath;
        this._lastClickMarker = new LastClickMarker(this.singleSliceImage._map, 'popup-single');
        this._lastClickMarkerSummed = new LastClickMarker(this.summedSlicesImage._map, 'popup-summed');

        console.log("_ra0 = " + DecDeg2HMS(RADECRangeInDegrees[0][0]) + " _ra1 =" + DecDeg2HMS(RADECRangeInDegrees[1][0]));
        console.log("_dec0 = " + DecDeg2HMS(RADECRangeInDegrees[0][1]) + " _dec1 =" + DecDeg2HMS(RADECRangeInDegrees[1][1]));

        //events
        this.singleSliceImage._map.getView().on('change:resolution', (event) => {
            this.updateView(event, this.summedSlicesImage._map.getView());
        });

        this.singleSliceImage._map.getView().on('change:center', (event) => {
            this.updateView(event, this.summedSlicesImage._map.getView());
        });

        this.summedSlicesImage._map.getView().on('change:resolution', (event) => {
            this.updateView(event, this.singleSliceImage._map.getView());
        });

        this.summedSlicesImage._map.getView().on('change:center', (event) => {
            this.updateView(event, this.singleSliceImage._map.getView());
        });
    }

    /**
     * Sets spectrumViewer
     * @param {SpectrumViewer} spectrumViewer 
     */
    setSpectrumViewer(spectrumViewer) {
        this.spectrumViewer = spectrumViewer
    }

    /**
     * Sets summedPixelsSpectrumViewer 
     * @param {SummedPixelsSpectrumViewer} summedPixelsSpectrumViewer 
     */
    setSummedPixelsSpectrumViewer(summedPixelsSpectrumViewer) {
        this.summedPixelsSpectrumViewer = summedPixelsSpectrumViewer
    }

    /**
     * Executes a POST request and updates singleSlice.
     * Request parameters are : 
     *  -for slice retrieval : selected slice index, fits file path
     *  -for image configuration :  ittName, lutName, vmName
     * 
     * @param {number} sliceIndex index of searched slice (int)
     */
    getAndPlotSingleSlice(sliceIndex) {
        console.log('getAndPlotSingleSlice: entering');
        showLoaderAction(true);
        let self = this;
        let config = getConfiguration();
        this.sliceIndex = sliceIndex;
        $.post('png', {
            'si': this.sliceIndex,
            'relFITSFilePath': this._relFITSFilePath,
            'ittName': config.ittName,
            'lutName': config.lutName,
            'vmName': config.vmName
        }).done(
            function(resp) {
                console.log("$.post('/png', {'si': sliceIndex, 'relFITSFilePath': _relFITSFilePath}).done(: entering");
                if (resp["status"] == false) {
                    alert("Something went wrong during the generation of the image. The message was '" + resp["message"] + "'");
                } else {
                    self.singleSliceImage.updateSlice(resp["result"]);
                    self.singleSliceImage.setRMS(parseFloat(resp["result"]["statistics"]["stdev"]),  FITS_HEADER.bunit);
                    if (withSAMP) {
                        dataPaths.relSlicePNG = resp["result"]["path_to_png"];
                    }
                }
                showLoaderAction(false);
                console.log("$.post('/png', {'si': sliceIndex, 'path': _path}).done(: exiting");
            }
        )
        console.log('_updateSliceWithPOST: exiting');
    }

    /**
     * Executes a POST request and updates summedSlice
     * Request parameters are : 
     *  -for slice retrieval : start index (on averaged spectrum), end index (on averaged spectrum), fits file path
     *  -for image configuration :  ittName, lutName, vmName
     * 
     * @param {number} sliceIndex0 start index (int)
     * @param {number} sliceIndex1 end index (int)
     */
    getAndPlotSummedSlices(sliceIndex0, sliceIndex1) {
        console.log(" getAndPlotSummedSlices : entering");
        showLoaderAction(true);
        let self = this;
        let config = getConfiguration();
        this.summedSlicesImage.sliceIndex0 = sliceIndex0;
        this.summedSlicesImage.sliceIndex1 = sliceIndex1;
        $.post('sumpng', {
            'si0': self.summedSlicesImage.sliceIndex0,
            'si1': self.summedSlicesImage.sliceIndex1,
            'relFITSFilePath': self._relFITSFilePath,
            'ittName': config.ittName,
            'lutName': config.lutName,
            'vmName': config.vmName
        }).done(
            function(resp) {
                console.log("$.post('/sumpng', {'si0': sliceIndex0, 'si1': sliceIndex1, 'relFITSFilePath': relFITSFilePath}).done() : entering");
                console.log("in _updateSummedSlicesWithPOST");

                if (resp["status"] == false) {
                    alert("Something went wrong during the generation of the image. The message was " +
                        resp["message"] + "'");
                } else {
                    self.summedSlicesImage.updateSlice(resp["result"]);
                    self.summedSlicesImage.setRMS(parseFloat(resp["result"]["statistics"]["stdev"]), FITS_HEADER.bunit+"*km/s");

                    if (withSAMP) {
                        dataPaths.relSummedSlicesPNG = resp["result"]["path_to_png"];
                    }
                }

                showLoaderAction(false);
                console.log("$.post('/sumpng', {'si0': sliceIndex0, 'si1': sliceIndex1, 'relFITSFilePath': relFITSFilePath}).done() : exiting");
            });
        console.log("_updateSummedSlicesWithPOST : exiting");
    }


    /**
     * Refreshes both slices display from current parameters
     */
    refresh() {
        console.log("refresh: entering")
        this.getAndPlotSingleSlice(this.sliceIndex);
        this.getAndPlotSummedSlices(this.summedSlicesImage.sliceIndex0, this.summedSlicesImage.sliceIndex1);
        console.log("refresh: exiting")
    }

    /**
     * Notifies to the view that an action occured on an image (image has been moved or zoomed in/out)
     * @param {*} event type of action
     * @param {*} viewRef modified view
     */
    updateView(event, viewRef) {
        let newValue = event.target.get(event.key);
        viewRef.set(event.key, newValue);
    }

    /**
     * Removes the currently selected box on the summedSlicesViewer
     */
    forgetSelectedBox() {
        this.summedSlicesImage.forgetSelectedBox();
    }

    /**
     * Returns a promise object querying the server to create and get a fits file corresponding to the current slice
     * It calls the createFITSSliceImage endpoint of the server, with relFITSFilePath ( currently opened fits file)
     * and iFREQ (current slice index) as parameters.
     *
     * @returns {Promise} Promise object containing a query to the server to create a fits file corresponding to the current slice
     */
    _getFITSSliceImage() {
        //Create a FITS file containing the image to download.
        return $.post("", {
            "method": "createFITSSliceImage",
            "relFITSFilePath": this._relFITSFilePath,
            "iFREQ": this.sliceIndex
        }).done(
            function(resp) {
                console.log("A FITS file has been created for the upper image.");
                let x = JSON.parse(resp);
                if (!x["status"]) {
                    console.log(`Something went wrong during the generation of the image FITS file, the message was
                    ${x["message"]}`);
                    alert(x["message"]);
                    //self._fitsSliceImagePath = x["result"];
                }
                /*else {
                    console.log(`Something went wrong during the generation of the image FITS file, the message was
                    ${x["message"]}`);
                    alert(x["message"]);
                }*/
            }
        ).fail(
            function(err) {
                let msg = "POST failed" + JSON.stringify(err, 0, 4);
                console.log(msg);
                alert(msg);
            }
        );
    }

    /**
     * Returns a promise object querying the server to create and get a fits file corresponding to the current slice
     * It calls the createFITSSumSliceImage endpoint of the server, with relFITSFilePath ( currently opened fits file),
     * iFREQ0 (average start index) and iFREQ1 (average end index) as parameters.
     *
     * @returns {Promise} Promise object containing a query to the server to create a fits file corresponding to the current averaged slice
     */
    _getFITSSumSliceImage() {
        //Create a FITS file containing the bottom image to download.
        return $.post("", {
            "method": "createFITSSumSliceImage",
            "relFITSFilePath": this._relFITSFilePath,
            "iFREQ0": this.summedSlicesImage.sliceIndex0,
            "iFREQ1": this.summedSlicesImage.sliceIndex1
        }).done(
            function(resp) {
                console.log("A FITS file has been created for the bottom image." + resp);
                let x = JSON.parse(resp);
                if (!x["status"]) {
                    console.log(`Something went wrong during the generation of the bottom image FITS file, the message was
                    ${x["message"]}`);
                    alert(x["message"]);
                    //self._fitsSumSliceImagePath = x["result"];
                }
                /* else {
                    console.log(`Something went wrong during the generation of the bottom image FITS file, the message was
                    ${x["message"]}`);
                    alert(x["message"]);
                }*/
            }
        ).fail(
            function(err) {
                let msg = "POST failed" + JSON.stringify(err, 0, 4);
                console.log(msg);
                alert(msg);
            }
        );
    }

    /**
     * Displays a popup at the position clicked in the slice. It shows the coordinates of the click in the image, 
     * the chanel index, ra/dec values and flux density. Flux density is always set as "To Be Determined" because a query to the server 
     * it necessary to get the value. "t.b.d." will be replaced once the value has been obtained.
     * @param {Slice} target the slice where the click occured
     * @param {array} coordinate a 2 elements array containing the x,y coordinates
     */
    _markLastClickInSlice(target, coordinate) {
        let raDec = this.coordsProjection.iRaiDecToHMSDMS(coordinate[0], coordinate[1]);
        target.setPositionAndFluxDensity({
            "coordinate": coordinate,
            "chanIndex": this.sliceIndex,
            "RADEC": {  'RA': raDec["ra"],
                        'DEC': raDec["dec"]
                     },
            "fluxDensity": "t.b.d."
        });
    };


    /**
     * Calls _markLastClickInSlice when slice image is clicked
     * @param {array} coordinate a 2 elements array containing the x,y coordinates
     */
    markLastClickInSlice(coordinate) {
        this._markLastClickInSlice(this._lastClickMarker, coordinate);
    };

        /**
     * Calls _markLastClickInSlice when averaged slice image is clicked
     * @param {array} coordinate a 2 elements array containing the x,y coordinates
     */
    markLastClickInSummedSlice(coordinate) {
        this._markLastClickInSlice(this._lastClickMarkerSummed, coordinate);
    };

    /**
     * Sets the value of flux density in a popup in a slice
     * @param {Slice} target the target slice
     * @param {number} density  density value (float)
     */
    _setFluxDensityInPopup(target, density) {
        target.setFluxDensity(density * unitRescale(FITS_HEADER.bunit), this.sliceIndex);
    }

    /**
     * Sets the value of flux density in a popup in the single slice ( by calling _setFluxDensityInPopup)
     * @param {Slice} target the target slice
     * @param {number} density  density value (float)
     */
    setFluxDensityInPopup(density) {
        this._setFluxDensityInPopup(this._lastClickMarker, density);
    }

        /**
     * Sets the value of flux density in a popup in the averaged slice (by calling _setFluxDensityInPopup)
     * @param {Slice} target the target slice
     * @param {number} density  density value (float)
     */
    setFluxDensityInSummedPopup(density) {
        this._setFluxDensityInPopup(this._lastClickMarkerSummed, density);
    }
}

/**
 * A class displaying a spectrum, using the Highcharts library
 * 
 * @property {Object} spectrumChart a highchart chart object
 */
class SpectrumViewer {
    /**
     * @constructor
     * @param {Object} paths dataPaths object
     * @param {string} containerId id of graph container
     * @param {ViewLinker} viewLinker ViewLinker object managing interactions between elements
     */
    constructor(paths, containerId, viewLinker) {
        this.spectrumChart = null;
        this._ifrequencyMarker = 0;
        this._containerId = containerId;
        this._viewLinker = viewLinker;

        this._xtitle = "undefined";
        this._ytitle = "undefined";
        this.toptitle = "undefined";
        this._relFITSFilePath = paths.relFITSFilePath;
        this._sampButton = undefined;
        this._initTitles();
        this._computeSliceIndex = this._computeSliceIndex.bind(this);
    }

    /**
     * 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: "frequency"
        };
    }

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

    /**
     * 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
     */
    _initTitles() {
        if (FITS_HEADER.isSITELLE()) {
            this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
            this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
            this.toptitle = "";
        } else if (FITS_HEADER.isCASA()) {
            this._xtitle = "Sky Frequency (GHz) - " + FITS_HEADER.specsys;
            this._ytitle = "Flux density (" + FITS_HEADER.bunit + ")";
            let coeff = Math.PI / 180. / 4.86e-6;
            this.toptitle = "B: " + round(FITS_HEADER.bmaj * coeff, 1) + "x" +
                round(FITS_HEADER.bmin * coeff, 1) + " PA " +
                round(FITS_HEADER.bpa, 0) + "°";
        } else if (FITS_HEADER.isGILDAS()) {
            this._xtitle = "Sky Frequency (GHz)";
            this._ytitle = "Flux density (" + FITS_HEADER.bunit + ")";
            this.toptitle = "";
        } else if (FITS_HEADER.isMUSE()) {
            this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
            this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
            this.toptitle = "";
        } else if (FITS_HEADER.isMIRIAD()) {
            this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
            this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
            this.toptitle = "";
        } else {
            alert("Warning, unknown instrument : " + FITS_HEADER.instrume);
        }
    }

    /**
     * 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
     */
    _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))
    }

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


    /**
     * 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
     */
    _getPoint = function(x, y, visible) {
        return {
            type: 'scatter',
            name: '',
            showInLegend: false,
            visible: visible,
            zIndex: 1,
            enableMouseTracking: false,
            marker: {
                radius: 5
            },
            data: [{
                x: x,
                y: y,
                color: '#BF0B23'
            }]
        }
    }

    /**
     * Toggles samp button visibility
     * @param {boolean} state status of button visibility
     */
    setSampButtonVisible(state) {
        if (this._sampButton !== undefined) {
            if (state === true) {
                this._sampButton.show();
            } else {
                this._sampButton.hide();
            }
        }
    }

    /**
     * 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: PLOT_WIDTH,
                height: PLOT_HEIGHT_RATIO,
                animation: false,
                zoomType: 'x',
                panning: true,
                panKey: 'shift',
                events: {
                    load: function(event) {
                        // graph is loaded
                        showLoaderAction(false);
                    },

                    click: function(event) {
                        //console.clear();
                        console.log("A click occurred on the spectrum : enter");
                        let sliceIndex = self._computeSliceIndex(plotData, event.xAxis[0].value);
                        // Display slice at index sliceIndex
                        self._viewLinker.getAndPlotSingleSlice(sliceIndex);
                        self._viewLinker.setFluxDensityInPopup(event.yAxis[0].value);
                        this.series[1].update(self._getPoint(event.xAxis[0].value, 0));
                    }
                }

            },
            boost: {
                useGPUTranslations: true,
                usePreAllocated: true
            },
            xAxis: {
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                title: {
                    text: xtitle
                },
                crosshair: true,
                reversed: true,
                events: {
                    // called when boudaries of spectrum are modified
                    setExtremes: function(event) {
                        if (event.min === undefined || event.max === undefined) {
                            self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].setExtremes(
                                self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].dataMin,
                                self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].dataMax);
                        } else {

                            let vcenter = FITS_HEADER.getVCenter();
                            let restfreq = FITS_HEADER.restfreq;

                            let minval = Math.round(f2v(event.min * 1e9, restfreq, vcenter) / 1e3);
                            let maxval = Math.round(f2v(event.max * 1e9, restfreq, vcenter) / 1e3);

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

                            self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.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: PLOT_DEFAULT_COLOR,
                    animation: {
                        duration: 0
                    },
                    lineWidth: PLOT_DEFAULT_LINE_WIDTH,
                    events: {
                        click: function(event) {
                            //console.clear();
                            console.log("A click occurred on the LINE : enter");                            
                            let sliceIndex = self._computeSliceIndex(plotData, event.point.x);
                            setSliceChannel("* Chan#"+sliceIndex);
                            // Display slice at index sliceIndex
                            self._viewLinker.setFluxDensityInPopup(event.point.y);
                            self._viewLinker.getAndPlotSingleSlice(sliceIndex);
                            this.chart.series[1].update(self._getPoint(event.point.x, 0));
                        }
                    }
                },
                marker: {
                    radius: 0
                }
            },
            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
            self._getPoint(0, 0, false)],
        }, function(chart) { // on complete  \&#8681;
            // save spectrum data
            chart.renderer.button('Save', 10, 10)
                .attr({
                    zIndex: 3, 
                    title : "Download the 1D spectrum in FITS format (readable in GILDAS/CLASS)"
                })
                .on('click', function(event) {
                    console.log("A click occurred on the save button : enter");
                    window.open(URL_ROOT + dataPaths.spectrum, '_blank');
                    event.stopPropagation();
                })
                .add();
            // send with samp
            self._sampButton = chart.renderer.button('Samp', 60, 10)
                .attr({
                    zIndex: 3,
                    title : "Send with SAMP",
                    id: "samp"
                })
                .on('click', function(event) {
                    sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
                    event.stopPropagation();
                })
                .add();
            //initially there is no samp connection
            self._sampButton.hide();
        });


    }

    /**
     * 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) {
        let xData = new Array(rlen);
        for (var i = 0; i < rlen; i++) {
            let tmp = linearTabulator(FITS_HEADER.crval3, FITS_HEADER.cdelt3, FITS_HEADER.crpix3, i + 1);
            if (FITS_HEADER.cunit3 in UNIT_FACTOR) {
                switch (FITS_HEADER.ctype3) {
                    case 'FREQ':
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        } else {
                            xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        }
                        break;

                    case 'VRAD':
                        // ### TO COMPLETE, crpix3 = 66, frest = 230.538, NOU=channel 0
                        // if centerVal = 0 : lines are correct but zoom does not work (ex  M83)
                        // if centerVal = crval3 : zoom is correct but lines do not work
                        let tmp1 = v2f(tmp * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.restfreq, 0 /*FITS_HEADER.crval3*/ ) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['FREQ']];
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp1;
                        } else {
                            xData[rlen - i - 1] = tmp1;
                        }
                        break;

                    case 'VELO-LSR':
                        // This case is equivalent to VRAD above
                        let tmpvelo = v2f(tmp * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.restfreq, 0 /*FITS_HEADER.crval3*/ ) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['FREQ']];
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmpvelo;
                        } else {
                            xData[rlen - i - 1] = tmpvelo;
                        }
                        break;

                    case 'WAVE':
                        xData[i] = tmp;
                        //alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                    case 'WAVN':
                        xData[i] = tmp;
                        //alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                    case 'AWAV':
                        xData[i] = tmp;
                        break;

                    default:
                        console.log("This should not happen");
                        alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
                        break;
                }
            }
        }
        return xData;
    }

    /**
     * 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 = null;
        switch (FITS_HEADER.ctype3) {
            case 'FREQ':
                if (FITS_HEADER.cdelt3 > 0) {
                    result = revertArray(data);
                } else {
                    result = data;
                }
                break;

            case 'VRAD':
                if (FITS_HEADER.cdelt3 > 0) {
                    result = data;
                } else {
                    result = revertArray(data);
                }
                break;

            // equivalent to VRAD
            case 'VELO-LSR':
                if (FITS_HEADER.cdelt3 > 0) {
                    result = data;
                } else {
                    result = revertArray(data);
                }
                break;

            case 'WAVE':
                result = data;
                //alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
            case 'WAVN':
                result = data;
                //alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
            case 'AWAV':
                result = data;
                break;

            default:
                console.log("This should not happen");
                alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
                break;
        }

        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)
     */
    _createFile(iRA, iDEC) {
        if (FITS_HEADER.ctype3 === "FREQ" || FITS_HEADER.ctype3 === "VRAD") {
            $.post("", {
                    "method": "createFits",
                    "relFITSFilePath": this._relFITSFilePath,
                    "iRA": iRA,
                    "iDEC": iDEC
                })
                .done(
                    function(resp) {
                        console.log("A FITS file has been created for the spectrum.");
                        var x = JSON.parse(resp);
                        if (x["status"]) {
                            dataPaths.spectrum = x["result"];
                        } else {
                            console.log(`Something went wrong during the generation of 
                            the spectrum FITS file, the message was ${x["message"]}`);
                            alert(x["message"]);
                        }
                    }
                )
                .fail(
                    function(err) {
                        var msg = "POST failed" + JSON.stringify(err, 0, 4);
                        console.log(msg);
                        alert(msg);
                    }
                );
        }
    }

    /**
     * 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) {
        console.log("plot: entering.");
        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);
        }
        // get spectrum
        $.post("", {
            "method": "getSpectrum",
            "relFITSFilePath": this._relFITSFilePath,
            "iRA": iRA,
            "iDEC": iDEC,
            "iFREQ0": 0,
            "iFREQ1": FITS_HEADER.naxis3 - 1
        }).done(
            /*
             ** This is the function which actually performs the plot as a callback on
             ** return from a call to the FITS file server in order to get the spectrum to draw.
             */
            function(resp) {
                console.log("getSpectrum callback : entering");
                if (resp.data["status"] == false) {
                    alert(resp.data["message"]);
                    showLoaderAction(false);
                    return;
                }

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

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

                //if (withSAMP) {
                //Create a FITS file containing the spectrum
                self._createFile(iRA, iDEC);
                //}

                //self._frequencyMarker.x = [plotData.x[self._ifrequencyMarker]];
                // set value of flux density in popup, should me moved out of this function
                self._viewLinker.setFluxDensityInPopup(plotData.y[self._ifrequencyMarker]);

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

                if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.isEnabled()) {
                    self._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin, 
                                                                                             self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax, 
                                                                                             [self._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis(), self.getSpectrumChartXAxis()]);
                }

                console.log("getSpectrum callback : exiting");
            }

        );
        console.log("plot: exiting.");
    };
}


/**
 * A class displaying an averaged spectrum, using the Highcharts library
 * 
 * @property {Object} summedPixelsSpectrumChart a highchart chart object
 * @property {LinePlotter} linePlotter an object drawing spectral lines on the chart
 */
class SummedPixelsSpectrumViewer {
    /**
     * 
     * @param {*} paths 
     * @param {*} containerId 
     * @param {*} viewLinker 
     */
    constructor(paths, containerId, viewLinker) {

        this.summedPixelsSpectrumChart = null;
        this.linePlotter = null;

        this._spectroUI = viewLinker.spectroUI;
        this._containerId = containerId;
        this._viewLinker = viewLinker;
        this._xtitle = "undefined";
        this._ytitle = "undefined";
        this._toptitle_unit = "";
        this._flux_unit = "";
        this._averageSpectrum = null;
        this._sampButton = undefined;
        this._relFITSFilePath = paths.relFITSFilePath;

        this._summedData = {
            // styling of selected area, not used for now
            /*type: 'scatter',
            marker: {
                color: '#1f77b4',
                size: 5.
            },
            line: {
                color: '#1f77b4',
                width: 1
            },
            connectgaps: 'true',
            hoverinfo: 'x+y',
            xaxis: 'x',*/
            //selected data
            x : [],
            y : []
        };

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

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

    /**
     * Replot the spectrum according to the received parameters
     * The plot is done trhough a call to this.plot()
     * 
     * The displayed spectral lines will be updated if they exist
     * 
     * @param {number} x start x position (float)
     * @param {number} xWidth width selected on x-axis (float)
     * @param {number} y start y position (float)
     * @param {number} yWidth width selected on y-axis (float)
     */
    replot(x, xWidth, y, yWidth) {
        let minVal = this.summedPixelsSpectrumChart.xAxis[0].min;
        let maxVal = this.summedPixelsSpectrumChart.xAxis[0].max;
        this.plot(x, xWidth, y, yWidth);

        /*if (this.linePlotter !== null) {
            this.linePlotter.removeLines();
        }*/

        // replot already selected spectral lines
        if (this.linePlotter !== null && this._viewLinker.spectroUI.isEnabled()) {
            this.linePlotter.loadAndPlotLines(  this.linePlotter.obsFreqMin, 
                                                this.linePlotter.obsFreqMax, 
                                                [this.getSummedPixelsSpectrumChartXAxis(), this._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
        }

        // and apply again
        this.summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);
    }

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

    /**
     * Sets title of spectrum, x and y axis
     * 
     * Title of the spectrum depends on its type (Sitelle, Casa, Gildas, Muse)
     */
    _initTitles() {
        if (FITS_HEADER.bmin !== undefined)
            this._flux_unit = "(" + this._summedPixelsSpectrumUnit(FITS_HEADER.bunit) + ")";

        if (FITS_HEADER.isSITELLE()) {
            this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
            this._ytitle = "FLUX (" + this._summedPixelsSpectrumUnit(header["BUNIT"]) + ")";
            this._toptitle_unit = "ergs/s/cm^2";
        } else if (FITS_HEADER.isCASA()) {
            this._xtitle = "Velocity (km/s) - " + FITS_HEADER.specsys;
            this._ytitle = "Int. flux density " + this._flux_unit;
            this._toptitle_unit = "Jy.km/s";
        } else if (FITS_HEADER.isGILDAS()) {
            this._xtitle = "Velocity (km/s)"
            this._toptitle_unit = "Jy.km/s";

            // special case
            if (FITS_HEADER.isSpectrumInK()) {
                this._ytitle = "Int. flux density (K)";
                this._toptitle_unit = "K.km/s";
            }
            // common case
            else {
                this._ytitle = "Int. flux density " + this._flux_unit;
            }
        } else if (FITS_HEADER.isMUSE()) {
            this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
            this._ytitle = "Int. flux)";
            this._toptitle_unit = "";
        } else {
            alert("Unknown instrument : " + FITS_HEADER.instrume);
        }
    }

    /**
     * Returns a formatted string containing a displayable unit name
     * @param {string} unit the source unit
     * @returns {string} a formatted unit
     */
    _summedPixelsSpectrumUnit(unit) {
        switch (unit) {
            case "Jy/beam":
                return "Jy";

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

            default:
                return "";
        }
    }

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

    /**
     * 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) {
        getAverageSpectrumTitle().innerHTML = title;
    }

    /**
     * Toggles SAMP button visibility
     * @param {boolean} state a boolean value corresponding to the new state of the button
     */
    setSampButtonVisible(state) {
        if (this._sampButton !== undefined) {
            if (state === true) {
                this._sampButton.show();
            } else {
                this._sampButton.hide();
            }
        }
    }

    /**
     * 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) {
        var imin = Math.round((min - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
        var imax = Math.round((max - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
        if (FITS_HEADER.cunit3 in 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() {
        var self = this;
        let target = document.getElementById(this._containerId);
        return Highcharts.chart(target, {
            title: {
                text: ''
            },
            chart: {
                type: 'line',
                animation: false,
                width: PLOT_WIDTH,
                height: PLOT_HEIGHT_RATIO,
                zoomType: 'x',
                panning: true,
                panKey: 'shift',
                events: {
                    click: function(event) {
                        console.log("A click occurred on the spectrum : enter");
                        //self._viewLinker.setFluxDensityInSummedPopup(event.yAxis[0].value);

                    },
                    selection: function(event) {
                        //console.clear();
                        // overplot selected area  in blue
                        this.xAxis[0].update({
                            plotBands: [{
                                from: event.xAxis[0].min,
                                to: event.xAxis[0].max,
                                color: 'rgba(68, 170, 213, .2)'
                            }]
                        });
                        //toggle-lines-search
                        self._spectroUI.hideEnergyGroupLines();
                        const velMin = Math.min(event.xAxis[0].min, event.xAxis[0].max);
                        const velMax = Math.max(event.xAxis[0].min, event.xAxis[0].max);

                        const obsFreqMin = v2f(velMax * 10 ** 3, FITS_HEADER.restfreq, FITS_HEADER.getVCenter()) / 10 ** 9;
                        const obsFreqMax = v2f(velMin * 10 ** 3, FITS_HEADER.restfreq, FITS_HEADER.getVCenter()) / 10 ** 9;

                        self.summedPixelsSpectrumChart.obsFreqMin = obsFreqMin;
                        self.summedPixelsSpectrumChart.obsFreqMax = obsFreqMax;

                        const graphs = [self._viewLinker.spectrumViewer.getSpectrumChartXAxis(),
                            self.getSummedPixelsSpectrumChartXAxis()
                        ];
                        if (self.linePlotter === null) {
                            self.linePlotter = new LinePlotter(self._spectroUI);
                        }

                        if (self._spectroUI.isEnabled()) {
                            self.linePlotter.loadAndPlotLines(obsFreqMin, obsFreqMax, graphs);
                        }

                        // update global variable containing spectrum
                        self._updateSummedSlices(event.xAxis[0].min, event.xAxis[0].max);
                        let xData = [];

                        for (let i = 0; i < this.series[0].data.length; i++) {
                            xData.push(this.series[0].data[i].x);
                        }

                        let imin = getCalculatedIndex(event.xAxis[0].min);
                        let imax = getCalculatedIndex(event.xAxis[0].max);

                        self._selectedSurface = self._getSummedSpectrumValue(self._averageSpectrum, imin, imax);
                        self.setChartTitle(self._getTopTitle(self._selectedSurface, self._toptitle_unit, event.xAxis[0].min, event.xAxis[0].max, imin, imax));
                        self._spectroUI.setLineLuminosityValue(self._selectedSurface);
                        if (!self._spectroUI.isLineConfigurationOk()) {
                            alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
                        }
                        return false;
                    }
                }

            },
            boost: {
                useGPUTranslations: true
            },
            xAxis: {
                title: {
                    text: self._xtitle
                },
                crosshair: true,
                reversed: false,
                gridLineWidth: 1,
                lineColor: '#FFFFFF'
            },
            yAxis: {
                gridLineWidth: 1,
                lineColor: '#FFFFFF',
                lineWidth: 1,
                opposite: true,
                title: {
                    text: self._ytitle
                }
            },
            plotOptions: {
                series: {
                    step: 'center',
                    zoneAxis: 'x',
                    animation: {
                        duration: 0
                    },
                    lineWidth: PLOT_DEFAULT_LINE_WIDTH,
                    events: {
                        click: function(event) {
                            //console.clear();
                            console.log("A click occurred on the LINE : enter");
                            // Display slice at index sliceIndex
                            self._viewLinker.setFluxDensityInSummedPopup(event.point.y);
                        }
                    }
                },
                marker: {
                    radius: 0
                }
            },
            tooltip: {
                formatter: function() {
                    const index = getCalculatedIndex(this.x);
                    return 'Chan# ' + index + ' ( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ')';
                }
            },
            series: [{
                color: PLOT_DEFAULT_COLOR
            }]
        }, function(chart) { // on complete  \&#8681;
            chart.myButton = chart.renderer.button('Save', 10, 10)
                .attr({
                    zIndex: 3,
                    title : "Download the 1D spectrum in FITS format (readable in GILDAS/CLASS)"
                })
                .on('click', function(event) {
                    window.open(URL_ROOT + dataPaths.averageSpectrum, '_blank');
                    event.stopPropagation();
                })
                .add();

            // send with samp
            self._sampButton = chart.renderer.button('Samp', 60, 10)
                .attr({
                    zIndex: 3,
                    title : "Send with SAMP"
                })
                .on('click', function(event) {
                    //console.log("A click occurred on the samp button : enter");
                    sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.averageSpectrum, "Artemix");
                    event.stopPropagation();
                })
                .add();

            //initially there is no samp connection
            self._sampButton.hide();
        });
    }

    /**
     * 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) {
        let xData = new Array(rlen);
        for (var i = 0; i < rlen; i++) {
            let tmp = linearTabulator(FITS_HEADER.crval3, FITS_HEADER.cdelt3, FITS_HEADER.crpix3, i + 1);
            let tmpCenter = tmp - (FITS_HEADER.restfreq - FITS_HEADER.crval3) / FITS_HEADER.cdelt3; /*- FITS_HEADER.naxis3 / 2 * FITS_HEADER.cdelt3*/ ;
            if (FITS_HEADER.cunit3 in UNIT_FACTOR) {
                switch (FITS_HEADER.ctype3) {
                    case 'FREQ':
                        // centered at 0, restfreq = crval3, crval3 = 0
                        //let tmp1 = f2v(tmpCenter * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.crval3, 0) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['VRAD']];
                        const frequency = tmpCenter * UNIT_FACTOR[FITS_HEADER.cunit3];
                        const restfreq = FITS_HEADER.restfreq;
                        const vcenter = 0;
                        let tmp1 = f2v(frequency, restfreq, vcenter) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['VRAD']];

                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[rlen - i - 1] = tmp1;
                        } else {
                            xData[i] = tmp1;
                        }
                        break;

                    case 'VRAD':
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        } else {
                            xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        }
                        break;

                    // equivalent to VRAD
                    case 'VELO-LSR':
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        } else {
                            xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
                        }
                        break;

                    case 'WAVE':
                        // alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp;
                        } else {
                            xData[rlen - i - 1] = tmp;
                        }
                        break;
                    case 'WAVN':
                        // alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp;
                        } else {
                            xData[rlen - i - 1] = tmp;
                        }
                        break;
                    case 'AWAV':
                        if (FITS_HEADER.cdelt3 > 0) {
                            xData[i] = tmp;
                        } else {
                            xData[rlen - i - 1] = tmp;
                        }
                        break;

                    default:
                        console.log("This should not happen");
                        alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
                }
            }
        }
        return xData;

    }

    /**
     * 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, unit) {
        switch (FITS_HEADER.ctype3) {
            case 'FREQ':
                if (FITS_HEADER.cdelt3 > 0) {
                    return revertArray(averageSpectrum);
                } else {
                    return averageSpectrum;
                }
                // M33CO
            case 'VRAD':
                averageSpectrum = averageSpectrum.map(function(x) {
                    return x * unitRescale(unit);
                });
                if (FITS_HEADER.cdelt3 > 0) {
                    return averageSpectrum;
                } else {
                    return revertArray(averageSpectrum);
                }

            // equivalent to VRAD
            case 'VELO-LSR':
                averageSpectrum = averageSpectrum.map(function(x) {
                    return x * unitRescale(unit);
                });
                if (FITS_HEADER.cdelt3 > 0) {
                    return averageSpectrum;
                } else {
                    return revertArray(averageSpectrum);
                }
            case 'WAVE':
                // alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                averageSpectrum = averageSpectrum.map(function(x) {
                    return x * unitRescale(unit);
                });
                if (FITS_HEADER.cdelt3 > 0) {
                    return averageSpectrum;
                } else {
                    return revertArray(averageSpectrum);
                }
            case 'WAVN':
                // alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
                averageSpectrum = averageSpectrum.map(function(x) {
                    return x * unitRescale(unit);
                });
                if (FITS_HEADER.cdelt3 > 0) {
                    return averageSpectrum;
                } else {
                    return revertArray(averageSpectrum);
                }
            case 'AWAV':
                averageSpectrum = averageSpectrum.map(function(x) {
                    return x * unitRescale(unit);
                });
                if (FITS_HEADER.cdelt3 > 0) {
                    return averageSpectrum;
                } else {
                    return revertArray(averageSpectrum);
                }

            default:
                console.log("This should not happen");
                alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
                break;
        }
    }

    /**
     * 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) {
        this.summedPixelsSpectrumChart = this._getChart();
        showLoaderAction(true);
        let self = this;
        $.post("", {
            "method": "getAverageSpectrum",
            "relFITSFilePath": self._relFITSFilePath,
            "iRA0": iRA0,
            "iRA1": iRA1,
            "iDEC0": iDEC0,
            "iDEC1": iDEC1
        }).done(function(resp) {
            console.log("SummedPixelsSpectrumViewer : callback of getAverageSpectrum: entering ");
            showLoaderAction(false);
            if (resp["status"] == false) {
                alert(`Something went wrong with the calculation of the average spectrum. The message was '${resp["message"]}'`);
            } else {
                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("Strange 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);

                // change name of function
                averageSpectrum = self._getYData(averageSpectrum, self._summedPixelsSpectrumUnit(FITS_HEADER.bunit));
                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.summedPixelsSpectrumChart.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.summedPixelsSpectrumChart);
                addYAxisSeries(self._viewLinker.spectrumViewer.spectrumChart);

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

                let title = self._getSummedSpectrumValue(averageSpectrum, 0, averageSpectrum.length - 1);
                self.setChartTitle(self._getTopTitle(title, self._toptitle_unit, self._summedData.x[0], self._summedData.x[averageSpectrum.length - 1], 0, averageSpectrum.length - 1));
            }
            console.log("SummedPixelsSpectrumViewer : callback of getAverageSpectrum: exiting ");
        })
    }
}

/**
 * Returns a ViewLinker object that provides an interface to manipulate Spectrums and Slices
 * 
 * @param {array} radecRange RA/DEC ranges corresponding to opened fits file 
 * @param {SpectroUI} spectroUI spectroscopy interface control object
 * @param {SourceTable} sourceTable NED interface object
 * @param {MarkerList} markerList list of markers
 * @returns {ViewLinker}
 */
function getViewLinker(radecRange, spectroUI, sourceTable, markerList) {

    let RADECRangeInDegrees = radecRange;
    console.log("Data of '" + dataPaths.relFITSFilePath + "' are contained in " + JSON.stringify(radecRange));

    let viewLinker = new ViewLinker(dataPaths, FITS_HEADER.width, FITS_HEADER.height, RADECRangeInDegrees, "slice",
        "summed-slices", spectroUI);

    // naxis3 is slice number
    viewLinker.getAndPlotSingleSlice(0);
    viewLinker.getAndPlotSummedSlices(0, FITS_HEADER.naxis3 - 1);

    let spectrumViewer = new SpectrumViewer(dataPaths, 'spectrum', viewLinker);
    viewLinker.setSpectrumViewer(spectrumViewer);
    spectrumViewer.plot(FITS_HEADER.width / 2, FITS_HEADER.height / 2);

    let summedPixelsSpectrumViewer = new SummedPixelsSpectrumViewer(dataPaths, 'summed-pixels-spectrum', viewLinker, spectroUI);
    viewLinker.setSummedPixelsSpectrumViewer(summedPixelsSpectrumViewer);
    summedPixelsSpectrumViewer.plot(0, FITS_HEADER.width - 1, 0, FITS_HEADER.height - 1);
    //refresh lines display when redshift selection changes
    sourceTable.addListener(summedPixelsSpectrumViewer);
    sourceTable.addListener(viewLinker.summedSlicesImage);
    sourceTable.addListener(viewLinker.singleSliceImage);

    markerList.addListener(viewLinker.summedSlicesImage);
    markerList.addListener(viewLinker.singleSliceImage);

    if (withSAMP) {
        // all implement setSampButtonVisible
        setOnHubAvailability([  spectrumViewer, 
                                summedPixelsSpectrumViewer, 
                                viewLinker.singleSliceImage, 
                                viewLinker.summedSlicesImage]);
    }

    return viewLinker;
}



export {
    getViewLinker
}