Source: public/javascript/modules/olqv_axes.js

import { ShapesFactory } from "./olqv_shapes.js";
import { FITS_HEADER } from "./fitsheader.js";
import {DecDeg2DMS, DecDeg2HMS, degToRad, radToDeg} from "./utils.js";
import {AxisButton} from "./olqv_olbuttons.js";

/**
 * 
 * @typedef {Object} AxesFactory
 * @extends ShapesFactory
 * 
 * @property {number} numeridians number of visible meridians (int)
 * @property {number} nparallels number of visible parallels (int)
 * @property {number} cdelt1 cdelt1 in radians (header is in degrees) (float)
 * @property {number} cdelt2 cdelt2 in radians (header is in degrees) (float)
 * @property {object} axisStyle styling options for axis
 */
class AxesFactory extends ShapesFactory {

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

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

    /**
     * 
     * @param {Viewer} viewer 
     */
    constructor(viewer) {
        super(viewer, 'axes');
        this.numeridians = 6;
        this.nparallels = 6;
        //axes are hidden by default
        this.isVisible = 0;
        // number of decimals for ra/dec values on axis
        //this._displayedDecimals = Math.abs(FITS_HEADER.cdelt1).toExponential().split("-")[1] - 1;
        // cdelt1 in radians
        this.cdelt1 = degToRad(FITS_HEADER.cdelt1);
        //cdelt2 in radians
        this.cdelt2 = degToRad(FITS_HEADER.cdelt2);

        this.axisStyle = new ol.style.Style({
            stroke: new ol.style.Stroke({
                // alpha = 0 to hide the axes
                color: 'rgb(0, 0, 255, 0)',
                width: 0,
            }),
        });

        this._buttonObject = new AxisButton(viewer);

    }

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

    getButtonClick(){
        this.toggleAxes(); 
        if(this.isVisible){
            this._buttonObject.getButton().classList.add("active");
        }else{
            this._buttonObject.getButton().classList.remove("active");
        }
        this.viewer.map.getView().changed();
    }

    /**
     * Updates the alpha value of the axes and the coordinates text
     * @param {number} alpha rgba alpha value
     */
    updateAxesStyle(alpha){
        this.axisStyle.getStroke().setColor('rgb(0, 0, 255, '+alpha+')');
        let features = this.layer.getSource().getFeatures();
        for(let f of features){
            if(f.getStyle().getText() !== null){
                f.getStyle().getText().getStroke().setColor('rgb(255, 255, 255, '+alpha+')');
                f.getStyle().getText().getFill().setColor('rgb(0, 0, 0, '+alpha+')');
            }
        }
    }

    /**
     * Makes the axes and coordinates visible
     */
    showAxes(){
        this.updateAxesStyle(1);
        this.layer.changed();
        this.isVisible = 1;
    }

    /**
     * Hides the axes and coordinates
     */
    hideAxes(){
        this.updateAxesStyle(0);
        this.layer.changed();
        this.isVisible = 0;
    }

    /**
     * Toggles visibility of axes and coordinates
     */
    toggleAxes(){
        if(this.isVisible){
            this.hideAxes();
        }else{
            this.showAxes();
        }
    }

    /**
     * Traces axes on image
     */
    build() {
        let axes = this.getAxes();
        let lineString = new ol.geom.MultiLineString(axes.parallels.concat(axes.meridians));

        let f = new ol.Feature({ geometry: lineString.simplify(1) });
        f.setStyle(this.axisStyle);

        this.layer.setZIndex(10);
        this.layer.getSource().addFeature(f);        

        // show coordinates at meridians top extremity
        for (let i = 0; i < axes.meridians.length; i++) {
            let coords = axes.meridians[i][axes.meridians[i].length - 1];
            let radec = this.projection.iRaiDecToRaDec(coords[0], coords[1]);
            this.addExtremityPoint(coords, DecDeg2HMS(radec.ra));
        }

        // show coordinates at parallels left extremity
        for (let i = 0; i < axes.parallels.length; i++) {
            let coords = axes.parallels[i][0];
            let radec = this.projection.iRaiDecToRaDec(coords[0], coords[1]);
            this.addExtremityPoint(coords, DecDeg2DMS(radec.dec));
        }

        this.layer.set('axes', "axes");
        this.layer.changed();
    }

    /**
     * Shows coordinates at both axis extremity
     * @param {array} ra/dec coordinates
     */
    addExtremityPoint(coords, label) {
        let point = new ol.geom.Point(coords);
        let f2 = new ol.Feature({ geometry: point });

        f2.setStyle(new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: 'green',
                width: 10,
            }),
            text: new ol.style.Text({
                font: '12px Calibri,sans-serif',
                fill: new ol.style.Fill({ color: 'rgb(0, 0, 0, 0)' }),
                stroke: new ol.style.Stroke({
                    color: 'rgb(255, 255, 255, 0)',
                    width: 2
                }),
                // get the text from the feature - `this` is ol.Feature
                // and show only under certain resolution
                text: label
            })
        }));
        this.layer.getSource().addFeature(f2);
    }

    /**
     * Returns the points defining the meridians curves. The result curves will be smoothed before display.
     * @param {array} ra/dec limit values
     * @returns {array} an array of x, y arrays with all coordinates of the meridians
     */
    getMeridians(limits) {
        // Plot the meridians,
        const x = FITS_HEADER.naxis1 * Math.abs(FITS_HEADER.cdelt1);
        const stepx = x / this.numeridians;


        const y = FITS_HEADER.naxis2 * Math.abs(FITS_HEADER.cdelt2);
        const stepy = y / this.numeridians;

        let ras = [];
        let decs = [];

        const radDegInterval = radToDeg(limits.RA_max) - radToDeg(limits.RA_min);
        const decDegInterval = radToDeg(limits.DEC_max) - radToDeg(limits.DEC_min);
        const coeff = 0.5;

        for(let i = radToDeg(limits.RA_min) - coeff*radDegInterval; 
                i <= radToDeg(limits.RA_max)+ coeff*radDegInterval; i = i+stepx){
            ras.push(i);
        }

        for(let i = radToDeg(limits.DEC_min) - coeff*decDegInterval; 
                i <= radToDeg(limits.DEC_max) + coeff*decDegInterval; i = i+stepy/20){
            decs.push(i);
        }

        let coordinates_list = [];
        for(let i = 0; i < ras.length; i=i+2){
            let coordinates = [];
            for(let j = 0; j < decs.length; j=j+2){
                let xy = this.projection.absToRel([ras[i]*(Math.PI/180)], [decs[j]*(Math.PI/180)]);
                const i_xp = Math.round(FITS_HEADER.crpix1 + ((xy.x) / (this.cdelt1)) - 1);
                const i_yp = Math.round(FITS_HEADER.crpix2 + ((xy.y) / (this.cdelt2)) - 1);
                if (this.viewer.isInExtent(i_xp, i_yp)) {
                    coordinates.push([i_xp, i_yp]);
                }
            }

            if (coordinates.length > 1)
                coordinates_list.push(coordinates);
        }
        return coordinates_list;
    }

    /**
     * Returns the points defining the parallels curves. The result curves will be smoothed before display.
     * @param {object} ra/dec limit values
     * @returns {array} an array of x, y arrays with all coordinates of the parallels
     */
    getParallels(limits) {
        // Plot the meridians, 
        const x = FITS_HEADER.naxis1 * Math.abs(FITS_HEADER.cdelt1);
        const stepx = x / this.nparallels;

        const y = FITS_HEADER.naxis2 * Math.abs(FITS_HEADER.cdelt2);
        const stepy = y / this.nparallels;

        let ras = [];
        let decs = [];

        const radDegInterval = radToDeg(limits.RA_max) - radToDeg(limits.RA_min);
        const decDegInterval = radToDeg(limits.DEC_max) - radToDeg(limits.DEC_min);
        const coeff = 0.5;
        
        for(let i = radToDeg(limits.RA_min) - coeff*radDegInterval;
            i <= radToDeg(limits.RA_max)+ coeff*radDegInterval; i = i+stepx/20){
            ras.push(i);
        }

        for(let i = radToDeg(limits.DEC_min) - coeff*decDegInterval;
             i <= radToDeg(limits.DEC_max) + coeff*decDegInterval; i = i+stepy){
            decs.push(i);
        }
        let coordinates_list = [];
        for(let i = 0; i < decs.length; i=i+2){
            let coordinates = [];
            for(let j = 0; j < ras.length; j=j+2){
                let xy = this.projection.absToRel([ras[j]*(Math.PI/180)], [decs[i]*(Math.PI/180)]);
                const i_xp = Math.round(FITS_HEADER.crpix1 + (xy.x) / (this.cdelt1) - 1);
                const i_yp = Math.round(FITS_HEADER.crpix2 + (xy.y) / (this.cdelt2) - 1);
                if (this.viewer.isInExtent(i_xp, i_yp)) {
                    coordinates.push([i_xp, i_yp]);
                }  
            }
            
            if (coordinates.length > 1)
                coordinates_list.push(coordinates);
        }
        
        return coordinates_list;
    }

    _updateMaxValues(x, y, result){
        let raDec = this.projection.relToAbs([x], [y]);
        let ra=raDec.ra[0];
        let dec=raDec.dec[0];

        if (ra < result.RA_min) {
            result.RA_min = ra;
        }
        if (ra > result.RA_max) {
            result.RA_max = ra;
        }
        if (dec < result.DEC_min) {
            result.DEC_min = dec
        }
        if (dec > result.DEC_max) {
            result.DEC_max = dec;
        }

        return result;
    }

    /**
     * Returns the boundaries of ra/dec values
     * @returns {object}
     */
    getLimits() {
        let result = { "RA_min": 0, "RA_max": 0, "DEC_min": 0, "DEC_max": 0 };

        let x = new Array(FITS_HEADER.naxis1).fill(0);        
        let y = new Array(FITS_HEADER.naxis2).fill(0); 
        // coordinates after projection
        let raDec;


        for (let i = 0; i < FITS_HEADER.naxis1; i++) {
            x[i] = (i - FITS_HEADER.crpix1) * this.cdelt1;
        }

        for (let j = 0; j < FITS_HEADER.naxis2; j++) {
            y[j] = (j - FITS_HEADER.crpix2) * this.cdelt2;
        }      

        raDec = this.projection.relToAbs([x[0]], [y[0]]);
        result = {  "RA_min": raDec.ra[0], 
                    "RA_max": raDec.ra[0], 
                    "DEC_min": raDec.dec[0], 
                    "DEC_max": raDec.dec[0] };

        // get min/max values of ra/dec after projection
        // search only on the edges of the image
        let i=0;
        for (let j = 0; j < y.length; j=j+1) {
            result = this._updateMaxValues(x[i], y[j], result);
        }

        i=x.length-1;
        for (let j = 0; j < y.length; j=j+1) {
            result = this._updateMaxValues(x[i], y[j], result);
        }

        let j=0;
        for (let i = 0; i < x.length; i=i+1) {            
            result = this._updateMaxValues(x[i], y[j], result);
            
        }

        j=y.length-1;
        for (let i = 0; i < x.length; i=i+1) {            
            result = this._updateMaxValues(x[i], y[j], result);
        }

        return result;
    }

    /**
     * Returns a list of corrdinates of point defining parallels and meridians
     * @returns {array}
     */
    getAxes() {
        let limits = this.getLimits();
        let coordinates_list = {
            meridians : this.getMeridians(limits),
            parallels : this.getParallels(limits)
        };
        //return multipoints;
        return coordinates_list;
    }
}


export {
    AxesFactory
};