Source: components/Digitizing.js

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

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

import '../images/svg/point.svg';
import '../images/svg/line.svg';
import '../images/svg/polygon.svg';
import '../images/svg/box.svg';
import '../images/svg/circle.svg';
import '../images/svg/freehand.svg';
import '../images/svg/text.svg';

import '../images/svg/pencil.svg';
import '../images/svg/edit.svg';
import '../images/svg/eraser.svg';
import '../images/svg/eraser-all.svg';
import '../images/svg/save.svg';

import '../images/svg/file-download.svg';
import '../images/svg/file-upload.svg';

/**
 * Digitizing element
 * Provides user interface for digitizing shapes and text
 * Attributes:
 *  context - The digitizing context to linked element to Digitizing module context
 *  selected-tool - Start selected drawing tools one of DigitizingAvailableTools or available-tools
 *  available-tools - List of available drawing tools based on DigitizingAvailableTools
 *  save - Enable save capability
 *  measure - Enable measure capability
 *  import-export - Enable import / export capabilities
 * @class
 * @name Digitizing
 * @augments HTMLElement
 * @example
 * <lizmap-digitizing context="draw" selected-tool="box" available-tools="point,line,polygon,box,freehand" save import-export measure></lizmap-digitizing>
 */
export default class Digitizing extends HTMLElement {
    constructor() {
        super();
        this._toolSelected = DigitizingAvailableTools[0];
        this._availableTools = DigitizingAvailableTools.slice(1);
    }

    connectedCallback() {

        if (this.hasAttribute('available-tools')) {
            const attrAvailableTools = this.getAttribute('available-tools')
                .split(',')
                .map((item) => item.trim())
                .filter((item) => this._availableTools.includes(item));
            if (attrAvailableTools.length > 0) {
                this._availableTools = attrAvailableTools;
            }
        }
        if (this.hasAttribute('selected-tool')) {
            const attrToolSelected = this.getAttribute('selected-tool');
            if (this._availableTools.includes(attrToolSelected)) {
                this._toolSelected = attrToolSelected;
            }
        }

        const mainTemplate = () => html`
        <div class="digitizing">
            <div class="digitizing-buttons btn-group dropend" data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.drawTools']}">
                <button type="button" class="btn ${this.deactivate ? '' : 'active btn-primary'}" @click=${(event) => {this.toggleToolSelected(event)}}>
                    <svg>
                        <use xlink:href="#pencil"></use>
                    </svg>
                    <!-- Display selected tool -->
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Point ? '' : 'visually-hidden'}">
                        <use xlink:href="#point"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Line ? '' : 'visually-hidden'}">
                        <use xlink:href="#line"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Polygon ? '' : 'visually-hidden'}">
                        <use xlink:href="#polygon"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Box ? '' : 'visually-hidden'}">
                        <use xlink:href="#box"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Circle ? '' : 'visually-hidden'}">
                        <use xlink:href="#circle"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Freehand ? '' : 'visually-hidden'}">
                        <use xlink:href="#freehand"></use>
                    </svg>
                    <svg class="digitizing-selected-tool ${this.toolSelected === DigitizingTools.Text ? '' : 'visually-hidden'}">
                        <use xlink:href="#text"></use>
                    </svg>
                </button>
                <button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
                    <span class="visually-hidden">Toggle Dropdown</span>
                </button>
                <ul class="dropdown-menu">
                    ${this._availableTools.includes(DigitizingTools.Point) ? html`
                    <li class="digitizing-${DigitizingTools.Point} btn ${this.toolSelected === DigitizingTools.Point ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Point} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Point]}">
                        <svg>
                            <use xlink:href="#point"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Line) ? html`
                    <li class="digitizing-${DigitizingTools.Line} btn ${this.toolSelected === DigitizingTools.Line ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Line} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Line]}">
                        <svg>
                            <use xlink:href="#line"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Polygon) ? html`
                    <li class="digitizing-${DigitizingTools.Polygon} btn ${this.toolSelected === DigitizingTools.Polygon ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Polygon} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Polygon]}">
                        <svg>
                            <use xlink:href="#polygon"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Box) ? html`
                    <li class="digitizing-${DigitizingTools.Box} btn ${this.toolSelected === DigitizingTools.Box ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Box} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Box]}">
                        <svg>
                            <use xlink:href="#box"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Circle) ? html`
                    <li class="digitizing-${DigitizingTools.Circle} btn ${this.toolSelected === DigitizingTools.Circle ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Circle} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Circle]}">
                        <svg>
                            <use xlink:href="#circle"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Freehand) ? html`
                    <li class="digitizing-${DigitizingTools.Freehand} btn ${this.toolSelected === DigitizingTools.Freehand ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Freehand} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Freehand]}">
                        <svg>
                            <use xlink:href="#freehand"></use>
                        </svg>
                    </li>` : ''}
                    ${this._availableTools.includes(DigitizingTools.Text) ? html`
                    <li class="digitizing-${DigitizingTools.Text} btn ${this.toolSelected === DigitizingTools.Text ? 'active btn-primary' : ''}" @click=${() => this.toolSelected = DigitizingTools.Text} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.'+DigitizingTools.Text]}">
                        <svg>
                            <use xlink:href="#text"></use>
                        </svg>
                    </li>` : ''}
                </ul>
            </div>
            <input type="color" class="digitizing-color btn" .value="${mainLizmap.digitizing.drawColor}" @input=${(event) => mainLizmap.digitizing._userChangedColor(event.target.value)} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.color']}">
            <button type="button" class="digitizing-edit btn ${mainLizmap.digitizing.isEdited ? 'active btn-primary' : ''}" ?disabled=${!mainLizmap.digitizing.featureDrawn} @click=${() => mainLizmap.digitizing.toggleEdit()} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.edit']}">
                <svg>
                    <use xlink:href="#edit"/>
                </svg>
            </button>
            <button type="button" class="digitizing-erase btn ${mainLizmap.digitizing.isErasing ? 'active btn-primary' : ''}" ?disabled=${!mainLizmap.digitizing.featureDrawn} @click=${() => mainLizmap.digitizing.toggleErasing()} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.erase']}">
                <svg>
                    <use xlink:href="#eraser"/>
                </svg>
            </button>
            <button type="button" class="digitizing-all btn" ?disabled=${!mainLizmap.digitizing.featureDrawn} @click=${() => this.eraseAll()} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.erase.all']}">
                <svg>
                    <use xlink:href="#eraser-all"/>
                </svg>
            </button>
            <button type="button" class="digitizing-toggle-visibility btn" ?disabled=${!mainLizmap.digitizing.featureDrawn} @click=${() => mainLizmap.digitizing.toggleVisibility()} data-bs-toggle="tooltip" data-bs-title="${lizDict['tree.button.checkbox']}">
                <i class="icon-eye-${mainLizmap.digitizing.visibility ? 'open' : 'close'}"></i>
            </button>
            <button type="button" class="digitizing-toggle-measure btn ${mainLizmap.digitizing.hasMeasureVisible ? 'active btn-primary' : ''} ${this.measureAvailable ? '' : 'hide'}" @click=${() => mainLizmap.digitizing.toggleMeasure()} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.measure']}">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                    <path d="M17 3l4 4l-14 14l-4 -4z"></path>
                    <path d="M16 7l-1.5 -1.5"></path>
                    <path d="M13 10l-1.5 -1.5"></path>
                    <path d="M10 13l-1.5 -1.5"></path>
                    <path d="M7 16l-1.5 -1.5"></path>
                </svg>
            </button>
            <button type="button" class="digitizing-save btn ${mainLizmap.digitizing.isSaved ? 'active btn-primary' : ''} ${this.saveAvailable ? '' : 'hide'}" @click=${()=> this.toggleSave()} data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.save']}">
                <svg>
                    <use xlink:href="#save" />
                </svg>
            </button>
            <div class="digitizing-import-export ${this.importExportAvailable ? '' : 'hide'}">
                <div class="btn-group dropend digitizing-export">
                    <button type="button" class="btn dropdown-toggle" ?disabled=${!mainLizmap.digitizing.featureDrawn} data-bs-toggle="dropdown" aria-expanded="false" title="${lizDict['attributeLayers.toolbar.btn.data.export.title']}">
                        <svg>
                            <use xlink:href="#file-download"></use>
                        </svg>
                    </button>
                    <ul class="dropdown-menu">
                        <li>
                            <button class="dropdown-item" type="button" @click=${() => mainLizmap.digitizing.download('geojson')}>GeoJSON</button>
                        </li>
                        <li>
                            <button class="dropdown-item" type="button" @click=${() => mainLizmap.digitizing.download('gpx')}>GPX</button>
                        </li>
                        <li>
                            <button class="dropdown-item" type="button" @click=${() => mainLizmap.digitizing.download('kml')}>KML</button>
                        </li>
                        <li>
                            <button class="dropdown-item" type="button" @click=${() => mainLizmap.digitizing.download('fgb')}>FlatGeobuf</button>
                        </li>
                    </ul>
                </div>
                <div class="digitizing-import">
                    <label class="btn" data-bs-toggle="tooltip" data-bs-title="${lizDict['digitizing.toolbar.import']}">
                        <svg>
                            <use xlink:href="#file-upload"></use>
                        </svg>
                        <input class="hide" type="file" accept=".kml, .geojson, .json, .gpx, .zip, .fgb" @change=${
                            (event) =>
                                    {
                                        if (event.target.files.length > 0){
                                            event.target.parentElement.parentElement.querySelector('.file-name').textContent = event.target.files[0].name;
                                            mainLizmap.digitizing.import(event.target.files[0]);
                                        }
                                    }
                        }>
                    </label>
                    <span class="file-name"></span>
                </div>
            </div>
            <div class="digitizing-state hide">
                <div class="digitizing-save-state hide">${lizDict['digitizing.toolbar.save.state']}</div>
            </div>
            <div class="digitizing-constraints ${mainLizmap.digitizing.hasConstraintsPanelVisible ? '' : 'hide'}">
                <details>
                    <summary>${lizDict['digitizing.constraint.title']}</summary>${lizDict['digitizing.constraint.details']}
                </details>
                <div class="input-append">
                    <input type="number" placeholder="${lizDict['digitizing.constraint.distance']}" class="distance" min="0" @input=${(event)=> mainLizmap.digitizing.distanceConstraint = event.target.value}>
                    <span class="add-on">m</span>
                </div>
                <div class="input-append">
                    <input type="number" placeholder="${lizDict['digitizing.constraint.angle']}" class="angle" @input=${(event)=> mainLizmap.digitizing.angleConstraint = event.target.value}>
                    <span class="add-on">°</span>
                </div>
            </div>
            <form class="digitizing-text-tools ${mainLizmap.digitizing.editedFeatures.length ? '' : 'hide'}">
                <details>
                    <summary>${lizDict['digitizing.toolbar.text']}</summary>${lizDict['digitizing.toolbar.text.hint']}
                </details>
                <div class="form-row">
                    <label for="textContent">${lizDict['digitizing.toolbar.textLabel']}</label>
                    <textarea id="textContent" placeholder="${lizDict['digitizing.toolbar.newText']}" .value=${mainLizmap.digitizing.editedFeatureText} @input=${ event=> mainLizmap.digitizing.editedFeatureText = event.target.value}></textarea>
                </div>
                <div class='digitizing-text-rotation form-row'>
                    <label for="textRotation">${lizDict['digitizing.toolbar.textRotation']}</label>
                    <div class="input-append">
                        <input id="textRotation" type="number" .value=${mainLizmap.digitizing.editedFeatureTextRotation} @input=${ event => { mainLizmap.digitizing.editedFeatureTextRotation = parseInt(event.target.value) }}>
                        <span class="add-on">°</span>
                    </div>
                </div>
                <div class="form-row">
                    <label for="textScale">${lizDict['digitizing.toolbar.textScale']}</label>
                    <input id="textScale" type="number" min="1" .value=${mainLizmap.digitizing.editedFeatureTextScale} @input=${ event => { mainLizmap.digitizing.editedFeatureTextScale = parseInt(event.target.value) }}>
                </div>
            </form>
        </div>`;

        render(mainTemplate(), this);

        const tooltipTriggerList = this.querySelectorAll('[data-bs-toggle="tooltip"]');
        [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl, {
            trigger: 'hover'
        }));

        mainEventDispatcher.addListener(
            () => {
                if (!this.disabled) {
                    render(mainTemplate(), this);
                }
            },
            ['digitizing.featureDrawn', 'digitizing.visibility', 'digitizing.toolSelected', 'digitizing.editionBegins', 'digitizing.editionEnds', 'digitizing.erasingBegins', 'digitizing.erasingEnds', 'digitizing.erase', 'digitizing.erase.all', 'digitizing.drawColor', 'digitizing.save', 'digitizing.measure', 'digitizing.editedFeatureText', 'digitizing.editedFeatureRotation', 'digitizing.editedFeatureScale']
        );
    }

    disconnectedCallback() {
    }

    /**
     * Digitizing context
     * The element attribute: context
     * @type {string}
     */
    get context() {
        if (this.hasAttribute('context')) {
            return this.getAttribute('context');
        }
        return 'draw';
    }

    /**
     * The element is deactivated
     * if the element is disabled
     * or if the tool is deactivated
     * @type {boolean}
     */
    get deactivate() {
        if (mainLizmap.digitizing.context !== this.context) {
            return true;
        }
        if (mainLizmap.digitizing.toolSelected === 'deactivate') {
            return true;
        }
        return false;
    }

    /**
     * The element is disabled if the context is not the same as the module
     * @type {boolean}
     */
    get disabled() {
        if (mainLizmap.digitizing.context !== this.context) {
            return true;
        }
        return false;
    }

    /**
     * Measure is available
     * The element has attribute: measure
     * @type {boolean}
     */
    get measureAvailable() {
        return this.hasAttribute('measure');
    }

    /**
     * Save is available
     * The element has attribute: save
     * @type {boolean}
     */
    get saveAvailable() {
        return this.hasAttribute('save');
    }

    /**
     * Import/export is available
     * The element has attribute: import-export
     * @type {boolean}
     */
    get importExportAvailable() {
        return this.hasAttribute('import-export');
    }

    /**
     * The available tools
     * The element attribute: available-tools
     * All or part of DigitizingAvailableTools except deactivate
     * @see DigitizingAvailableTools
     * @type {string}
     */
    get availableTools() {
        return this._availableTools;
    }

    /**
     * The selected tool
     * The element attribute: selected-tool
     * @type {string}
     */
    get toolSelected() {
        return this._toolSelected;
    }

    /**
     * Setting the selected
     * @see DigitizingAvailableTools
     * @param {string} tool - switch new OL map on top of OL2 one
     */
    set toolSelected(tool) {
        if (this._availableTools.includes(tool)) {
            this._toolSelected = tool;
            mainLizmap.digitizing.toolSelected = tool;
        }
    }

    /**
     * Toggle selected tool
     * @param {MouseEvent} event - The click event on the button
     */
    toggleToolSelected(event) {
        if (this.toolSelected === DigitizingAvailableTools[0]) {
            bootstrap.Dropdown.getOrCreateInstance(event.currentTarget).toggle();
        } else {
            mainLizmap.digitizing.toolSelected = (mainLizmap.digitizing.toolSelected !== DigitizingAvailableTools[0]) ? DigitizingAvailableTools[0] : this.toolSelected;
        }
        event.stopPropagation();
    }

    eraseAll() {
        if (!confirm(lizDict['digitizing.confirm.erase.all'])) {
            return false;
        }
        mainLizmap.digitizing.eraseAll();
    }

    toggleSave() {
        mainLizmap.digitizing.toggleSave();
        if (mainLizmap.digitizing.isSaved) {
            this.querySelector('button.digitizing-save').dataset.originalTitle = lizDict['digitizing.toolbar.save.remove'];
            this.querySelector('div.digitizing-save-state').classList.remove('hide');
            this.querySelector('div.digitizing-state').classList.remove('hide');
        } else {
            this.querySelector('button.digitizing-save').dataset.originalTitle = lizDict['digitizing.toolbar.save'];
            this.querySelector('div.digitizing-save-state').classList.add('hide');
            this.querySelector('div.digitizing-state').classList.add('hide');
        }
    }
}