Source: public/javascript/modules/olqv_slice.js

import { HMS2DecDeg,DMS2DecDeg} from "./utils.js";
import { withSAMP, dataPaths, URL_ROOT, URL_3D_TO_2D, yafitsTarget } from './init.js';
import { ServerApi } from './serverApi.js'
import { FITS_HEADER } from './fitsheader.js';
import { MarkerManager, Marker } from "./olqv_markers.js";
import { sAMPPublisher, setOnHubAvailabilityButtons } from "./samp_utils.js";
import { getProjection } from "./olqv_projections.js";
import { DOMAccessor } from "./domelements.js";
import { EventFactory } from './customevents.js';
import { Viewer } from './olqv_viewer.js';
import { AxesFactory } from './olqv_axes.js';
import {highlightedFeature, standardFeature} from "./olqv_shapestyle.js";
import { ResetButton, View2DButton, SaveImageButton, PublishSAMPButton, DeleteButton} from "./olqv_olbuttons.js";
import { CustomControls } from "./olqv_customcontrols.js";
import { KeyCodeProcessor } from "./olqv_keyboardevents.js";
import { Boxes3DFactory } from "./olqv_boxes.js";
import { InfosBlock } from "./olqv_infosblock.js";

/**
 * Returns an event signaling that a slice has been modified
 * data packed inside the event are used in testing context
 * @param {object} slice an instance of Slice classes
 * @param {string} type type of the modified slice ( single or summed)
 * @param {object} data slice data
 * @returns Event
 */
function getSliceUpdateEvent(slice, sliceIndex, type, data){
    let event = new Event(type);
    event.rmsValue = slice.getRms();
    event.rmsUnit = slice.getRmsUnit();
    event.channel = sliceIndex;
    event.sliceMinValue = data['statistics']['min'];
    event.sliceMaxValue = data['statistics']['max'];

    let xRef = Math.round(FITS_HEADER.naxis1 / 2);
    let yRef = Math.round(FITS_HEADER.naxis2 / 2);

    event.sliceRefRaDec = slice._projection.iRaiDecToHMSDMS(xRef, yRef);
    event.sliceRefXi = xRef;
    event.sliceRefYi = yRef;
    event.polygon = slice._polygon;

    return event;
}

/**
 * Returns the coordinates of the defautl box created on a slice
 * @returns a dictionary where keys are iRA0, iRA1, iDEC0, iDEC1
 */
function getBoxCoordinates(){
    const naxis1Index = FITS_HEADER.naxis1 - 1;
    const naxis2Index = FITS_HEADER.naxis2 - 1;
    return {"iRA0": Math.round(naxis1Index / 2 - naxis1Index / 16),
            "iRA1": Math.round(naxis1Index / 2 + naxis1Index / 16),
            "iDEC0": Math.round(naxis2Index / 2 - naxis2Index / 16),
            "iDEC1": Math.round(naxis2Index / 2 + naxis2Index / 16)}

    /*return {"iRA0": 0,
            "iRA1": naxis1Index,
            "iDEC0": 0,
            "iDEC1": naxis2Index}*/
}



/**
 * Base class for Slice display
 * @typedef {Object} Slice
 */
class Slice {

    static objectType = "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._viewLinker = viewLinker;

        // infoBlock set to null to use Viewer class
        var context = {
            'file': dataPaths.relFITSFilePath,
            /*'url': url,*/
            'url': '',
            'pixel-slice-range': 0,
            'slice-phys-chars': {
                'type': FITS_HEADER.ctype3,
                'unit': FITS_HEADER.cunit3,
                'value': FITS_HEADER.crval3
            },
            'array-phys-chars': { 'type': FITS_HEADER.btype, 'unit': FITS_HEADER.bunit }
        };

        // contains infos about selected elements in slice
        // not used in 3D at the moment
        let infosBlock = new InfosBlock(document.getElementById("infos-line"),
                                        document.getElementsByTagName("body")[0], context);

        // object displaying slice
        this.viewer = new Viewer(this._viewLinker.relFITSFilePath, width, height, sliceDivId, canvasId,
            null, infosBlock);

        //container for options buttons in slice
        this.customControls = new CustomControls(this.viewer);

        this._projection = getProjection(FITS_HEADER.projectionType);
        this._hidden_canvas = document.getElementById(canvasId);
        this._hidden_canvas.height = height;
        this._hidden_canvas.width = width;

        this.axesFactory = new AxesFactory(this.viewer, FITS_HEADER.projectionType);
        this.axesFactory.build();

        this.boxesFactory = null;
        this.selector = this.viewer.getSelector();

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

        this._RADECRangeInDegrees = RADECRangeInDegrees;

        this._minValue = null;
        this._maxValue = null;

        // standard deviation
        this._rms = null;
        this._rmsUnit = null;
        this._mean = null;

        this.map_controls = null;
        this._sampButton = null;

        //this._initControl();

        /*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.viewer.map;

        // initial image resolution, for later reset
        this._defaultResolution = this._map.getView().getResolution();

        // constrols display of markers on open layers image
        this._markerManager = new MarkerManager(this._map, true);
        // list of markers declared by user
        this._markers = [];
        // listeners of grid modified event
        this.gridEventListeners = [];
        this.sliceLoadedlisteners = [];

        this.updateSlice = this.updateSlice.bind(this);
        this._imageLoadFunction = this._imageLoadFunction.bind(this);
        this._getMap = this._getMap.bind(this);

    }

    changeZoom(level){
        //if(Number.isInteger(level)){
            const box = getBoxCoordinates();
            this._map.getView().setCenter([(box.iRA1+box.iRA0)/2, (box.iDEC1+box.iDEC0)/2]);
            this._map.getView().setZoom(this._map.getView().getZoom() + level);           
        //}
    }

    
    addSliceLoadedListener(listener){
        this.sliceLoadedlisteners.push(listener);
    }

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

    _executeSliceLoadedListener(event) {
        for (let l of this.sliceLoadedlisteners) {
            l.sliceLoaded(event);
        }
    }

    get map(){
        return this._map;
    }


    /**
     * Add a grid event listener
     * @param {object} listener 
     */
    addGridEventListener(listener) {
        this.gridEventListeners.push(listener);
    }

    /**
     * Removes a grid event listener
     * @param {object} listener 
     */
    removeGridEventListener = (listener) => {
        for (let i = 0; i < this.gridEventListeners.length; i++) {
            if (this.gridEventListeners[i] === listener) {
                this.gridEventListeners.splice(i, 1)
            }
        }
    }

    /**
     * Triggers grid modified event
     * @param {object} event 
     */
    _executeListeners(event) {
        for (let l of this.gridEventListeners) {
            l.updateGridCall(event);
        }
    }

    /**
     * Called when grid modified event has been received
     * @param {object} event 
     */
    updateGridCall(event) {
        this.axesFactory.getButtonClick();
    }

    /**
     * Sets the string RMS value with its unit
     * @param {number} value (float)
     * @param {string} unit
     */

    getRms(){
        return this._rms;
    }

    getRmsUnit(){
        return this._rmsUnit;
    }

    setRms(value, unit) {
        this._rms = Number.parseFloat(value);
        this._rmsUnit = unit;

        // writes values in dom
        this._setRmsLabel("rms=" + Number.parseFloat(value).toExponential(2) + ' ' + unit);
    }

    /**
     * Sets the string RMS value with its unit
     * @param {number} value (float)
     * @param {string} unit
     */
    setMean(value, unit) {
        this._mean = "mean=" + Number.parseFloat(value).toExponential(2) + ' ' + unit;
        this._setMeanLabel(this._mean);

    }

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

    /**
     * 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));
        let res =  this._projection.raDecToiRaiDec(event.detail.ra, event.detail.dec);
        try {
            this._markerManager.addMarker(res["iRa"], res["iDec"], event.detail["object"].trim());
            this._markers.push(new Marker(event.detail.ra, "", event.detail.dec, "", event.detail["object"].trim()));
        } 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();
        this._markers = [];
        for (const marker of event.detail.markers) {
            if (marker.ra !== undefined && marker.dec !== undefined && marker.label !== undefined) {
                this._markers.push(new Marker(marker.ra, "", marker.dec, "", marker.label));
                try {
                    let iRaiDec = this._projection.raDecToiRaiDec(HMS2DecDeg(marker.ra), DMS2DecDeg(marker.dec));
                    this._markerManager.addMarker(iRaiDec["iRa"], iRaiDec["iDec"], marker["label"].trim());
                } catch (error) {
                    alert(`Error with object RA : ${marker.ra}, DEC : ${marker.dec}, label : ${marker.label} : ${error} `);
                    throw error;
                }
            } else {
                alert(`Coordinates are syntaxically incorrect for object RA : ${marker.RA.value}, DEC : ${marker.DEC.value}, label : ${marker.label}`);
                throw new Erro("Coordinates are syntaxically incorrect");
            }
        }
    }

    /**
     * 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) {
        this.viewer.imageLoadFunction(image, src);
    }

    /**
     * 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
     * @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]);
        //console.log(olc);
        //if (pixelAtPosition) {
        //    let data_steps_index = pixelAtPosition.slice(0, 3).join('_');
        //    if (data_steps_index !== "255_255_255") {
                result = "RA=" + raDec['ra'] + ', DEC=' + raDec["dec"] + "  ";
                result += "x=" + Math.floor(olc[0]) + ', y=' + Math.floor(olc[1]);
        //    }
        //} else {
        //    result = "???";
        //}
        return result;
    }

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

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

    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {Event} event event triggered by the click
     */
    onclick(event) {
        throw new Error("This method must be implemented");
    }


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

    /**
     * Creates default buttons on the slice viewer
     * @param {object} hidden  list of hidden fields
     */
    _initCommonControl(hidden) {
        let self = this;
        
        // buttons in side bar
        var keyCodeProcessor = new KeyCodeProcessor(this.viewer);        
        keyCodeProcessor.open();
        
        this.customControls.addButton(this.selector.getButtonObject().getButton());
        for(let key of this.selector.getButtonObject().getKeyboardMapping()){
            keyCodeProcessor.teach(key, () => this.selector.getButtonObject().getButton().click());
        }

        /*let clickMarkerButton = new ClickMarkerButton(this.viewer);
        for(let key of clickMarkerButton.getKeyboardMapping()){
            keyCodeProcessor.teach(key, () => clickMarkerButton.getButton().click());
        }
        this.customControls.addButton(clickMarkerButton.getButton());*/

        if(!hidden.includes("boxes")){
            this.boxesFactory = new Boxes3DFactory(this.viewer); 
            this.customControls.addButton(this.boxesFactory.getButtonObject().getButton());
            for(let key of this.boxesFactory.getButtonObject().getKeyboardMapping()){
                keyCodeProcessor.teach(key, () => this.boxesFactory.getButtonObject().getButton().click());
            }

            // Pressing del|Suppr => remove the selected feature.
            // button is not displayed but key action is registered
            let deleteButton = new DeleteButton(this.viewer);
            for(let key of deleteButton.getKeyboardMapping()){
                keyCodeProcessor.teach(key, () => deleteButton.getButton().click());
            }
        }

        let axesFactory = new AxesFactory(this.viewer, FITS_HEADER.projectionType);
        axesFactory.build();
        let axesButton = axesFactory.getButtonObject();
        //axesButton.getButton().onclick = ()=>{axesFactory.getButtonClick()};   
        axesButton.setClickAction(()=>{axesFactory.getButtonClick()});     
        this.customControls.addButton(axesButton.getButton());
        axesFactory.getButtonClick()
        for(let key of axesButton.getKeyboardMapping()){
            keyCodeProcessor.teach(key, () => axesButton.getButton().click());
        }

        let reset = new ResetButton(this.viewer);
        reset.setClickAction(()=>{this.viewer.reset()});
        this.customControls.addButton(reset.getButton());
        for(let key of reset.getKeyboardMapping()){
            keyCodeProcessor.teach(key, () => reset.getButton().click());
        }

        // Pressing del|backspace => remove the selected feature.
        let deleteButton = new DeleteButton(this.viewer);
        for(let key of deleteButton.getKeyboardMapping()){
            keyCodeProcessor.teach(key, () =>  { if (this.selector.isOpened()) this.selector.removeSelection() });
        }

        
        //this.selector.open();
        //return controls;
    }

    enableSelectorMode(){
        this.selector.getButtonObject().getButton().click();
    }

    /**
     * Function to add more buttons to the default ones
     * declared in _initControl
     * @returns {array}
     */
    _initAdditionalControl() {
        return [];
    }
}


/**
 * 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.sliceIndex;
        this._lastClickMarker = '';
        this._initCommonControl(["boxes"]);
        this._initAdditionalControl();
        this.map_controls = this._getMapControl();
        //click in slice viewer
        this.viewer.addClickEvent((event) => { this.onclick(event) });
        this._getMapControl = this._getMapControl.bind(this);
    }

    setSliceIndex(sliceIndex) {
        this.sliceIndex = sliceIndex;
    }

    onclick(event) {
        if(!this.selector.isOpened()){
            //is this test still useful ?
	        let localName = "";
            try{
		        localName = event.originalEvent.target.localName;
            }catch(e){
                console.log(e);
            }

            if(event.originalEvent == null || localName == "canvas"){            
                // keep old x axis limits before replot
                if(this._viewLinker.isRefreshable){
                    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.markLastClickInSlices(event.coordinate);
                        this._viewLinker.summedSlicesImage.onclick(event);
                        this._viewLinker.spectrumViewer.setFrequencyMarker(this.sliceIndex);
                        //DOMAccessor.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.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(
                                        this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
                                        this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
                                        [this._viewLinker.summedPixelsSpectrumViewer.getSpectrumChartXAxis(), 
                                            this._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
                                }
                                // keep current zoom level
                                this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
                            });
                    }
                }else{
                    alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
                }
            }
        }


    }

    /**
     * Function to add more buttons to the default ones
     * declared in _initControl
     */
    _initAdditionalControl() {
        const apiQuery = new ServerApi();
        // open image in 2D viewer
        let view2d = new View2DButton(this.viewer);
        view2d.setClickAction(()=>{
            //this._sendSliceWithSamp();
            let getPath = apiQuery.getFITSSliceImage(this.sliceIndex, 
                                                     this._viewLinker.relFITSFilePath,
                                                     (resp)=>{});
            getPath.then(x => {
                let res = JSON.parse(x);
                let url = URL_3D_TO_2D + res.result;
                window.open(url);
            });           
        });
        this.customControls.addButton(view2d.getButton());
    
        //download image
        if (yafitsTarget === "OBSPM") {
            let saveImage = new SaveImageButton(this.viewer);
            saveImage.setClickAction(()=>{
                let getPath = apiQuery.getFITSSliceImage(this.sliceIndex, 
                                                         this._viewLinker.relFITSFilePath,
                                                         (resp)=>{});
                getPath.then(x => {
                    let res = JSON.parse(x);
                    let url = URL_ROOT + res.result;
                    window.open(url, '_blank');
                });         
            });
            this.customControls.addButton(saveImage.getButton());            
        }

        if (withSAMP) {
            let sampButton = new PublishSAMPButton(this.viewer);
            //setOnHubAvailability(customControls,sampButton);
            sampButton.setClickAction(()=>{
                let getPath = apiQuery.getFITSSliceImage(this.sliceIndex, 
                    this._viewLinker.relFITSFilePath,
                    (resp)=>{});
                        getPath.then(x => {
                        let res = JSON.parse(x);
                        let url = URL_ROOT + res.result;
                        let parts = this._viewLinker.relFITSFilePath.split('/');
                         // sends fits file and png at the same time
                        sAMPPublisher.sendFitsImageToAll(url, parts[parts.length - 1],
                        parts[parts.length - 1] + "-" + self.sliceIndex);
                        sAMPPublisher.sendMarkers(self._markers);
                        });
                        sAMPPublisher.sendPNGSlice();
                        sAMPPublisher.sendMarkers(this._markers)
            });
            setOnHubAvailabilityButtons(DOMAccessor.get3DSampConnection(), this.customControls, sampButton);     
        }      
    }

    /**
     * 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: DOMAccessor.getSingleSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this.coordinateFormat(olc) }
            }),
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: DOMAccessor.getSummedSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this._viewLinker.summedSlicesImage.coordinateFormat(olc) }
            }),
            new ol.control.FullScreen()
        ]/*.concat(this._initControl()).concat(this._extendControl())*/;
    }


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

        console.log("### EXTENT");
        console.log(this._viewLinker.extent);

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

        // update color bar
        DOMAccessor.updateSingleSliceColorBar(URL_ROOT + "/" + data["path_to_legend_png"]);
        this.setRms(parseFloat(data["statistics"]["stdev"]), FITS_HEADER.bunit);
    }


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

    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {object} fn a function
     */
    _setMeanLabel = (text) => {
        DOMAccessor.setSliceMean(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: Math.round(FITS_HEADER.naxis3/2) - Math.round(FITS_HEADER.naxis3/8) ,
            iFREQ1: Math.round(FITS_HEADER.naxis3/2) + Math.round(FITS_HEADER.naxis3/8) ,*/
            iFREQ0: 0,
            iFREQ1: FITS_HEADER.naxis3 - 1
        };

        // last position clicked in the image is initialized at initial pixel value
        const initialPixel = FITS_HEADER.getCentralPixelPosition();
        this._lastClick = {
            iRA: initialPixel[0],
            iDEC: initialPixel[1]
        }

        // 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._boxSource = new ol.source.Vector({
            wrapX: false
        });

        this._boxLayer = new ol.layer.Vector({
            source: this._boxSource
        });

        this._polygon = null;

        //private attributes
        this._select = this.getSelect();
        this._dragBox = this._getDragBox();
        this._initCommonControl([]);
        this._initAdditionalControl();
        this.map_controls = this._getMapControl();

        this.selectAction = (feature)=>{
            //  this.selectedBox = e.selected[0];                
            this.selectedBox = feature;
            var extent = feature.getGeometry().getExtent();

            // keep current limits for x axis when refreshing the plot
            this._viewLinker.summedPixelsSpectrumViewer.replot(extent[0], extent[2], extent[1], extent[3]);

            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]);            
        }
        this.boxesFactory.setSelectAction(this.selectAction);


        this._map.addInteraction(this._select);

        //click in summed slice viewer
        this.viewer.addClickEvent((event) => { this.onclick(event) });

        this._imageLoadFunction = this._imageLoadFunction.bind(this);
        this.onclick = this.onclick.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);
    }

    get boxSource() {
        return this._boxSource;
    }

    _sendSliceWithSamp(){
	const apiQuery = new ServerApi();
        let getPath = apiQuery.getSummedFITSSliceImage( this.sliceIndex0, 
                                                        this.sliceIndex1, 
                                                        this._viewLinker.relFITSFilePath,
                                                        (resp)=>{});
        getPath.then(x => {
            let res = JSON.parse(x);
            let url = URL_ROOT + res.result;
            let parts = this._viewLinker.relFITSFilePath.split('/');
            // sends fits image
            sAMPPublisher.sendFitsImageToAll(url, parts[parts.length - 1], parts[parts.length - 1] + "-" + self.sliceIndex);
            sAMPPublisher.sendMarkers(self._markers);
        });
        sAMPPublisher.sendPNGSummedSlices();
        sAMPPublisher.sendMarkers(this._markers);
    }

    drawInitialBox(){
        const box = getBoxCoordinates();
        /*const iRA0 = 0;
        const iRA1 = naxis1Index;
        const iDEC0 =0;
        const iDEC1 =naxis2Index;*/
        const coords = [box.iRA0, box.iRA1, box.iDEC0, box.iDEC1];
        this.boxesFactory.addBox(coords[0],coords[1],coords[2],coords[3]);
        let pf = this.boxesFactory.source.getFeatures()[0];
        this._polygon = pf.getGeometry();

        let select = this.getSelect();
        this.map.addInteraction(select);
        select.getFeatures().push(pf);
        select.dispatchEvent({
            type: 'select',
            selected: [pf],
            deselected: []
        });
        
    }

   /**
     * Function to add more buttons to the default ones
     * declared in _initControl
     */
   _initAdditionalControl() {
        const apiQuery = new ServerApi();

        // open image in 2D viewer
        let view2d = new View2DButton(this.viewer);
        view2d.setClickAction(()=>{
            let getPath = apiQuery.getSummedFITSSliceImage(this.sliceIndex0, 
                                                this.sliceIndex1,  
                                                    this._viewLinker.relFITSFilePath,
                                                    (resp)=>{});
            getPath.then(x => {
                let res = JSON.parse(x);
                let url = URL_3D_TO_2D + res.result;
                window.open(url);
            });           
        });
        this.customControls.addButton(view2d.getButton());

        //download image
        if (yafitsTarget === "OBSPM") {
            let saveImage = new SaveImageButton(this.viewer);
            saveImage.setClickAction(()=>{
                var getPath = apiQuery.getSummedFITSSliceImage( this.sliceIndex0, 
                                                                this.sliceIndex1, 
                                                                this._viewLinker.relFITSFilePath, 
                                                                (resp)=>{});
                getPath.then(result => {
                    var res = JSON.parse(result);
                    var url = URL_ROOT + res.result;
                    window.open(url, '_blank');;

                });                        
            });
            this.customControls.addButton(saveImage.getButton());
        }

        if (withSAMP) {
            let sampButton = new PublishSAMPButton(this.viewer);
            //setOnHubAvailability(customControls,sampButton);
            sampButton.setClickAction(()=>{
                let getPath = apiQuery.getSummedFITSSliceImage( this.sliceIndex0, 
                                                                this.sliceIndex1, 
                                                                this._viewLinker.relFITSFilePath, 
                                                                (resp)=>{});
                getPath.then(x => {
                    let res = JSON.parse(x);
                    let url = URL_ROOT + res.result;
                    let parts = this._viewLinker.relFITSFilePath.split('/');
                    // sends fits image
                    sAMPPublisher.sendFitsImageToAll(url, parts[parts.length - 1], parts[parts.length - 1] + "-" + self.sliceIndex);
                    sAMPPublisher.sendMarkers(self._markers);
                });
                sAMPPublisher.sendPNGSummedSlices();
                sAMPPublisher.sendMarkers(this._markers)

            });
            setOnHubAvailabilityButtons(DOMAccessor.get3DSampConnection(), this.customControls, sampButton);     
        }      
    }    

    onclick(event) {      
        // true if event is a click from another map object (i.e the other slice)
        let singleSliceClick = false;
        if(event.type==="click" && this.viewer.map.ol_uid !== event.target.ol_uid ){
            singleSliceClick = true;;
        }        

        // the selector object is closed or user clicked on a pixel in the other slice
        if(!this.selector.isOpened() || singleSliceClick === true ){
            if(event.originalEvent == null || event.originalEvent.target.localName == "canvas"){
                if(this._viewLinker.isRefreshable){
                    if (event.coordinate !== undefined) {
                        this._lastClick.iRA = Math.round(event.coordinate[0]);
                        this._lastClick.iDEC = Math.round(event.coordinate[1]);
                    }
                    
                    let apiQuery = new ServerApi();
                    apiQuery.getSummedSliceValueAtPixel(
                        this._lastClick.iRA, this._lastClick.iDEC,
                        this.regionOfInterest.iFREQ0, this.regionOfInterest.iFREQ1,
                        this._viewLinker.relFITSFilePath,
                        (resp)=>{                            
                            // 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"];
                            const pixelValue = result["result"][0][0];

                            if (result["status"] && pixelValue !== null){
                                
                                this._viewLinker.spectrumViewer.setFrequencyMarker(this._viewLinker.getSliceIndex());
                                //let self = this;
                                this._viewLinker.spectrumViewer.plot(this._lastClick.iRA,
                                    this._lastClick.iDEC/*, () => {
                                        // set old x axis limits
                                        self._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
                                    }*/);
                
                
                                if (event.coordinate !== undefined)
                                    this._viewLinker.markLastClickInSlices(event.coordinate);
                
                                // event is only a value refresh request, i.e. it comes from an interval selection on the spectrum
                                if (event.type === EventFactory.EVENT_TYPES.refreshFrequency) {
                                    this._viewLinker.setFluxDensityInSummedPopup(pixelValue, true);
                                }
                                // other event, i.e. a click on the image
                                else {
                                    this._viewLinker.setFluxDensityInSummedPopup(pixelValue, false);
                                }
                            }
                        });            
                }else{
                    alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
                }
            }
        }
    }

    /**
     * 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: DOMAccessor.getSingleSliceMousePosition(),
                undefinedHTML: '',
                coordinateFormat: (olc) => { return this._viewLinker.singleSliceImage.coordinateFormat(olc) }
            }),
            new ol.control.MousePosition({
                className: 'custom-mouse-position',
                target: DOMAccessor.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 a ol.interaction.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) {
                for(let i=0; i<this._boxSource.getFeatures().length; i++){
                    if(this._boxSource.getFeatures()[i] !== e.selected[0]){
                        this._boxSource.getFeatures()[i].setStyle(standardFeature(this._boxSource.getFeatures()[i]));
                    }else{
                        this._boxSource.getFeatures()[i].setStyle(highlightedFeature(this._boxSource.getFeatures()[i]));
                    }
                }
                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]);

                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]);
            }
            console.log(e);
       });

        return select;
    }

    /**
     * Returns a DragBox object, defining what happens when user creates a box in the slice 
     * @returns {DragBox} ol.interaction.DragBox
     */
    _getDragBox() {
        let interaction = new ol.interaction.Select({ condition: ol.events.click, hitTolerance: 2 });
       
        const translate = new ol.interaction.Translate({
            features: interaction.getFeatures(),
        });
        this.viewer.map.addInteraction(translate); 

        let dragBox = new ol.interaction.DragBox();
        let self = this;
        dragBox.on('boxend', (e) => {
            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 pf = new ol.Feature({
                    geometry: new ol.geom.Polygon([corners])
                });               

                pf.setStyle(standardFeature(pf));
                this._boxSource.addFeature(pf);

                let select = new ol.interaction.Select();
                let translate = new ol.interaction.Translate({
                    features : pf
                });
        
                this._map.addInteraction(translate);
                this._map.addInteraction(select);
            } else {
                alert("At least one of selected points have no value.");
            }
        });
        dragBox.on('boxdrag', () => {

        });

        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._boxLayer);
        }

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

        // update color bar
        DOMAccessor.updateSingleSummedColorBar(URL_ROOT + "/" + data["path_to_legend_png"]);
        this.setRms(parseFloat(data["statistics"]["stdev"]), FITS_HEADER.bunit);



    }

    /**
     * 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._boxSource.removeFeature(this.selectedBox);
            this._boxSource.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) => {
        DOMAccessor.setSummedSliceRMS(text);
    }


    /**
     * 
     * This is an abstract function that must be implemented in a derived class
     * @param {object} fn a function
     */
    _setMeanLabel = (text) => {
        DOMAccessor.setSummedSliceMean(text);
    }
}

export {
    Slice, SingleSlice, SummedSlice, getSliceUpdateEvent
}