Source: components/MousePosition.js

/**
 * @module components/MousePosition.js
 * @name MousePosition
 * @copyright 2023 3Liz
 * @author BOISTEAULT Nicolas
 * @license MPL-2.0
 */

import { mainLizmap } from '../modules/Globals.js';
import { html, render } from 'lit-html';

import { transform, get as getProjection } from 'ol/proj.js';

import { forward } from '../dependencies/mgrs.js';

import MGRS from '../modules/MGRS.js';

/**
 * @class
 * @name MousePosition
 * @augments HTMLElement
 */
export default class MousePosition extends HTMLElement {
    constructor() {
        super();

        this._numDigits = 0;

        // 'm', 'ft', 'us-ft','degrees', 'dm', 'dms'
        this._displayUnit = 'm';

        // 'm', 'ft', 'us-ft', 'degrees'
        this._qgisProjectProjectionUnits;

        // lon/lat in map projection
        this._longitude;
        this._latitude;

        // lon/lat in inputs (written when mouse move or by user)
        this._lonInput;
        this._latInput;
    }

    // Don't add line break between <input>s or it adds a space in UI
    mainTemplate(lon, lat){
        return html`
            <div class="mouse-position">
                <div class="editable-position ${['dm', 'dms', 'mgrs'].includes(this._displayUnit) ? 'hide' : ''}">
                    <input type="number" step="any" placeholder="longitude" @input=${(event) => this._lonInput = parseFloat(event.target.value)} @keydown=${(event) => { if (event.key === 'Enter') { this._centerToCoords(); } }} .value=${isNaN(lon) ? 0 : lon}><input type="number" step="any" placeholder="latitude" @input=${(event) => this._latInput = parseFloat(event.target.value)} @keydown=${(event) => { if (event.key === 'Enter') { this._centerToCoords(); } }} .value=${isNaN(lat) ? 0 : lat}>
                </div>
                <div class="readonly-position ${['dm', 'dms', 'mgrs'].includes(this._displayUnit) ? '' : 'hide'}">
                    <span>${lon}</span>
                    <span>${lat}</span>
                </div>
                <button class="btn btn-sm" title="${lizDict['mouseposition.removeCenterPoint']}" @click=${() => this._removeCenterPoint()}><i class="icon-refresh"></i></button>
            </div>
            <div class="coords-unit">
                <select title="${lizDict['mouseposition.select']}" @change=${(event) => { this.displayUnit = event.target.value }}>
                    ${this._qgisProjectProjectionUnits === 'm' ? html`
                    <option selected value="m">${lizDict['mouseposition.units.m']}</option>` : ''}
                    ${ ['ft', 'us-ft'].includes(this._qgisProjectProjectionUnits) ? html`
                    <option selected value="f">${lizDict['mouseposition.units.f']}</option>` : ''}

                    <option value="degrees">${lizDict['mouseposition.units.d']}</option>
                    <option value="dm">${lizDict['mouseposition.units.dm']}</option>
                    <option value="dms">${lizDict['mouseposition.units.dms']}</option>
                    <option value="mgrs">MGRS</option>
                </select>
            </div>`;
    }

    _centerToCoords() {
        if (this._lonInput && this._latInput) {
            let lonlatInputInMapProj;

            // If map projection is not yet in degrees => reproject to EPSG:4326
            if (this._displayUnit === 'degrees' && mainLizmap.lizmap3.map.projection.getUnits() !== 'degrees') {
                lonlatInputInMapProj = transform([this._lonInput, this._latInput], 'EPSG:4326', mainLizmap.projection);
            } else {
                lonlatInputInMapProj = transform([this._lonInput, this._latInput], mainLizmap.qgisProjectProjection, mainLizmap.projection);
            }

            mainLizmap.center = lonlatInputInMapProj;
            // Display point
            const featureAsWKT = `POINT(${lonlatInputInMapProj[0]} ${lonlatInputInMapProj[1]})`;
            mainLizmap.map.setHighlightFeatures(featureAsWKT,"wkt", mainLizmap.projection);
        }
    }

    _removeCenterPoint() {
        mainLizmap.map.clearHighlightFeatures();
    }

    /**
     * @param {string} unit
     */
    set displayUnit(unit){
        unit === 'm' ? this._numDigits = 0 : this._numDigits = 5;
        this._displayUnit = unit;

        if (this._longitude && this._latitude){
            this.redraw(this._longitude, this._latitude);
        }

        if(unit === 'mgrs'){
            if(!this._MGRS){
                this._MGRS = new MGRS({
                    showLabels: true,
                    wrapX: false,
                });
                this._MGRS.setProperties({
                    name: 'LizmapMousePositionMGRS'
                });
            }
            mainLizmap.map.addToolLayer(this._MGRS);
        } else {
            if(this._MGRS){
                mainLizmap.map.removeToolLayer(this._MGRS);
            }
        }
    }

    // Callback to map's mousemove event
    _mousemove(evt){
        if (evt == null) {
            return;
        }else{
            let lon,lat;
            // OL2
            if(evt.xy){
                const lonlat = mainLizmap.lizmap3.map.getLonLatFromPixel(evt.xy);
                if (lonlat !== null) {
                    lon = lonlat.lon;
                    lat = lonlat.lat;
                }
            } else if (evt.pixel){ //OL6
                [lon, lat ] = mainLizmap.map.getCoordinateFromPixel(evt.pixel);
            }

            if(lon && lat){
                this._longitude = lon;
                this._latitude = lat;

                this.redraw(lon, lat);
            }
        }
    }

    redraw(lon, lat) {
        let lonLatToDisplay = [lon, lat];

        // Display in degree, degree minute, degree minute second or MGRS
        if (['degrees', 'dm', 'dms', 'mgrs'].includes(this._displayUnit)) {
            // If map projection is not yet in degrees => reproject to EPSG:4326
            if (mainLizmap.lizmap3.map.projection.getUnits() !== 'degrees') {
                lonLatToDisplay = transform(lonLatToDisplay, mainLizmap.projection, 'EPSG:4326');
            }

            // If in degrees, lon/lat are editable
            if (this._displayUnit === 'degrees') {
                render(this.mainTemplate(lonLatToDisplay[0].toFixed(this._numDigits), lonLatToDisplay[1].toFixed(this._numDigits)), this);
            } else if (this._displayUnit === 'mgrs') {
                let mgrsCoords = '';
                try {
                    mgrsCoords = forward(lonLatToDisplay);

                    mgrsCoords = mgrsCoords.slice(0, -12) + ' ' + mgrsCoords.slice(-12, -10) + ' ' + mgrsCoords.slice(-10, -5) + ' ' + mgrsCoords.slice(-5);
                } catch (error) {
                    console.error(error);
                }

                render(this.mainTemplate(mgrsCoords, ''), this);
            } else {
                lonLatToDisplay[0] = this.getFormattedLonLat(lonLatToDisplay[0], 'lon', this._displayUnit);
                lonLatToDisplay[1] = this.getFormattedLonLat(lonLatToDisplay[1], 'lat', this._displayUnit);

                render(this.mainTemplate(lonLatToDisplay[0], lonLatToDisplay[1]), this);
            }
        } else {
            lonLatToDisplay = transform(lonLatToDisplay, mainLizmap.projection, mainLizmap.qgisProjectProjection);

            render(this.mainTemplate(lonLatToDisplay[0].toFixed(this._numDigits), lonLatToDisplay[1].toFixed(this._numDigits)), this);
        }

        this._lonInput = lonLatToDisplay[0];
        this._latInput = lonLatToDisplay[1];
    }

    getFormattedLonLat (coordinate, axis, dmsOption) {
        if (!dmsOption) {
            dmsOption = 'dms';    //default to show degree, minutes, seconds
        }

        coordinate = (coordinate + 540) % 360 - 180; // normalize for sphere being round

        var abscoordinate = Math.abs(coordinate);
        var coordinatedegrees = Math.floor(abscoordinate);

        var coordinateminutes = (abscoordinate - coordinatedegrees) / (1 / 60);
        var tempcoordinateminutes = coordinateminutes;
        coordinateminutes = Math.floor(coordinateminutes);
        var coordinateseconds = (tempcoordinateminutes - coordinateminutes) / (1 / 60);
        coordinateseconds = Math.round(coordinateseconds * 10);
        coordinateseconds /= 10;

        if (coordinateseconds >= 60) {
            coordinateseconds -= 60;
            coordinateminutes += 1;
            if (coordinateminutes >= 60) {
                coordinateminutes -= 60;
                coordinatedegrees += 1;
            }
        }

        if (coordinatedegrees < 10) {
            coordinatedegrees = "0" + coordinatedegrees;
        }
        var str = coordinatedegrees + "\u00B0";

        if (dmsOption.indexOf('dms') >= 0) {
            if (coordinateminutes < 10) {
                coordinateminutes = "0" + coordinateminutes;
            }
            str += coordinateminutes + "'";

            if (coordinateseconds < 10) {
                coordinateseconds = "0" + coordinateseconds;
            }
            str += coordinateseconds + '"';
        } else if (dmsOption.indexOf('dm') >= 0) {
            coordinateminutes = Math.round(tempcoordinateminutes * 1000);
            coordinateminutes = coordinateminutes / 1000;
            if (coordinateminutes < 10) {
                coordinateminutes = "0" + coordinateminutes;
            }
            str += coordinateminutes + "'";
        }

        if (axis == "lon") {
            str += coordinate < 0 ? "W" : "E";
        } else {
            str += coordinate < 0 ? "S" : "N";
        }
        return str;
    }

    connectedCallback() {
        // Init variables
        this._qgisProjectProjectionUnits = getProjection(mainLizmap.qgisProjectProjection).getUnits();
        this.displayUnit = this._qgisProjectProjectionUnits;

        // Listen to mousemove event
        // OL2
        mainLizmap.lizmap3.map.events.register('mousemove', this, this._mousemove);
        // OL6
        mainLizmap.map.on('pointermove', (evt) => this._mousemove(evt));

        // First render
        render(this.mainTemplate(null, null), this);
    }

    disconnectedCallback() {
        // OL2
        mainLizmap.lizmap3.map.events.unregister('mousemove', this, this._mousemove);
        // OL6
        mainLizmap.map.un('pointermove', (evt) => this._mousemove(evt));
    }
}