Source: public/javascript/modules/olqv_markers.js

import { ShapesFactory } from "./olqv_shapes.js";
import { FITS_HEADER } from "./fitsheader.js";
import { getProjection } from "./olqv_projections.js";
import { createAndAppendFromHTML, inRange, DecDeg2HMS, DecDeg2DMS, degToRad, HMS2DecDeg, DMS2DecDeg } from "./utils.js";
import { EventFactory } from './customevents.js'
import {MarkerButton} from './olqv_olbuttons.js';

class Marker{
    constructor(RA, RAunit, DEC, DECunit, label){
        this.RA = {"value" : RA, "unit" : RAunit};
        this.DEC = {"value" : DEC, "unit" : DECunit};
        this.label = {"value" : label};
    }
}

/**
 * Class representing an input list of markers 
 */
class MarkerList {
    /**
     * @constructor
     * @param {string} dataId  id of element containing the list of markers
     * @param {string} triggerId id of element triggering the list validation
     * @param {string} clearId id of the element triggering a data clearing
     */
    constructor(dataId, triggerId, clearId) {
        // object listening events from this list
        this._listeners = [];
        this._markers = new Set([]);
        this._dataElement = document.getElementById(dataId);
        this._dataElement.value = "";
        this._clearElement = document.getElementById(clearId);
        this._triggerElement = document.getElementById(triggerId);

        this._triggerElement.addEventListener("click", ()=>{this._updateMarkers()});
        this._clearElement.addEventListener("click", () => { this._clearMarkers() });

    }

    /**
     * Adds a markers in the list corresponding to an entry clicked in the NED table
     * Action triggered by a SourceTable instance
     * 
     * @param {Event} event event sent by a SourceTable instance
     */
    sourceTableCall(event) {
        if (!this._markers.has(event.detail.object)) {
            console.log("### source table call");
            console.log(event.detail.ra + "  " + event.detail.dec);
            console.log(DecDeg2HMS(event.detail.ra) + "  " + DecDeg2DMS(event.detail.dec));
            const source = DecDeg2HMS(event.detail.ra) + ";" + DecDeg2DMS(event.detail.dec) + ";" + event.detail.object;
            if (this._dataElement.value.trim() !== "")
                this._dataElement.value = this._dataElement.value + "\n" + source.trim();
            else
                this._dataElement.value = source.trim();
            let marker = new Marker(event.detail.object.ra, "",  
                                    event.detail.object.dec, "",  
                                    event.detail.object.label);
            this._markers.add(marker);
            //this._markers.add(event.detail.object);
        }
    }
    /**
     * Adds an object listening to event from this object
     * @param {Object} listener 
     */
    addListener(listener) {
        this._listeners.push(listener);
    }

    /**
     * Removes an object listening to event from this object
     * @param {Object} listener 
     */
    removeListener(listener) {
        for (let i = 0; i < this._listeners.length; i++) {
            if (this._listeners[i] === listener) {
                this._listeners.splice(i, 1);
                return true;
            }
        }
    }

    /**
     * Returns the event sent to listening objects
     * It contains RA, DEC coordinates and a label of an object
     *
     * @returns {CustomEvent}
     */
    _getEvent() {
        let data = this._dataElement.value;
        const lines = data.trim();
        let markers = [];

        if(lines !== ""){
            for (let line of lines.split("\n")) {
                const marker = line.split(";");
                markers.push({ "ra": marker[0], "dec": marker[1], "label": marker[2] });
            }
        }

        let event = EventFactory.getEvent(EventFactory.EVENT_TYPES.custom, {
            detail: {
                markers: markers,
                action: "plot"
            }
        });

        return event;
    }

    /**
     * Notifies all listeners that the list has been updated
     */
    _updateMarkers() {
        const event = this._getEvent();
        for (let l of this._listeners) {
            try{
                l.markerListUpdate(event);
            }catch(error){
                return;
            }
        }
    }

    /**
     * Notifies all listeners that the list has been cleared
     */
    _clearMarkers() {
        this._dataElement.value = "";
        this._markers.clear();
        for (let l of this._listeners) {
            l.markerListClear();
        }
    }
}


/**
 * Display markers on a map, used in 3D
 */
class MarkerManager {
    /**
     * @constructor
     * @param {Map} map target open layer Map object
     * @param {boolean} isMultiMarkers displays only last marker if false, all markers if true
     */
    constructor(map, isMultiMarkers) {
        this._map = map;
        this._multiMarkers = isMultiMarkers;
        this._source = new ol.source.Vector();
        this._layer = new ol.layer.Group({});
        this._layer.getLayers().push(new ol.layer.Vector({ source: this._source, style: null }));
        this._markers = new Set([]);
        this._initialized = false;
    }

    getMarkers(){
        return this._markers;
    }

    /**
     * Adds a new marker on map
     * @param {number} pixRa RA in pixels (int)
     * @param {number} pixDec DEC in pixels (int)
     * @param {string} label label of the marker
     */
    addMarker(pixRa, pixDec, label) {
        const marker_id = label.trim();
        // remove previous markers
        if (!this._multiMarkers) {
            this._source.clear();
        }

        //this does not work if done in constructor, why ?
        if (!this._initialized) {
            this._map.addLayer(this._layer);
            this._initialized = true;
        }

        if (!this._markers.has(marker_id)) {
            let f = new ol.Feature({ geometry: new ol.geom.Point([pixRa, pixDec]) });
            f.setStyle(this._getStyle(label));
            this._source.addFeature(f);
            this._markers.add(marker_id);
            this._source.refresh();
        }
    }

    /**
     * Removes all markers
     */
    clearMarkers() {
        this._source.clear();
        this._markers.clear();
        this._source.refresh();
    }

    /**
     * Returns a Style object defining a marker
     * @param {string} label 
     * @returns {Style} an open layers Style object
     */
    _getStyle(label) {
        return new ol.style.Style({
            image: new ol.style.Circle({
                radius: 4,
                stroke: new ol.style.Stroke({
                    color: '#3399CC',
                    width: 2
                }),
                fill: new ol.style.Fill({
                    color: "magenta"
                })
            }),
            text: new ol.style.Text({
                font: '16px Calibri,sans-serif',
                fill: new ol.style.Fill({ color: '#000' }),
                stroke: new ol.style.Stroke({
                    color: '#fff',
                    width: 2
                }),
                offsetX: 10,
                offsetY: -10,
                text: label
            })
        });
    }
}


/**
 * A class to create and to manage markers in the 2D part of YAFITS. A marker is basically a record defined by a position, the pixel value at that position and a label on a 2D map. With YAFITS on a 2D map, one can use two possible interfaces to work on the collection of markers.
 * <ul> 
 * <li>The graphic interface displays the markers above the map. It uses the {@link https://openlayers.org/en/v5.3.0/apidoc/| OpenLayers API}. </li>
 * <li>The spreadsheet interface offers an "<i>à la Excel</i>" interaction with the collection. It uses the {@link https://bossanova.uk/jspreadsheet/v3/| Jexcel API}. </li>
 * </ul> 
 * Both interfaces allow to edit the collection of markers.
 * As announced above, a marker is a aggregate of informations which is implemented in a dictionary whose keys are defined as follows.

 * <ul>
 * <li><tt>id</tt> (integer) - an id for that marker. Two # markers have # ids. id == -1 invalid or empty marker.</li>
 * <li><tt>label</tt> (string) - a label for that marker. No constraint.</li>
 * <li><tt>RA</tt> (string) - a valid representation of a right ascension.</li>
 * <li><tt>DEC</tt> (string) - a valid representation of a declination.</li>
 * <li><tt><i>type</i> (<i>unit</i>)</tt> (number) - the pixel value. The field is named after FITS <tt>BTYPE</tt> and <tt>BUNIT</tt>.</li>
 * </ul>
 * 
 * <dl>
 * <dt> On the spreadsheet </dt> <dd> a marker is stored in one row of a {@link https://bossanova.uk/jspreadsheet/v3/| Jexcel API} table with one value associated to one key per column. </dd>
 * <dt> On the graphics interface </dt> <dd> a marker is stored in an OpenLayers {@link https://openlayers.org/en/v5.3.0/apidoc/module-ol_Feature-Feature.html|feature} </dd>
 * </dl>
 * 
 * It's the software responsibility to maintain the respective contents of the jexcel sheet and the collection of features dedicated to makers in sync.
 *  @extends ShapesFactory
 */
class MarkersFactory extends ShapesFactory {

    static enter(what) {
        //console.group(this.name + "." + what);
    }

    static exit() {
        //console.groupEnd();
    }

    /**
     * @constructor
     * Creates an instance.
     * 
     * @see {@link ./Viewer.html|Viewer}
     * @param {Viewer} viewer instance of Viewer class that will host the markers.
     */
    constructor(viewer) {
        super(viewer, 'marker');
        MarkersFactory.enter(this.constructor.name);
        let self = this;
        let context = this.infosBlock.getContext();
        this.btype = context['array-phys-chars']['type'];
        this.bunit = context['array-phys-chars']['unit'];
        this.pixelValueColumnName = `${this.btype} (${this.bunit})`;
        this.mode = this;
        this.modalMarkersFormId = `ModalMarkersForm-${this.viewerId}`;
        this.overlayId = `markersOverlay-${this.viewerId}`;
        this.textOfMarkersId = `text_of_markers-${this.viewerId}`;
        this.clearMarkersId = `clear_markers-${this.viewerId}`;
        this.copyVerbatimId = `copy_verbatim-${this.viewerId}`;
        this.copyAsJSONId = `copy_as_json-${this.viewerId}`;
        this.refreshMarkersDisplayId = `refresh_markers_display-${this.viewerId}`;
        this.closeMarkersId = `close_markers-${this.viewerId}`;

        //this.raLabelFormatter = this.viewer.getCoordinatesFormatter().getRaLabelFormatter();
        //this.decLabelFormatter = this.viewer.getCoordinatesFormatter().getDecLabelFormatter();
        //this.raDDtoPixelConverter = this.viewer.getCoordinatesFormatter().getRaDDtoPixelConverter();
        //this.decDDtoPixelConverter = this.viewer.getCoordinatesFormatter().getDecDDtoPixelConverter();
        this.projection = getProjection(FITS_HEADER.projectionType);
        this.indexToMarker = {};
        this.selectedRowIndexes = [];
        this.layer.setZIndex(13);

        this.buttonObject = new MarkerButton(this.viewer);

        this.seetablebutton = document.createElement("button");
        this.seetablebutton.setAttribute("class", "btn btn-primary btn-sm");
        this.seetablebutton.setAttribute("data-tooltip", "tooltip");
        this.seetablebutton.setAttribute("title", "Markers in a spreadsheet");
        this.seetablebutton.append(" See markers table");
        this.seetablebutton.onclick = () => { $(`#${this.overlayId}`).css("height", "100%"); };

        this.interaction = new ol.interaction.Draw({ source: this.source, type: 'Point' });
        let f = function () {
            if (self.isOpened) {
                self.close();
                self.buttonObject.unsetActive();
            } else {
                self.open(self.interaction, 'crosshair');
                self.buttonObject.setActive();
            }
        }
        this.buttonObject.setClickAction(f);
        this.numOfMarkers = 0;
        this.markerIndex = 0;
        this.data = [];

        this.html = `
            <div class="overlay" style="overflow:hidden" id="${this.overlayId}">
                <div class="row">
                    <div class="col">
                       <h1> Markers spreadsheet </h1>
                       <hr/>
                    </div>
                </div>
                <div class="row">
                    <div class="col">
                        <div class="btn-toolbar" role="group" aria-label="Markers actions">
                            <button type="button" class="btn btn-success ml-1" id="${this.refreshMarkersDisplayId}">Refresh display</button>
                            <button type="button" class="btn btn-success ml-1" id="${this.clearMarkersId}">Clear</button>
                            <button type="button" class="btn btn-primary btn-sm fas fa-files-o ml-1" id="${this.copyVerbatimId}" data-tooltip="tooltip" title="Copy verbatim"></button>
                            <button type="button" class="btn btn-primary btn-sm ml-1" id ="${this.copyAsJSONId}">Copy as JSON</button>
                            <button type="button" class="btn btn-primary ml-1" id="${this.closeMarkersId}">Back to image</button>
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col">
                        <hr/>
                        <div id="${this.textOfMarkersId}"></div>
                        <hr/>
                    </div>
                </div>
                <textarea style="width:0px;height:0px;opacity:0"></textarea>
            </div>
        `;
        this.body = $("body")[0];
        this.overlay = createAndAppendFromHTML(this.html, this.body);
        this.hiddenInfosBlock = this.overlay.querySelector("textarea");

        /**
         * @member {dictionary[]} - Defines the title, type, width in pixels, name and read/writability of the jexcel sheet's columns.
         */
        this.columns = [
            { title: 'id', type: 'numeric', width: '50px', name: 'id', readOnly: true },
            { title: 'label', type: 'text', width: '100px', name: 'label', readOnly: false },
            { title: 'RA', type: 'text', width: '150px', name: 'RA', readOnly: false },
            { title: 'DEC', type: 'text', width: '150px', name: 'DEC', readOnly: false },
            { title: 'iRA', type: 'text', width: '150px', name: 'iRA', readOnly: true },
            { title: 'iDEC', type: 'text', width: '150px', name: 'iDEC', readOnly: true },
            { title: this.pixelValueColumnName, type: 'numeric', width: '150px', name: this.pixelValueColumnName, readOnly: true }
        ];
        this.fieldNames = this.columns.map((o) => { return o.title });
        this.jexcel = this.createJexcel(`${this.textOfMarkersId}`);
        this.n2i = {};
        this.n2i = { 'id': 0, 'label': 1, 'RA': 2, 'DEC': 3 };
        this.n2i[this.pixelValueColumnName] = 4;

        /*
         ** What is happening when a point has been drawn ( with a simple click ) ?
         */
        this.interaction.on("drawend", event => {
            MarkersFactory.enter("drawend callback");
            this.getMarkerFluxAndRecord(event.feature);
            /*const [iRA, iDEC] = event.feature.getGeometry().getCoordinates();
            const [iRAMax, iDECMax] = this.viewer.maxIndexes;
            if ((inRange(iRA, 0, iRAMax)) && inRange(iDEC, 0, iDECMax)) {
                var p = this.getPixelValuePromise(iRA, iDEC, this.viewer.getRelFITSFilePath());
                p.then(
                    result => {
                        this.recordMarker(event.feature, result);
                        this.numOfMarkers += 1
                    },
                    error => { alert(error) }
                )
            } else {
                alert(`Coordinates (${iRA}, ${iDEC} out of field`);
                this.remove(event.feature);
            }*/
            MarkersFactory.exit();
        });

        $(`#${this.textOfMarkersId}`).bind('input propertychange', () => {
            $("#place_markers").attr('disabled', true);
        });

        $(`#${this.refreshMarkersDisplayId}`).click(() => {
            MarkersFactory.enter("refreshMarkers")
            this.deleteMarkerFeatures(this.getAllMarkerFeatures());
            this.jexcel2OL();
            MarkersFactory.exit();
        });

        $(`#${this.clearMarkersId}`).click(() => {
            $(`#${this.textOfMarkersId}`).val('');
            this.deleteMarkerFeatures(this.getAllMarkerFeatures());
            this.indexToMarker = {};
            this.markerIndex = 0;
            this.data = [];
            this.jexcel.setData(this.data);
        });

        $(`#${this.copyVerbatimId}`).click(() => {
            MarkersFactory.enter("copy verbatim");
            let context = { "file": `${this.viewer.getRelFITSFilePath()}` }
            if (this.viewer.is3D()) {
                context['slice range'] = `${this.viewer.getSliceRange()}`;
            }

            let collection = this.getMarkersList();
            this.hiddenInfosBlock.value = `Markers\r\n\r\n`;
            this.hiddenInfosBlock.value += this.infosBlock.preamble2TXT();

            collection.forEach(element => {
                for (var k in element) {
                    const newLocal = `${k}:${this.format(element[k]["value"])} ${element[k]["unit"]}\r\n`;
                    this.hiddenInfosBlock.value += newLocal;
                }
                this.hiddenInfosBlock.value += '\r\n';
            });

            this.hiddenInfosBlock.select();
            try {
                var success = document.execCommand('copy');
            } catch (err) {
                alert("Copy failed. " + err);
            }
            MarkersFactory.exit();
        });

        $(`#${this.copyAsJSONId}`).click(() => {
            MarkersFactory.enter("copy as JSON");

            let collection = this.getMarkersList();
            //let data = this.getDataAsJSON();
            this.hiddenInfosBlock.value = JSON.stringify($.extend(this.infosBlock.preamble2DICT(), { 'markers': collection }), 0, 4);
            this.hiddenInfosBlock.select();
            try {
                var success = document.execCommand('copy');
            } catch (err) {
                alert("Copy failed. " + err);
            }
            MarkersFactory.exit();
        });

        $(`#${this.closeMarkersId}`).click(() => { $(`#${this.overlayId}`).css("height", "0%") });

        /*
         ** This the behaviour when a marker has been selected.
         */
        this.selector.select['Point'] = (feature) => {
            MarkersFactory.enter("A marker is selected");
            this.selected(feature);
            MarkersFactory.exit();
        };

        /*
         ** This the behaviour when a marker has been unselected.
         */
        this.selector.unselect['Point'] = (feature) => {
            MarkersFactory.enter("A marker is unselected");
            this.unselected(feature);
            MarkersFactory.exit();
        };


        /*
         ** This is the behaviour when a selected marker is going to be removed.
         */
        this.selector.remove['Point'] = (f) => {
            ShapesFactory.enter("A selected marker is about to be removed");
            this.removeFromJExcel(f);
            this.remove(f);
            ShapesFactory.exit();
        };

        MarkersFactory.exit();
    }

    getMarkerFluxAndRecord(feature){
        const [iRA, iDEC] = feature.getGeometry().getCoordinates();
        const [iRAMax, iDECMax] = this.viewer.maxIndexes;
        if ((inRange(iRA, 0, iRAMax)) && inRange(iDEC, 0, iDECMax)) {
            var p = this.getPixelValuePromise(iRA, iDEC, this.viewer.getRelFITSFilePath());
            p.then(
                result => {
                    this.recordMarker(feature, result);
                    this.numOfMarkers += 1;
                    this.map.render();
                },
                error => { 
                    if(error.includes("IndexError"))
                        // IndexError message from python server
                        alert(error.split("-")[1].split("'")[1]);
                    else
                        // any other error
                        alert(error);
                }
            )
        } else {
            alert(`Coordinates (${iRA}, ${iDEC} out of field`);
            //this.remove(feature);
        }
    }

    addMarker(iRa, iDec, label){
        let f = new ol.Feature({ geometry: new ol.geom.Point([iRa, iDec]) });
        f.set("label", label);
        f.setStyle(this.style_f(f));
        return f;
    }

    /**
     * Returns an array of objects representing markers with all their properties
     * @returns array
     */
    getMarkersList(){
        let collection = [];
        let data = this.getDataAsJSON();

        for(let marker of data){
            let d = {};
            for(let prop of Object.keys(marker)){
                let unit = "";
                if(prop === 'iRA' || prop === 'iDEC')
                    unit = "pixels";

                d[prop] = {"value" : marker[prop], "unit": unit };
            }
            collection.push(d);
        }
        return collection;

    }

    /**
     * Behaviour when an instance is activated.
     * 
     * @see {@link https://openlayers.org/en/v5.3.0/apidoc/module-ol_interaction_Interaction-Interaction.html|Interaction }
     * @see {@link https://developer.mozilla.org/fr/docs/Web/CSS/cursor | cursor}
     * @param {Interaction} interaction 
     * @param {cursor} cursor 
     */
    open(interaction, cursor) {
        MarkersFactory.enter(this.open.name);
        super.open(interaction, cursor);
        this.infosBlock.getInfosLine().append(this.seetablebutton);
        MarkersFactory.exit();
    }

    /**
     * Utility. Transforms an array into a dictionary. The values are taken in a, the keys in this.fieldnames
     * 
     * Requires that condition is that a and fieldnames have the same length. Otherwise an empty dictionary 
     * is returned.
     * 
     * @param {any[]} a 
     * @returns {dictionary}
     */
    a2O(a) {
        MarkersFactory.enter(this.a2O.name);
        let a2O_result = {};
        if (this.fieldNames.length == a.length) {
            for (let i = 0; i < a.length; i++) {
                a2O_result[this.fieldNames[i]] = a[i];
            }
        }
        MarkersFactory.exit();
        return a2O_result;
    }

    /**
     * Utility. Transforms a jexcel spreadsheet as returned by {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| getData}, i.e. an array of arrays, into an array of dictionaries.
     * @returns {dictionary[]}
     */
    getDataAsJSON() {
        MarkersFactory.enter(this.getDataAsJSON.name);
        let json_result = [];
        let data = this.jexcel.getData();
        for (let i = 0; i < data.length; i++) {
            let result = this.a2O(data[i]);
            json_result.push(result);
        }
        MarkersFactory.exit();
        return json_result;
    }

    /**
     * Callback. Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onselection} behaviour 
     * of the spreadsheet after some selection has been performed. Here the 'id' field(s) of the selected row(s) are recorded.
     * 
     * @param {jexcel-sheet} sheet 
     * @param {number} x1 
     * @param {number} y1 
     * @param {number} x2
     * @param {number} y2 
     */
    noteSelectedRows(sheet, x1, y1, x2, y2) {
        MarkersFactory.enter(this.noteSelectedRows.name);
        this.selectedRowIndexes = this.jexcel.getSelectedRows(true).map(i => this.jexcel.getRowData(i)[this.n2i['id']]);
        this.selectedRowIndexes = this.selectedRowIndexes.filter(i => i != -1);
        MarkersFactory.exit();
    }

    /**
     * Callback. Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| oninsertrow} behaviour
     * once a new (empty) row has been inserted. Here the 'id' of that row is forced to -1.
     * @param {jexcel-sheet} sheet not used in this implementation
     * @param {number} rowNumber the index number of the row to be inserted.
     * @param {number} numOfRows not used in this implementation
     * @param {number} rowRecords not used in this implementation
     * @param {boolean} insertBefore how the insertion is performed.
     */
    setId(sheet, rowNumber, numOfRows, rowRecords, insertBefore) {
        MarkersFactory.enter(this.setId.name);
        var i = insertBefore ? rowNumber : rowNumber + 1;
        this.data = this.getDataAsJSON();
        this.data[i]["id"] = -1;
        this.data[i][this.pixelValueColumnName] = "undefined";
        this.jexcel.setData(this.data);
        MarkersFactory.exit();
    }

    /**
     * Callback.  Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onbeforechange} behaviour (i.e. just before a change is performed).
     * @param {jexcel-sheet} sheet not used in this implementation
     * @param {jexcel-row} cell not used in this implementation
     * @param {number} x not used in this implementation
     * @param {number} y not used in this implementation
     * @param {any} value not used in this implementation
     */
    b4change(sheet, cell, x, y, value) {
        MarkersFactory.enter(this.b4change.name);
        MarkersFactory.exit();
    }

    /**
     * Utility. Checks the content of a row considered as a dictionary. Here it verifies
     * that RA and DEC fields are valid strings to express right ascension and declination.
     * @param {dictionary} row 
     * @returns {boolean}
     */
    validateRow(row) {
        MarkersFactory.enter(this.validateRow.name);
        var result = this.checkRADEC(row.RA, row.DEC);
        MarkersFactory.exit();
        return result;
    }

    /**
     * Promise. Connect the get pixel value asynchronous calls
     * with the consumer of the result to these calls.
     * 
     * @param {number} iRA              - the integer first coordinate of the pixel of interest   
     * @param {number} iDEC             - the integer second coordinate of the pixel of interest
     * @param {string} relFITSFilePath  - the FITS file path
     */
    getPixelValuePromise(iRA, iDEC, relFITSFilePath) {
        MarkersFactory.enter(this.getPixelValuePromise.name);
        MarkersFactory.exit();
        var viewer = this.viewer;
        return new Promise(function (resolve, reject) {
            var numDims = viewer.numDims();
            switch (numDims) {
                case 2:
                    var url = "getPixelValueAtiRAiDEC";
                    params = "?relFITSFilePath=" + relFITSFilePath + "&iRA=" + Math.round(iRA) + "&iDEC=" + Math.round(iDEC);
                    break;

                case 3:
                    var iFreq = viewer.getSliceRange();
                    if (typeof iFreq != "number") {
                        reject(`Invalid value of iFreq (${iFreq}). An integer is expected `);
                    }
                    var url = "getPixelValueAtiFreqiRAiDEC";
                    var params = "?relFITSFilePath=" + relFITSFilePath + "&iRA=" + Math.round(iRA) + "&iDEC=" + Math.round(iDEC) + "&iFREQ=" + Math.round(iFreq);
                    break;

                default:
                    reject(`Can't proceed with ${numDims}D data`);
            }
            $.get(url + params, (data, status, xhr) => {
                MarkersFactory.enter("Back from GET pixel value");
                if (status != "success") {
                    reject("A problem occurred with the request/server");
                } else {
                    if (!data["status"]) {
                        reject(data["message"]);
                    } else {
                        resolve(data["result"]);
                    }
                }
                MarkersFactory.exit();
            });
        });
    }



    /**
     * 
     * Callback.  Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onchange} behaviour (i.e. just when a change occurred).
     * @param {jexcel-sheet} sheet            - the sheet where change occurred
     * @param {jexcel-cell} cell              - the cell where change occurred
     * @param {number} x                      - the column number
     * @param {number} y                      - the row number
     * @param {any}                           - the new value 
     */
    changed(sheet, cell, x, y, value) {
        MarkersFactory.enter(this.changed.name);
        this.data = this.getDataAsJSON();
        const [iRAMax, iDECMax] = this.viewer.maxIndexes;
        let row = this.data[y];
        let self = this;
        var p = new Promise((resolve, reject)=>{
            const { RA, DEC } = this.validateRow(row);
            const iRaiDec = this.projection.raDecToiRaiDec(RA.decdegree, DEC.decdegree);
            // if RA/DEC values have been modified
            row.iRA = iRaiDec.iRa;
            row.iDEC = iRaiDec.iDec;
            if (RA && DEC) {
                if (inRange(iRaiDec.iRa, 0, iRAMax) && inRange(iRaiDec.iDec, 0, iDECMax)) {
                    var q = self.getPixelValuePromise(iRaiDec.iRa, iRaiDec.iDec, self.viewer.getRelFITSFilePath());
                    q.then(
                        result => {
                            resolve(result)
                        },
                        message => {
                            reject(message)
                        }
                    );
                } else {
                    reject(`Coordinates (${row.RA}, ${row.DEC} out of field`);
                }
            } else {
                reject("Mysterious !")
            }
        }).then(
            result => {
                row[this.pixelValueColumnName] = result["value"];
                //let context = this.infosBlock.getContext();
                console.dir(row);
                if (row.id == -1) {
                    row.id = this.markerIndex;
                    this.markerIndex += 1;
                }
            },
            error => {
                console.log(error);
                if (error.length > 0) alert(error);
                row.id = -1;
                row[this.pixelValueColumnName] = undefined;
            }
        ).finally(() => {
            this.data[y] = row;
            this.jexcel.setData(this.data);
            this.jexcel2OL();
        });
        MarkersFactory.exit();
    }

    /**
     * Callback.  Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onload} behaviour (i.e. setData).
     * 
     */
    loaded() {
        MarkersFactory.enter(this.loaded.name);
        $(".jexcel > tbody > tr > td.readonly").css("color", "purple");
        MarkersFactory.exit();
    }

    /**
     * 
     * Callback.  Defines the {@link  https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| ondeleterow} behaviour.
     */
    deleteSelectedMarkers() {
        MarkersFactory.enter(this.deleteSelectedMarkers.name);
        this.data = this.getDataAsJSON();
        var aof = this.selectedRowIndexes.map((index) => { return this.indexToMarker[index] });
        this.deleteMarkerFeatures(aof);
        for (var i = 0; i < this.selectedRowIndexes.length; i++) {
            delete this.indexToMarker[this.selectedRowIndexes[i]];
        }
        this.selectedRowIndexes = [];
        MarkersFactory.exit();
    }

    /**
     * Utility. Produces a graphic representation of the collection of markers from the content of the jexcel sheet.
     * (Jexcel to OpenLayers)
     */
    jexcel2OL() {
        MarkersFactory.enter(this.jexcel2OL.name);
        // Delete all the markers present on the OL view
        this.deleteMarkerFeatures(this.getAllMarkerFeatures());
        this.numOfMarkers = 0;
        this.indexToMarker = {};

        // For each valid row in the jexcel sheet create a marker on the OL view
        var validRows = this.getDataAsJSON().filter(x => x.id != -1);
        let self = this;
        validRows.forEach(row => {
            var RAinDD = self.checkRA(row.RA).decdegree;
            var DECinDD = self.checkDEC(row.DEC).decdegree;
            if (RAinDD && DECinDD) {
                var label = row.label;
                var f = new ol.Feature({ geometry: new ol.geom.Point([row.iRA, row.iDEC]) });
                f.set("label", label);
                f.setStyle(this.style_f(f));
                self.numOfMarkers += 1;
                self.source.addFeature(f);
                self.indexToMarker[row.id] = f;
            }
        });
        this.source.refresh({force:true});
        this.source.changed();
        this.layer.changed();
        this.map.render();

        MarkersFactory.exit();
    }

    /**
     * Creates an empty jexcel sheet and defines its behaviour.
     * 
     * <ul>
     * <li> The sheet will be a child of <tt>parentId</tt>.</li>
     * <li> Its columns are defined by [MarkersFactory's columns member]{@link MarkersFactory#columns}
     * @param {string} parentId  - the id of parent element.
     * </ul>
     */
    createJexcel(parentId) {
        MarkersFactory.enter(this.createJexcel.name);
        var result = jexcel(document.getElementById(parentId), {
            columns: this.columns,
            allowInsertRow: true,
            onload: this.loaded.bind(this),
            onselection: this.noteSelectedRows.bind(this),
            ondeleterow: this.deleteSelectedMarkers.bind(this),
            oninsertrow: this.setId.bind(this),
            onchange: this.changed.bind(this),
            onbeforechange: this.b4change.bind(this)
        });
        MarkersFactory.exit();
        return result;
    }

    /**
     * Utility. Returns the toolbox button that will activate this instance.
     * @returns {button}
     */
    getButtonObject() {
        return this.buttonObject;
    }

    /**
     * 
     * @returns a {@link https://developer.mozilla.org/fr/docs/Web/HTML/Element/Button|button}
     */
    markersCount() {
        MarkersFactory.enter(this.markersCount.name);
        var result = this.source.getFeatures().filter(f => f.getGeometry().getType() == 'Point').length;
        MarkersFactory.exit();
        return result;
    }

    /**
     * Utility. Record a new marker after a click on the graphic interface. 
     * Both graphic and jexcel interfaces are updated.
     * 
     * @param {event} event - the click event that triggers the marker's creation
     * @param {dictionary} flux - a dictionary {"value":.., "unit":..., "type:..."}
     */
    recordMarker(feature, flux) {
        MarkersFactory.enter(this.recordMarker.name);
        let coordinates = feature.getGeometry().getCoordinates();

        let label = `m_${this.numOfMarkers}`;
        if(feature.get("label") !== undefined)
            label = label + "-" + feature.get("label");

        feature.set('label', label);
        // get ra/dec coorinates with projection
        const radec = this.projection.iRaiDecToHMSDMS(coordinates[0], coordinates[1]);

        feature.set('RA', radec['ra']);
        feature.set('DEC', radec['dec']);
        feature.set(this.btype, flux["value"]);
        feature.setId(this.markerIndex);
        feature.setStyle(this.style_f(feature));

        let properties = {};
        let measurements = {};
        measurements["iRA"] = { "value": Math.round(coordinates[0]), "unit": "pixel" };
        measurements["iDEC"] = { "value": Math.round(coordinates[1]), "unit": "pixel" };
        measurements["RA"] = { "value": radec['ra'], "unit": "HMS" };
        measurements["DEC"] = { "value": radec['dec'], "unit": "DMS" };
        measurements[this.btype] = flux;
        properties["measurements"] = measurements;
        feature.set("properties", properties);

        this.indexToMarker[this.markerIndex] = feature;
        let d = {
            'id': this.markerIndex, 
            'RA': radec['ra'],
            'DEC': radec['dec'],
            'iRA' : Math.round(coordinates[0]),
            'iDEC' : Math.round(coordinates[1]),
            'label': label
        };
        d[this.pixelValueColumnName] = flux["value"];
        this.data.push(d);
        this.jexcel.setData(this.data);

        this.markerIndex += 1;
        MarkersFactory.exit();
    }

    /**
     * Utility. Removes a marker, i.e. one row, from the jexcel sheet given its OpenLayers counterpart (feature). 
     * @param {feature} markerFeature
     */
    removeFromJExcel(markerFeature) {
        MarkersFactory.enter(this.removeFromJExcel.name);
        let id = markerFeature.getId();
        this.data = this.getDataAsJSON();
        const equid = (element) => element.id == id;
        let index = this.data.findIndex(equid);
        if (index != -1) {
            this.data.splice(index, 1);
            this.jexcel.setData(this.data);
        }
        MarkersFactory.exit();
    }

    // Still to be worked on.
    addMarkers(arrayOfMarkers) {
        MarkersFactory.enter(this.addMarkers.name);

        for (let i = 0; i < arrayOfMarkers.length; i++) {            
            const raInRad = degToRad(arrayOfMarkers[i]["RAInDD"]);
            const decInRad = degToRad(arrayOfMarkers[i]["DECInDD"]);
            let result = this.projection.raDecToiRaiDec(arrayOfMarkers[i]["RAInDD"], arrayOfMarkers[i]["DECInDD"]);
            //var [iRA, iDEC] = this.absToPixelConverter(arrayOfMarkers[i]["RAInDD"], arrayOfMarkers[i]["DECInDD"]);


            //this.addMarker([result["x"], result["y"]], arrayOfMarkers[i]["label"]);
            console.log("iRA iDec", result["x"], result["y"]);
            let f = new ol.Feature({ geometry: new ol.geom.Point([result["iRa"], result["iDec"]]) });
            f.set("label", arrayOfMarkers[i]["label"]);
            f.setStyle(this.style_f(f));
            this.source.addFeature(f);

            //this.addMarker(result.iRa, result.iDec, arrayOfMarkers[i]["label"]);
            this.getMarkerFluxAndRecord(f);
            //this.recordMarker(f, {"value":10});
            //this.numOfMarkers += 1;
        }
        //this.source.refresh();
        //this.layer.changed();
        //this.map.render();
        MarkersFactory.exit()
    }

    /**
     * Utility. Return an array of all the OpenLayers features representing markers.
     * @returns {feature[]}
     */
    getAllMarkerFeatures() {
        MarkersFactory.enter(this.getAllMarkerFeatures.name)
        var allMarkers = [];
        this.source.getFeatures().forEach(function (feature) {
            if (feature.getGeometry().getType() == 'Point') {
                allMarkers.push(feature);
            }
        });
        MarkersFactory.exit();
        return allMarkers;
    }

    /**
     * Utility. Delete OpenLayers features representing markers.
     * @param {feature[]} arrayOfFeatures the features to delete
     */
    deleteMarkerFeatures(arrayOfFeatures) {
        MarkersFactory.enter(this.deleteMarkerFeatures.name);
        for (var i = 0; i < arrayOfFeatures.length; i++) {
            this.remove(arrayOfFeatures[i]);
        }
        this.source.refresh();
        MarkersFactory.exit();
    }

    // Still to be worked on.
    matchAndEvaluate(value, pat) {
        MarkersFactory.enter(this.matchAndEvaluate.name);
        var result = value.match(pat);
        if (result) result = result.map(function (elem, index) {
            if (index > 0) return Number(elem);
            else return elem;
        });
        MarkersFactory.exit();
        return result;
    }

    // Not used apparently
    /*RAtoString(RA) {
        return RA.hour + "h" + RA.minute + "m" + RA.second + "s";
    }

    // Not used apparently
    RAtoDecimalDegree(RA) {
        return RA.hour * 15 + RA.minute / 4 + RA.second / 240;
    }*/

    /**
     * Utility. Check that a string expresses correctly a Right Ascension.
     * @param {string} RAText 
     * @returns a dictionary {hour:.., minute:..., second:..., decdegree:...} if yes and null if no (decdegree === decimal degree)
     */
    checkRA(RAText) {
        MarkersFactory.enter(this.checkRA.name);
        var RA = { hour: 0, minute: 0, second: 0, degree: 0, decdegree: 0 };
        var value = RAText;
        var pat = /(\d{1,2})[hH:\s]\s*(\d{1,2})[mM:\s]\s*(\d{1,2}(\.\d+)?)[sS]?/;
        var result = this.matchAndEvaluate(value, pat);
        if (result &&
            (result[1] < 24) &&
            (result[2] < 60) &&
            (result[3] < 60)) {
            RA.hour = result[1];
            RA.minute = result[2];
            RA.second = result[3];
            RA.decdegree = RA.hour * 15 + RA.minute / 4 + RA.second / 240;
            RA.rad = degToRad(RA.decdegree);
            result = RA;
        } else {
            pat = /(\d{1,3})\.(\d+)/;
            result = this.matchAndEvaluate(value, pat);
            if (result) {
                RA.decdegree = parseFloat(value);
                RA.rad = degToRad(RA.decdegree);
                RA.hour = Math.floor(RA.decdegree / 15);
                RA.minute = Math.floor((RA.decdegree / 15 - RA.hour) * 60);
                RA.second = (RA.decdegree / 15 - RA.hour - RA.minute / 60) * 3600;
                RA.second = Math.floor(RA.second * 1000) / 1000.;
                XPathResult = RA;
                result = RA
            } else {
                alert("Wrong input for RA. Valid symbols for hours : [hH: ], minutes : [mM: ] and seconds : [sS]");
                result = null;
            }
        }
        if (result == null) {
            alert("Wrong input for RA. Valid symbols for hours : [hH: ], minutes : [mM': ] and seconds : [sS\"]");
        }

        MarkersFactory.exit();
        return result;
    }

    // Not used apparently
    /*DECtoString(DEC) {
        return (DEC.negative ? '-' : '') + DEC.degree + "\xB0" + DEC.arcminute + "'" + DEC.arcsecond + "\"";
    }

    // Not used apparently
    DECtoDecimalDegree(DEC) {
        var result = DEC.degree + DEC.arcminute / 60. + DEC.arcsecond / 3600.
        if (DEC.negative) result = -result;
        return result;
    }*/
    /** 
     * Utility. Check that a string expresses correctly a Declination.
     * @param { string } DECText
     * @returns {dictionary} a dictionary { degree:.., minute:..., second:..., decdegree:... } if yes and null if no (decdegree === decimale degree)
    */
    checkDEC(DECText) {
        MarkersFactory.enter(this.checkDEC.name);
        var DEC = { negative: false, degree: 0, arcminute: 0, arcsecond: 0, decdegree: 0. };
        var value = DECText.trim();
        var pat = /-?(\d{1,2})[dD\xB0:\s]\s*(\d{1,2})['mM:\s]\s*(\d{1,2}(\.\d+)?)[s"]?/;
        var result = this.matchAndEvaluate(value, pat);
        if (result &&
            ((result[1] == 90 && result[2] == 0 && result[3] == 0) ||
                (result[1] < 90 && result[2] < 60 && result[3] < 60))) {
            if (result[0][0] == '-' || result[0].charCodeAt(0) == 8722 || result[0].charCodeAt(0) == 45) {
                DEC.negative = true;
            }
            DEC.degree = result[1];
            DEC.minute = result[2];
            DEC.second = result[3];
            DEC.decdegree = (DEC.degree + DEC.minute / 60. + DEC.second / 3600.) * (DEC.negative ? -1 : 1);
            DEC.rad = degToRad(DEC.decdegree);
            result = DEC;
        } else {
            if (value.charCodeAt(0) == 8722 || value.charCodeAt(0) == 45) { value = "-" + value.slice(1); }
            var pat1 = /-?(\d{1,3}(\.\d+)?)/;
            var result1 = this.matchAndEvaluate(value, pat1);
            if (result1) {
                DEC.decdegree = result1[1];
                DEC.rad = degToRad(DEC.decdegree);
                if (value[0] == "-") {
                    DEC.decdegree = -DEC.decdegree;
                }
                DEC.negative = DEC.decdegree < 0.0;
                //
                DEC.degree = Math.abs(Math.floor(DEC.decdegree));
                DEC.minute = Math.floor((DEC.decdegree - DEC.degree) * 60);
                DEC.second = (DEC.decdegree - DEC.degree - DEC.minute / 60) * 3600;
                if (DEC.second < 0.) DEC.second = 0.;
                if (DEC.second > 60) DEC.second = 60.;
                result = DEC;
            }
        }

        if (result == null) {
            alert("Wrong input for DEC. Valid symbols for degrees : [\xB0dD: ], minutes : [mM': ] and seconds : [sS\"]");
        }
        MarkersFactory.exit();
        return result;
    }

    /**
     * Utility. Check that a pair of string expresses correctly a pair (Right Ascension, Declination)
     * @param {string} RAText 
     * @param {string} DECText 
     * @returns {dictionary} a dictionary { RA: result of checkRA(RAText), DEC: result of checkDEC(DECText)}
     */
    checkRADEC(RAText, DECText) {
        MarkersFactory.enter(this.checkRADEC.name);
        var result = { 'RA': this.checkRA(RAText), 'DEC': this.checkDEC(DECText) };
        MarkersFactory.exit();
        return result;
    }

    enablePlaceMarkers() {
        $(`#${this.refreshMarkersDisplayId}`).attr('disabled', false);
    }

    // Not used apparently.
    parseTextOfMarkers() {
        MarkersFactory.enter(this.parseTextOfMarkers.name);
        var markers = [];
        var status = true;
        var content = $(`#${this.textOfMarkersId}`).val();
        var lines = content.split(/\r|\r\n|\n/);
        for (var i = 0; i < lines.length; i++) {
            let line = lines[i].trim();
            if (line.length > 0) {
                var fields = line.split(",");
                if (fields.length < 2) {
                    alert("Line '" + line + "' looks incomplete");
                    status = false;
                    break;
                }
                var label = "" + i;
                if (fields.length >= 3) {
                    label = fields[2];
                }
                var RA = null;
                var DEC = null;
                if ((RA = this.checkRA(fields[0])) && (DEC = this.checkDEC(fields[1]))) {
                    markers.push({ "RAInDD": RA.decdegree, "DECInDD": DEC.decdegree, "id": (fields.length == 3) ? fields[2] : "", "label": label });
                } else {
                    status = false;
                    break;
                }
            }
        }

        if (status) {
            this.enablePlaceMarkers();
        } else {
            markers = [];
        }
        MarkersFactory.exit();
        return markers;
    }

    // Not used apparently.
    appendMarkerTA(RA, DEC, label, iRA, iDEC, id = null, withFlux = false, FITSHeader = null) {
        var ta = $(`#${this.textOfMarkersId}`);
        ta.val(ta.val() + RA + ", " + DEC + (id ? id : "") + ", " + label + ", " + iRA + ", " + iDEC + "\n");
    }

    // Not used apparently
    cleanMarkerTA() {
        $(`#${this.textOfMarkersId}`).val("");
    }

    // Still to be worked on.
    checkAndDrawSampMarkers() {
        MarkersFactory.enter(this.checkAndDrawSampMarkers.name);
        var new_markers = this.parseTextOfMarkers();
        this.deleteMarkerFeatures(this.getAllMarkerFeatures());
        this.addMarkers(new_markers);
        MarkersFactory.exit();
    }

    /**
     * Utility. Format the textual expression of a float number.
     * @param {number} floatValue 
     * @returns {string} a string as produced by toExponential(4)
     */
    format(floatValue) {
        let result = floatValue;
        if (typeof result === "number" && !Number.isInteger(result)) {
            result = result.toExponential(4);
        }
        return result;
    };
}


export {
    Marker,
    MarkerList,
    MarkerManager,
    MarkersFactory
}