☰

WME πŸ‡ΊπŸ‡¦ E40 Geometry

Π”Π°Π½Π½Ρ‹ΠΉ скрипт позволяСт ΠΈΠ·ΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΏΠ»ΠΎΡ‰Π°Π΄ΡŒ POI, Π²Ρ‹Ρ€Π°Π²Π½ΠΈΠ²Π°Ρ‚ΡŒ ΠΈ ΠΊΠΎΠΏΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey, Greasemonkey ΠΈΠ»ΠΈ Violentmonkey.

Для установки этого скрипта Π²Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅, Ρ‚Π°ΠΊΠΎΠ΅ ΠΊΠ°ΠΊ Tampermonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Violentmonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Userscripts.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ скриптов.

(Ρƒ мСня ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ скриптов, Π΄Π°ΠΉΡ‚Π΅ ΠΌΠ½Π΅ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ скрипт!)

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

(Ρƒ мСня ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй, Π΄Π°ΠΉΡ‚Π΅ ΠΌΠ½Π΅ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ скрипт!)

// ==UserScript==
// @name         WME E40 Geometry
// @name:uk      WME πŸ‡ΊπŸ‡¦ E40 Geometry
// @name:ru      WME πŸ‡ΊπŸ‡¦ E40 Geometry
// @version      0.13.0
// @description  A script that allows aligning, scaling, and copying POI geometry
// @description:uk Π—Π° допомогою Ρ†ΡŒΠΎΠ³ΠΎ скрипта Π²ΠΈ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π»Π΅Π³ΠΊΠΎ Π·ΠΌΡ–Π½ΡŽΠ²Π°Ρ‚ΠΈ ΠΏΠ»ΠΎΡ‰Ρƒ Ρ‚Π° Π²ΠΈΡ€Ρ–Π²Π½ΡŽΠ²Π°Ρ‚ΠΈ POI
// @description:ru Π”Π°Π½Π½Ρ‹ΠΉ скрипт позволяСт ΠΈΠ·ΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΏΠ»ΠΎΡ‰Π°Π΄ΡŒ POI, Π²Ρ‹Ρ€Π°Π²Π½ΠΈΠ²Π°Ρ‚ΡŒ ΠΈ ΠΊΠΎΠΏΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ
// @license      MIT License
// @author       Anton Shevchuk
// @namespace    https://greasyfork.org/users/227648-anton-shevchuk
// @supportURL   https://github.com/AntonShevchuk/wme-e40/issues
// @match        https://*.waze.com/editor*
// @match        https://*.waze.com/*/editor*
// @exclude      https://*.waze.com/user/editor*
// @icon         data:image/png;base64,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
// @grant        none
// @require      https://update.greasyfork.org/scripts/389765/1794584/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/571719/1793257/GeoUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1792042/WME-Bootstrap.js
// @require      https://update.greasyfork.org/scripts/450221/1793261/WME-Base.js
// @require      https://update.greasyfork.org/scripts/450320/1794414/WME-UI.js
//
// @require      https://cdn.jsdelivr.net/npm/@turf/[email protected]/turf.min.js
// ==/UserScript==

(function () {
    'use strict';

    const NAME = 'E40';

    // User level required for apply geometry for all entities in the view area
    const REQUIRED_LEVEL = 2;
    // Translations
    const TRANSLATION = {
        'en': {
            title: 'POI Geometry',
            description: 'Change geometry in the current view area',
            options: {
                title: 'Navigation Points',
                navigationPoint: 'Highlight entrance for selected place',
                navigationPointAll: 'Highlight all entrances for selected place',
                navigationPointOnHover: 'Highlight entrance on hover',
            },
            warning: '⚠️ This option is available for editors with a rank higher than ' + REQUIRED_LEVEL,
            help: 'You can use the <strong>Keyboard shortcuts</strong> to apply the settings. It\'s more convenient than clicking on the buttons.',
            orthogonalize: 'Orthogonalize',
            smooth: 'Smooth',
            simplify: 'Simplify',
            scale: 'Scale',
            rotate: 'Rotate',
            circle: 'Circle',
            square: 'Square',
            copy: 'Copy',
            about: '<a href="https://greasyfork.org/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
        },
        'uk': {
            title: 'ГСомСтрія POI',
            description: 'Π—ΠΌΡ–Π½ΠΈΡ‚ΠΈ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€Ρ–ΡŽ ΠΎΠ±\u2019Ρ”ΠΊΡ‚Ρ–Π² Ρƒ ΠΏΠΎΡ‚ΠΎΡ‡Π½ΠΎΠΌΡƒ Ρ€ΠΎΠ·Ρ‚Π°ΡˆΡƒΠ²Π°Π½Π½Ρ–',
            options: {
                title: 'Π’ΠΎΡ‡ΠΊΠΈ Π½Π°Π²Ρ–Π³Π°Ρ†Ρ–Ρ—',
                navigationPoint: 'ΠŸΡ–Π΄ΡΠ²Ρ–Ρ‡ΡƒΠ²Π°Ρ‚ΠΈ Π½Π°Π²Ρ–Π³Π°Ρ†Ρ–ΡŽ Π΄ΠΎ місця',
                navigationPointAll: 'ΠŸΡ–Π΄ΡΠ²Ρ–Ρ‡ΡƒΠ²Π°Ρ‚ΠΈ Π½Π°Π²Ρ–Π³Π°Ρ†Ρ–ΡŽ Π΄ΠΎ всіх Ρ‚ΠΎΡ‡ΠΎΠΊ Π²Ρ…ΠΎΠ΄Ρƒ',
                navigationPointOnHover: 'ΠŸΡ–Π΄ΡΠ²Ρ–Ρ‡ΡƒΠ²Π°Ρ‚ΠΈ Π½Π°Π²Ρ–Π³Π°Ρ†Ρ–ΡŽ Π·Π° навСдСнням мишки',
            },
            warning: '⚠️ Ця опція доступна лишС для Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€Ρ–Π² Π· Ρ€Π°Π½Π³ΠΎΠΌ Π²ΠΈΡ‰Ρ–ΠΌ Π½Ρ–ΠΆ ' + REQUIRED_LEVEL,
            help: 'ВикористовуйтС <strong>гарячі ΠΊΠ»Π°Π²Ρ–ΡˆΠΈ</strong>, Ρ†Π΅ Π·Π½Π°Ρ‡Π½ΠΎ швидшС Π½Ρ–ΠΆ використовувати ΠΊΠ½ΠΎΠΏΠΊΠΈ',
            orthogonalize: 'Вирівняти',
            smooth: 'Π—Π³Π»Π°Π΄ΠΈΡ‚ΠΈ',
            simplify: 'Бпростити',
            scale: 'ΠœΠ°ΡΡˆΡ‚Π°Π±ΡƒΠ²Π°Ρ‚ΠΈ',
            rotate: 'ΠŸΠΎΠ²Π΅Ρ€Π½ΡƒΡ‚ΠΈ',
            circle: 'ΠšΡ€ΡƒΠ³',
            square: 'ΠšΠ²Π°Π΄Ρ€Π°Ρ‚',
            copy: 'ΠšΠΎΠΏΡ–ΡŽΠ²Π°Ρ‚ΠΈ',
            about: '<a href="https://greasyfork.org/uk/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
        },
        'ru': {
            title: 'ГСомСтрия POI',
            description: 'Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ² Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ располоТСнии',
            options: {
                title: 'Π’ΠΎΡ‡ΠΊΠΈ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ',
                navigationPoint: 'ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ Π΄ΠΎ Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ мСста',
                navigationPointAll: 'ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ ΠΊΠΎ всСм Ρ‚ΠΎΡ‡ΠΊΠ°ΠΌ Π²Ρ…ΠΎΠ΄Π°',
                navigationPointOnHover: 'ΠŸΠΎΠ΄ΡΠ²Π΅Ρ‡ΠΈΠ²Π°Ρ‚ΡŒ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ ΠΏΡ€ΠΈ Π½Π°Π²Π΅Π΄Π΅Π½ΠΈΠΈ ΠΌΡ‹ΡˆΠΈ',
            },
            warning: '⚠️ Π­Ρ‚Π° опция доступна для Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ΠΎΠ² с Ρ€Π°Π½Π³ΠΎΠ² Π²Ρ‹ΡˆΠ΅ ' + REQUIRED_LEVEL,
            help: 'Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ <strong>ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΈ клавиш</strong>, ΠΈ Π½Π΅ Π½Π°Π΄ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ ΠΊΠ»Π°Ρ†Π°Ρ‚ΡŒ ΠΊΠ½ΠΎΠΏΠΊΠΈ',
            orthogonalize: 'Π’Ρ‹Ρ€ΠΎΠ²Π½ΡΡ‚ΡŒ',
            smooth: 'Π‘Π³Π»Π°Π΄ΠΈΡ‚ΡŒ',
            simplify: 'Π£ΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ',
            scale: 'ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ',
            rotate: 'ΠŸΠΎΠ²Π΅Ρ€Π½ΡƒΡ‚ΡŒ',
            circle: 'ΠšΡ€ΡƒΠ³',
            square: 'ΠšΠ²Π°Π΄Ρ€Π°Ρ‚',
            copy: 'ΠšΠΎΠΏΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ',
            about: '<a href="https://greasyfork.org/uk/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
        }
    };

    const SETTINGS = {
        options: {
            navigationPoint: true,
            navigationPointAll: false,
            navigationPointOnHover: false,
        },
    };

    let E40Instance;
    function setE40Instance(instance) {
        E40Instance = instance;
    }
    /**
     * Scale selected place(s) to X mΒ²
     * @param {Number} x square meters
     * @param {Boolean} orMore flag
     * @return {boolean}
     */
    function scale(x, orMore = false) {
        E40Instance.scale(E40Instance.getSelectedPlaces(), x, orMore);
        return false;
    }
    /**
     * Scale all places in the editor area to X mΒ²
     * @param {Number} x square meters
     * @param {Boolean} orMore flag
     * @return {boolean}
     */
    function scaleAll(x = 650, orMore = true) {
        E40Instance.scale(E40Instance.getAllPlaces(), x, orMore);
        return false;
    }
    /**
     * Orthogonalize selected place(s)
     * @return {boolean}
     */
    function orthogonalize() {
        E40Instance.orthogonalize(E40Instance.getSelectedPlaces());
        return false;
    }
    /**
     * Orthogonalize all places in the editor area
     * @return {boolean}
     */
    function orthogonalizeAll() {
        // skip parking, natural and outdoors
        // TODO: make options for filters
        E40Instance.orthogonalize(E40Instance.getAllPlaces([
            'CAMPING_TRAILER_PARK',
            'FOREST_GROVE',
            'JUNCTION_INTERCHANGE',
            'NATURAL_FEATURES',
            'OUTDOORS',
            'PARKING_LOT',
            'PLAYGROUND',
        ]));
        return false;
    }
    /**
     * Smooth selected place(s)
     * @return {boolean}
     */
    function smooth() {
        E40Instance.smooth(E40Instance.getSelectedPlaces());
        return false;
    }
    /**
     * Simplify selected place(s)
     * @param {Number} tolerance
     * @return {boolean}
     */
    function simplify(tolerance = 0.00001) {
        E40Instance.simplify(E40Instance.getSelectedPlaces(), tolerance);
        return false;
    }
    /**
     * Simplify all places in the editor area
     * @param {Number} tolerance
     * @return {boolean}
     */
    function simplifyAll(tolerance = 0.00001) {
        // skip parking, natural and outdoors
        E40Instance.simplify(E40Instance.getAllPlaces(['OUTDOORS', 'PARKING_LOT', 'NATURAL_FEATURES']), tolerance);
        return false;
    }
    /**
     * Transform the Point to circle place
     * @param {Number} area in square meters
     * @param {Number} steps
     */
    function circle(area, steps = 64) {
        E40Instance.circle(E40Instance.getSelectedVenues(), area, steps);
        return false;
    }
    /**
     * Transform the Point to square place
     * @param {Number} area in square meters
     */
    function square(area) {
        E40Instance.square(E40Instance.getSelectedVenues(), area);
        return false;
    }
    /**
     * Copy selected places
     * Last of them will be chosen
     */
    function copyPlaces() {
        let venues = E40Instance.getSelectedPlaces();
        let ids = [];
        for (let i = 0; i < venues.length; i++) {
            let id = E40Instance.copyPlace(venues[i]);
            ids.push(id);
        }
        E40Instance.selectVenues(ids);
    }
    /**
     * wmeSDK.Map.enablePolygonResize()
     */
    function enablePolygonResize() {
        console.log('%c' + NAME + ': %cenable resize for Polygon', 'color: #0DAD8D; font-weight: bold', 'color: dimgray; font-weight: normal');
        let places = E40Instance.getSelectedPlaces();
        if (places.length) {
            E40Instance.wmeSDK.Map.enablePolygonResize();
        }
    }
    /**
     * wmeSDK.Map.enablePolygonRotation()
     */
    function enablePolygonRotation() {
        console.log('%c' + NAME + ': %cenable rotation for Polygon', 'color: #0DAD8D; font-weight: bold', 'color: dimgray; font-weight: normal');
        let places = E40Instance.getSelectedPlaces();
        if (places.length) {
            E40Instance.wmeSDK.Map.enablePolygonRotation();
        }
    }

    // https://fontawesome.com/v4/icons/
    function getPlaceButtons() {
        return {
            A: {
                title: '<i class="fa fa-circle-thin" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).smooth,
                shortcut: 'S+49',
                callback: () => smooth()
            },
            B: {
                title: '<i class="fa fa-square-o" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).orthogonalize,
                shortcut: 'S+50',
                callback: () => orthogonalize()
            },
            C: {
                title: '1️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify + ' (tolerance = 0.00001)',
                shortcut: null,
                callback: () => simplify(0.00001)
            },
            D: {
                title: '3️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify + ' (tolerance = 0.00003)',
                shortcut: null,
                callback: () => simplify(0.00003)
            },
            E: {
                title: '5️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify + ' (tolerance = 0.00005)',
                shortcut: null,
                callback: () => simplify(0.00005)
            },
            F: {
                title: '<i class="fa fa-clone" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).copy,
                shortcut: null,
                callback: () => copyPlaces()
            },
            G: {
                title: '<i class="fa fa-repeat" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).rotate,
                shortcut: 'S+51',
                callback: () => enablePolygonRotation()
            },
            H: {
                title: '<i class="fa fa-expand" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).scale,
                shortcut: 'S+52',
                callback: () => enablePolygonResize()
            },
            I: {
                title: '500mΒ²',
                description: WMEUI.t(NAME).scale + ' 500mΒ²',
                shortcut: 'S+53',
                callback: () => scale(500)
            },
            J: {
                title: '650mΒ²',
                description: WMEUI.t(NAME).scale + ' 650mΒ²',
                shortcut: 'S+54',
                callback: () => scale(650)
            },
            K: {
                title: '650+',
                description: WMEUI.t(NAME).scale + ' 650+',
                shortcut: 'S+55',
                callback: () => scale(650, true)
            },
        };
    }
    function getPointButtons() {
        return {
            M: {
                title: '<i class="fa fa-circle-thin fa-2x" aria-hidden="true"></i> 500mΒ²',
                description: WMEUI.t(NAME).circle,
                shortcut: null,
                callback: () => circle(503, 32)
            },
            N: {
                title: '<i class="fa fa-circle-thin fa-2x" aria-hidden="true"></i> 650mΒ²',
                description: WMEUI.t(NAME).circle,
                shortcut: null,
                callback: () => circle(651, 64)
            },
            O: {
                title: '<i class="fa fa-circle-thin fa-2x" aria-hidden="true"></i> R=20m',
                description: WMEUI.t(NAME).circle,
                shortcut: null,
                callback: () => circle(1256.64, 64)
            },
            P: {
                title: '<i class="fa fa-square-o fa-2x" aria-hidden="true"></i> 500mΒ²',
                description: WMEUI.t(NAME).square,
                shortcut: null,
                callback: () => square(500)
            },
            R: {
                title: '<i class="fa fa-square-o fa-2x" aria-hidden="true"></i> 650mΒ²',
                description: WMEUI.t(NAME).square,
                shortcut: null,
                callback: () => square(650)
            },
            S: {
                title: '<i class="fa fa-square-o fa-2x" aria-hidden="true"></i> 1000mΒ²',
                description: WMEUI.t(NAME).square,
                shortcut: null,
                callback: () => square(1000)
            },
        };
    }
    function getTabButtons() {
        return {
            A: {
                title: '<i class="fa fa-square-o" aria-hidden="true"></i>',
                description: WMEUI.t(NAME).orthogonalize,
                callback: () => orthogonalizeAll()
            },
            B: {
                title: '1️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify,
                callback: () => simplifyAll(0.00001)
            },
            C: {
                title: '3️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify,
                callback: () => simplifyAll(0.00003)
            },
            D: {
                title: '5️⃣ πŸ“',
                description: WMEUI.t(NAME).simplify,
                callback: () => simplifyAll(0.00005)
            },
            E: {
                title: '500+',
                description: WMEUI.t(NAME).scale + ' 500mΒ²+',
                callback: () => scaleAll(500, true)
            }
        };
    }

    const TYPES = {
        boardwalk: 10,
        stairway: 16,
        railroad: 18,
        runway: 19};

    const layerConfig = {
        styleContext: {},
        styleRules: [
            {
                predicate: (properties) => properties.styleName === "styleNode",
                style: {
                    pointRadius: 4,
                    fillColor: '#ffffff',
                    strokeColor: '#14e2d9',
                    strokeWidth: 3,
                    strokeLinecap: 'round',
                    // graphicName: 'x',
                    graphicZIndex: 9999,
                },
            },
            {
                predicate: (properties) => properties.styleName === "styleLine",
                style: {
                    strokeWidth: 3,
                    strokeColor: '#14e2d9',
                    strokeLinecap: 'round',
                    graphicZIndex: 9999,
                }
            },
            {
                predicate: (properties) => properties.styleName === "styleSecondaryLine",
                style: {
                    strokeWidth: 2,
                    strokeColor: '#ffffff',
                    strokeLinecap: 'round',
                    graphicZIndex: 9999,
                }
            },
            {
                predicate: (properties) => properties.styleName === "styleDashedLine",
                style: {
                    strokeWidth: 2,
                    strokeColor: '#ffffff',
                    strokeLinecap: 'round',
                    strokeDashstyle: 'dash',
                    graphicZIndex: 9999,
                }
            },
            {
                predicate: (properties) => properties.styleName === "styleDashedSecondaryLine",
                style: {
                    strokeWidth: 1,
                    strokeColor: '#ffffff',
                    strokeLinecap: 'round',
                    strokeDashstyle: 'dash',
                    graphicZIndex: 9999,
                }
            }
        ],
    };

    /**
     * Creates a GeoJSON Polygon representing a circle centered at a given point
     * with a radius calculated from a desired area in square meters.
     *
     * @param {object} centerPoint - A GeoJSON Point feature (e.g., turf.point([lon, lat])).
     * @param {number} areaSqMeters - The desired area of the circle in square meters (mΒ²).
     * @param {number} [steps=64] - The number of steps/segments to create the circle (higher = smoother).
     * @returns {object} A GeoJSON Polygon Feature representing the circle.
     */
    function createCirclePolygon(centerPoint, areaSqMeters, steps = 64) {
        if (centerPoint.type !== 'Point') {
            throw new Error('Invalid centerPoint: Must be a GeoJSON Point feature.');
        }
        if (typeof areaSqMeters !== 'number' || areaSqMeters <= 0) {
            throw new Error('Invalid areaSqMeters: Must be a positive number.');
        }
        // 1. Calculate the required radius (R) from the Area (A)
        // The formula for the area of a circle is: A = Ο€ * RΒ²
        // Rearranging for the radius: R = sqrt(A / Ο€)
        const radiusMeters = Math.sqrt(areaSqMeters / Math.PI);
        // 2. Convert the radius from meters to kilometers (Turf.js default unit)
        const radiusKilometers = radiusMeters / 1000;
        // 3. Use turf.circle to create the polygon
        return turf.circle(centerPoint, radiusKilometers, {
            steps: steps,
            units: 'kilometers' // Explicitly set units, though it's the default
        });
    }
    /**
     * Creates a GeoJSON Polygon representing a square centered at a given point
     * with a side length calculated from a desired area in square meters.
     *
     * @param {object} centerPoint - A GeoJSON Point feature (e.g., turf.point([lon, lat])).
     * @param {number} areaSqMeters - The desired area of the square in square meters (mΒ²).
     * @returns {object} A GeoJSON Polygon Feature representing the square.
     */
    function createSquarePolygon(centerPoint, areaSqMeters) {
        if (centerPoint.type !== 'Point') {
            throw new Error('Invalid centerPoint: Must be a GeoJSON Point feature.');
        }
        if (typeof areaSqMeters !== 'number' || areaSqMeters <= 0) {
            throw new Error('Invalid areaSqMeters: Must be a positive number.');
        }
        // 1. Calculate the required Side Length (S) from the Area (A)
        // The formula for the area of a square is: A = SΒ²
        // Rearranging for the side length: S = sqrt(A)
        const sideLengthMeters = Math.sqrt(areaSqMeters);
        // 2. Calculate the distance from the center to any edge of the square
        // This is half the side length: HalfSide = S / 2
        const halfSideMeters = sideLengthMeters / 2;
        // 3. Since Turf.js typically handles distances in kilometers, convert the half-side.
        const halfSideKilometers = halfSideMeters / 1000;
        // 4. Calculate the bounding box (bbox) coordinates
        // We can use a combination of `turf/destination` or, more simply for a centered square,
        // manually calculate the offsets using Turf's distance handling for min/max coordinates.
        // However, a simpler approach is to calculate the bounding box for the square's corners.
        // A centered square's extent is defined by its center coordinates +/- (half-side in distance units).
        // The `turf/bbox` function is often used to get the extent of a feature, but here we need
        // to calculate the BBOX based on a distance from the center point.
        // Calculate the geographic bounding box [west, south, east, north]
        // Due to the complexities of Earth's curvature, calculating precise coordinates
        // by simply adding/subtracting distances (especially for large squares) is difficult.
        // A robust, though slightly over-engineered, way is to use the `turf/buffer` function
        // to approximate the square's corners.
        // A simpler approach for small, localized areas is to calculate the min/max coordinates
        // by using the `turf/transformScale` on a unit square. However, this is more complex.
        // A common and practical approximation for *small* areas:
        // const [lon, lat] = centerPoint.coordinates;
        // For simplicity, we'll use an approximation based on latitude/longitude differences.
        // WARNING: This approximation is only accurate for very small areas or near the equator.
        // For a highly accurate square, you would use geodesic distance functions (like turf/destination)
        // to find the four corners based on the center point and the half-side distance.
        // --- Robust Geodesic Calculation for the Four Corners ---
        const options = { units: 'kilometers' };
        // 45 degrees: Northeast, 135 degrees: Northwest, 225 degrees: Southwest, 315 degrees: Southeast
        const cornerNE = turf.destination(centerPoint, halfSideKilometers * Math.SQRT2, 45, options);
        const cornerSW = turf.destination(centerPoint, halfSideKilometers * Math.SQRT2, 225, options);
        const minLon = cornerSW.geometry.coordinates[0];
        const minLat = cornerSW.geometry.coordinates[1];
        const maxLon = cornerNE.geometry.coordinates[0];
        const maxLat = cornerNE.geometry.coordinates[1];
        // The BBOX format is [minX, minY, maxX, maxY] => [west, south, east, north]
        const calculatedBbox = [minLon, minLat, maxLon, maxLat];
        // 5. Use turf.bboxPolygon to create the square polygon from the bounding box
        return turf.bboxPolygon(calculatedBbox);
    }
    /**
     * Iteratively simplifies a GeoJSON Polygon ring by removing points that form
     * an angle between 175Β° and 180Β° with their neighbors.
     * @param {object} geojsonObject A GeoJSON Feature<Polygon> or Polygon geometry object.
     * @returns {object} The simplified GeoJSON Polygon geometry object (type: "Polygon").
     */
    function simplifyPolygon(geojsonObject) {
        let points = geojsonObject.coordinates[0].slice();
        const MIN_UNIQUE_POINTS = 4; // A, B, C, A (length 4) means 3 unique points (a triangle)
        const MIN_ANGLE = 175.0;
        const MAX_ANGLE = 185.0;
        let pointsRemoved = 0;
        let iteration = 0;
        console.log("--- Starting Polygon Simplification (175Β° to 185Β° removal) ---");
        while (points.length > MIN_UNIQUE_POINTS) {
            iteration++;
            let pointIndexToRemove = -1;
            // Check points from index 1 up to length - 2.
            for (let i = 1; i < points.length - 1; i++) {
                const angle = GeoUtils.findAngle(points[i - 1], points[i], points[i + 1]);
                if (angle >= MIN_ANGLE && angle <= MAX_ANGLE) {
                    pointIndexToRemove = i;
                    console.log(`[Iter ${iteration}] Found point to remove at index ${i} (${points[i].map((c) => c.toFixed(2)).join(', ')}). Angle: ${angle.toFixed(4)}Β°`);
                    break; // Remove only one point per iteration
                }
            }
            if (pointIndexToRemove !== -1) {
                points.splice(pointIndexToRemove, 1);
                pointsRemoved++;
                // Update the closure point
                points[points.length - 1] = points[0];
                console.log(`[Iter ${iteration}] Point removed. New length: ${points.length}. Unique points remaining: ${points.length - 1}.`);
            }
            else {
                console.log(`[Iter ${iteration}] No point found in the angle range [${MIN_ANGLE}Β°, ${MAX_ANGLE}Β°]. Stopping.`);
                break;
            }
        }
        if (points.length <= MIN_UNIQUE_POINTS) {
            console.log(`Reached minimum size of 3 unique points (array length ${points.length}). Stopping.`);
        }
        console.log(`--- Simplification Finished. Total points removed: ${pointsRemoved} ---`);
        return {
            type: "Polygon",
            coordinates: [points]
        };
    }
    /**
     * Orthogonalize polygon geometry using iterative gradient descent.
     * Adjusts vertices to make angles multiples of 90Β° while preserving shape.
     * Based on the iD editor algorithm (used in OpenStreetMap).
     *
     * @param geojsonObject - GeoJSON Polygon object
     * @param threshold - Angle threshold in degrees (default 12)
     * @returns GeoJSON Polygon with orthogonalized coordinates
     */
    function normalizeRightAngles(geojsonObject, threshold = 12) {
        const lowerThreshold = Math.cos(((90 - threshold) * Math.PI) / 180);
        const upperThreshold = Math.cos((threshold * Math.PI) / 180);
        const epsilon = 1e-4;
        let nodes = structuredClone(geojsonObject.coordinates[0]);
        // Work in Mercator projection to avoid lat/lon distortion
        let points = nodes.slice(0, -1).map((n) => [n[0], lat2latp(n[1])]);
        let corner = { i: 0, dotp: 1 };
        // Special case: triangles β€” only move the least-square corner
        if (points.length === 3) {
            for (let i = 0; i < 1000; i++) {
                let motions = points.map((b, i, arr) => calcMotion(b, i, arr));
                let tmp = addPts(points[corner.i], motions[corner.i]);
                points[corner.i][0] = tmp[0];
                points[corner.i][1] = tmp[1];
                if (corner.dotp < epsilon)
                    break;
            }
            let n = points[corner.i];
            n[1] = latp2lat(n[1]);
            let id = nodes[corner.i].toString();
            for (let i = 0; i < nodes.length; i++) {
                if (nodes[i].toString() === id) {
                    nodes[i][0] = n[0];
                    nodes[i][1] = n[1];
                }
            }
            return { type: 'Polygon', coordinates: [nodes] };
        }
        // General case: gradient descent minimizing squareness
        const originalPoints = nodes.slice(0, -1).map((n) => [n[0], lat2latp(n[1])]);
        let score = Number.POSITIVE_INFINITY;
        for (let i = 0; i < 1000 && !(score < epsilon); i++) {
            let motions = points.map((b, i, arr) => calcMotion(b, i, arr));
            for (let j = 0; j < motions.length; j++) {
                let tmp = addPts(points[j], motions[j]);
                points[j][0] = tmp[0];
                points[j][1] = tmp[1];
            }
            let newScore = squareness(points);
            if (newScore < score)
                score = newScore;
        }
        // Apply changes back to nodes
        for (let i = 0; i < points.length; i++) {
            if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) {
                let n = points[i];
                n[1] = latp2lat(n[1]);
                let id = nodes[i].toString();
                for (let j = 0; j < nodes.length; j++) {
                    if (nodes[j].toString() === id) {
                        nodes[j][0] = n[0];
                        nodes[j][1] = n[1];
                    }
                }
            }
        }
        // Remove collinear points (angle ~180Β°)
        for (let i = 0; i < points.length; i++) {
            let dotp = normalizedDotProduct(i, points);
            if (dotp < -1 + epsilon) {
                let id = nodes[i].toString();
                for (let j = 0; j < nodes.length; j++) {
                    if (nodes[j].toString() === id) {
                        nodes[j] = false;
                    }
                }
            }
        }
        nodes = nodes.filter((item) => item !== false);
        return { type: 'Polygon', coordinates: [nodes] };
        function calcMotion(b, i, array) {
            let a = array[(i - 1 + array.length) % array.length];
            let c = array[(i + 1) % array.length];
            let p = subPts(a, b);
            let q = subPts(c, b);
            let scale = 2 * Math.min(dist(p, [0, 0]), dist(q, [0, 0]));
            p = normPt(p, 1.0);
            q = normPt(q, 1.0);
            let dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]);
            if (array.length > 3) {
                if (dotp < -Math.SQRT1_2)
                    dotp += 1.0;
            }
            else if (dotp && Math.abs(dotp) < corner.dotp) {
                corner.i = i;
                corner.dotp = Math.abs(dotp);
            }
            return normPt(addPts(p, q), 0.1 * dotp * scale);
        }
        function squareness(pts) {
            return pts.reduce((sum, _, i, arr) => {
                let dotp = filterDotProduct(normalizedDotProduct(i, arr));
                return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
            }, 0);
        }
        function normalizedDotProduct(i, pts) {
            let a = pts[(i - 1 + pts.length) % pts.length];
            let b = pts[i];
            let c = pts[(i + 1) % pts.length];
            let p = normPt(subPts(a, b), 1.0);
            let q = normPt(subPts(c, b), 1.0);
            return p[0] * q[0] + p[1] * q[1];
        }
        function filterDotProduct(dotp) {
            if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold)
                return dotp;
            return 0;
        }
        function addPts(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
        function subPts(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
        function dist(a, b) { return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2); }
        function normPt(p, s) {
            let l = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
            return l === 0 ? [0, 0] : [p[0] / l * s, p[1] / l * s];
        }
    }
    function lat2latp(lat) {
        return (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + (lat * (Math.PI / 180)) / 2));
    }
    function latp2lat(a) {
        return (180 / Math.PI) * (2 * Math.atan(Math.exp((a * Math.PI) / 180)) - Math.PI / 2);
    }

    class E40 extends WMEBase {
        constructor(name, settings, tabButtons, placeButtons, pointButtons) {
            super(name, settings);
            this.onMouseEnter = ({ featureId }) => {
                this.showVector(featureId);
            };
            this.onMouseLeave = ({ featureId }) => {
                let selected = this.getSelectedVenue();
                if (selected?.id !== featureId) {
                    this.removeVector(this.wmeSDK.DataModel.Venues.getById({ venueId: featureId }));
                }
            };
            this.initTab(tabButtons);
            this.initPlacePanel(placeButtons);
            this.initShortcuts(placeButtons);
            this.initPointPanel(pointButtons);
            this.initLayer();
            this.initHandlers();
        }
        /**
         * Initialize the tab with buttons
         * @param {Object} buttons
         */
        initTab(buttons) {
            let tab = this.helper.createTab(WMEUI.t(NAME).title, {
                sidebar: this.wmeSDK.Sidebar,
                image: GM_info.script.icon
            });
            tab.addText('description', WMEUI.t(NAME).description);
            if (this.wmeSDK.State.getUserInfo().rank >= REQUIRED_LEVEL) {
                tab.addButtons(buttons);
            }
            else {
                tab.addText('warning', WMEUI.t(NAME).warning);
            }
            /** @type {WMEUIHelperFieldset} */
            let fsOptions = this.helper.createFieldset(WMEUI.t(NAME).options.title);
            let options = this.settings.get('options');
            let checkboxes = {};
            for (let item in options) {
                if (options.hasOwnProperty(item)) {
                    checkboxes[item] = {
                        title: WMEUI.t(NAME).options[item],
                        callback: (event) => {
                            this.settings.set('options', item, event.target.checked);
                            if (item === 'navigationPointOnHover') {
                                event.target.checked ? this.initHoverHandlers() : this.destroyHoverHandlers();
                            }
                        },
                        checked: this.settings.get('options', item),
                    };
                }
            }
            fsOptions.addCheckboxes(checkboxes);
            tab.addElement(fsOptions);
            tab.addDiv('text', WMEUI.t(NAME).help);
            tab.addText('info', '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version);
            tab.addText('blue', 'made in');
            tab.addText('yellow', 'Ukraine');
            tab.inject();
        }
        initPlacePanel(buttons) {
            this.placePanel = this.helper.createPanel(WMEUI.t(NAME).title);
            this.placePanel.addButtons(buttons);
        }
        initPointPanel(buttons) {
            this.pointPanel = this.helper.createPanel(WMEUI.t(NAME).title);
            this.pointPanel.addButtons(buttons);
        }
        initShortcuts(buttons) {
            for (let btn in buttons) {
                if (buttons.hasOwnProperty(btn)) {
                    let button = buttons[btn];
                    if (button.hasOwnProperty('shortcut')) {
                        this.createShortcut(btn, button.description, button.shortcut, button.callback);
                    }
                }
            }
        }
        initLayer() {
            this.wmeSDK.Map.addLayer({
                layerName: this.name,
                styleRules: layerConfig.styleRules,
                styleContext: layerConfig.styleContext
            });
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
        }
        initHandlers() {
            this.wmeSDK.Events.trackDataModelEvents({ dataModelName: "venues" });
            this.wmeSDK.Events.on({
                eventName: "wme-data-model-objects-changed",
                eventHandler: ({ dataModelName, objectIds }) => {
                    this.refreshPanel();
                    let selected = this.getSelectedVenue();
                    if (dataModelName === 'venues'
                        && selected
                        && objectIds.length > 0
                        && objectIds.indexOf(selected.id) !== -1
                        && this.settings.get('options', 'navigationPoint')) {
                        this.removeVectors();
                        this.showVector(selected.id);
                    }
                }
            });
            if (this.settings.get('options', 'navigationPointOnHover')) {
                this.initHoverHandlers();
            }
        }
        initHoverHandlers() {
            this.wmeSDK.Events.trackLayerEvents({ layerName: "venues" });
            this.wmeSDK.Events.on({ eventName: "wme-layer-feature-mouse-enter", eventHandler: this.onMouseEnter });
            this.wmeSDK.Events.on({ eventName: "wme-layer-feature-mouse-leave", eventHandler: this.onMouseLeave });
        }
        destroyHoverHandlers() {
            this.wmeSDK.Events.off({ eventName: "wme-layer-feature-mouse-enter", eventHandler: this.onMouseEnter });
            this.wmeSDK.Events.off({ eventName: "wme-layer-feature-mouse-leave", eventHandler: this.onMouseLeave });
            this.wmeSDK.Events.stopLayerEventsTracking({ layerName: "venues" });
        }
        showVector(featureId) {
            let venue = this.wmeSDK.DataModel.Venues.getById({ venueId: featureId });
            let center;
            if (venue.geometry.type === 'Polygon') {
                center = turf.centroid(venue.geometry).geometry.coordinates;
            }
            else {
                center = venue.geometry.coordinates;
            }
            let segments = this.wmeSDK.DataModel.Segments.getAll();
            let except = [TYPES.boardwalk, TYPES.stairway, TYPES.railroad, TYPES.runway];
            segments = segments.filter((segment) => except.indexOf(segment.roadType) === -1);
            if (venue.navigationPoints.length) {
                for (let i = 0; i < venue.navigationPoints.length; i++) {
                    let point = venue.navigationPoints[i].point.coordinates;
                    let nearestPoint = this.findNearestPoint(segments, point);
                    this.createVector(featureId + '_' + i, center, point, (i === 0) ? 'styleDashedLine' : 'styleDashedSecondaryLine');
                    this.createVector(featureId + '_' + i, point, nearestPoint, (i === 0) ? 'styleLine' : 'styleSecondaryLine');
                    if (i === 0
                        && !this.settings.get('options', 'navigationPointAll')) {
                        break;
                    }
                }
            }
            else {
                let nearestPoint = this.findNearestPoint(segments, center);
                this.createVector(featureId, center, nearestPoint, 'styleLine');
            }
            this.showLayer();
        }
        /**
         * Finds the nearest point to a given point from a set of segments.
         *
         * @param {Array} segments - An array of segments where each segment contains a geometry property representing a line.
         * @param {Object} point - The reference point to find the nearest point to.
         * @return {Array} An array representing the coordinates of the nearest point to the given point.
         */
        findNearestPoint(segments, point) {
            let nearestPoint, nearestPointCoordinates = [], nearestPointDistance;
            for (let i = 0; i < segments.length; i++) {
                let segment = segments[i];
                try {
                    nearestPoint = turf.nearestPointOnLine(segment.geometry, point);
                    let distance = turf.distance(nearestPoint, point, {
                        units: 'meters'
                    });
                    if (nearestPointDistance === undefined || distance < nearestPointDistance) {
                        nearestPointDistance = distance;
                        nearestPointCoordinates = nearestPoint.geometry.coordinates;
                    }
                }
                catch (e) {
                    this.log('Error while finding nearest point');
                }
            }
            return nearestPointCoordinates;
        }
        /**
         * Create the vector by coordinates
         * @param {String} featureId
         * @param {[Number,Number]} from coordinates
         * @param {[Number,Number]} to coordinates
         * @param {String} styleName style name
         */
        createVector(featureId, from, to, styleName = 'styleLine') {
            const A = turf.point(from, { styleName: "styleNode" }, { id: `${styleName}_from_${featureId}` });
            const B = turf.point(to, { styleName: "styleNode" }, { id: `${styleName}_to_${featureId}` });
            this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: A });
            this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: B });
            const lineCoordinates = [
                A.geometry.coordinates,
                B.geometry.coordinates,
            ];
            // https://www.waze.com/editor/sdk/interfaces/index.SDK.FeatureStyle.html
            const line = turf.lineString(lineCoordinates, {
                styleName: styleName,
            }, { id: `${styleName}_line_${featureId}` });
            this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: line });
        }
        /**
         * Remove all vectors from the layer for the current venue
         */
        removeVector(venue) {
            let featureIds = [];
            if (venue.navigationPoints?.length) {
                for (let i = 0; i < venue.navigationPoints.length; i++) {
                    let featureId = venue.id + '_' + i;
                    if (i === 0) {
                        featureIds.push(`styleLine_from_${featureId}`);
                        featureIds.push(`styleLine_to_${featureId}`);
                        featureIds.push(`styleLine_line_${featureId}`);
                        featureIds.push(`styleDashedLine_from_${featureId}`);
                        featureIds.push(`styleDashedLine_to_${featureId}`);
                        featureIds.push(`styleDashedLine_line_${featureId}`);
                    }
                    else {
                        featureIds.push(`styleSecondaryLine_from_${featureId}`);
                        featureIds.push(`styleSecondaryLine_to_${featureId}`);
                        featureIds.push(`styleSecondaryLine_line_${featureId}`);
                        featureIds.push(`styleDashedSecondaryLine_from_${featureId}`);
                        featureIds.push(`styleDashedSecondaryLine_to_${featureId}`);
                        featureIds.push(`styleDashedSecondaryLine_line_${featureId}`);
                    }
                }
            }
            else {
                let featureId = venue.id;
                featureIds = [
                    `styleLine_from_${featureId}`,
                    `styleLine_to_${featureId}`,
                    `styleLine_line_${featureId}`,
                ];
            }
            this.wmeSDK.Map.removeFeaturesFromLayer({ layerName: this.name, featureIds });
        }
        /**
         * Remove all vectors from the layer
         */
        removeVectors() {
            this.wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: this.name });
        }
        /**
         * Show the Layer
         */
        showLayer() {
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: true });
        }
        /**
         * Hide the Layer
         */
        hideLayer() {
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
        }
        /**
         * Handler for `place.wme` event
         * @param {jQuery.Event} event
         * @param {HTMLElement} element
         * @param {Venue} model
         */
        onPlace(event, element, model) {
            if (this.canEditVenue(model)) {
                this.createPlacePanel(event, element);
            }
        }
        /**
         * Handler for `point.wme` event
         * @param {jQuery.Event} event
         * @param {HTMLElement} element
         * @param {Venue} model
         */
        onPoint(event, element, model) {
            if (this.canEditVenue(model)) {
                this.createPointPanel(event, element);
            }
        }
        /**
         * Handler for `venue.wme` event
         * @param {jQuery.Event} event
         * @param {HTMLElement} element
         * @param {Venue} model
         */
        onVenue(event, element, model) {
            if (this.settings.get('options', 'navigationPoint')) {
                this.showVector(model.id);
            }
        }
        /**
         * Handler for `venues.wme` event
         * @param {jQuery.Event} event
         * @param {HTMLElement} element
         * @param {Venue[]} models
         * @return {Null}
         */
        onVenues(event, element, models) {
            models = models.filter((model) => !model.isResidential
                && this.canEditVenue(model));
            if (models.length > 0) {
                if (models[0].geometry.type === 'Polygon') {
                    this.createPlacePanel(event, element);
                }
                else {
                    this.createPointPanel(event, element);
                }
            }
        }
        /**
         * Handler for `none.wme` event
         * @return {Null}
         */
        onNone() {
            this.removeVectors();
            this.hideLayer();
        }
        /**
         * @param {String[]} except
         * @return {Venue[]} models
         */
        getAllPlaces(except = []) {
            let venues = this.getAllVenues(except);
            return venues.filter((venue) => venue.geometry.type === 'Polygon');
        }
        /**
         * @return {Venue[]} models
         */
        getSelectedPlaces() {
            let venues = this.getSelectedVenues();
            return venues.filter((venue) => venue.geometry.type === 'Polygon');
        }
        /**
         * Create the panel with buttons
         * @param event
         * @param {HTMLElement} element
         */
        createPlacePanel(event, element) {
            if (element?.querySelector('div.wme-ui-panel.e40')) {
                return;
            }
            element?.prepend(this.placePanel.html());
            this.updateLabel();
        }
        /**
         * Create the panel with buttons
         * @param event
         * @param {HTMLElement} element
         */
        createPointPanel(event, element) {
            if (element?.querySelector('div.wme-ui-panel.e40')) {
                return;
            }
            element?.prepend(this.pointPanel.html());
            this.updateLabel();
        }
        /**
         * Refresh the panel if something was changed
         */
        refreshPanel() {
            let venue = this.getSelectedVenue();
            let element = document.getElementById('venue-edit-general');
            element?.querySelector('div.wme-ui-panel.e40')?.remove();
            if (venue) {
                if (venue.geometry.type === 'Polygon') {
                    this.createPlacePanel(null, element);
                }
                else {
                    this.createPointPanel(null, element);
                }
            }
        }
        /**
         * Updated label
         */
        updateLabel() {
            let places = this.getSelectedVenues();
            if (places.length === 0) {
                return;
            }
            let info = [];
            for (let i = 0; i < places.length; i++) {
                let place = places[i];
                if (place.geometry.type === 'Polygon') {
                    info.push(Math.round(turf.area(place.geometry)) + 'mΒ²');
                }
            }
            let label = WMEUI.t(NAME).title;
            if (info.length) {
                label += ' (' + info.join(', ') + ')';
            }
            let elm = document.querySelector('div.wme-ui-panel.e40 wz-label');
            if (elm)
                elm.innerText = label;
        }
        /**
         * Scale places to X mΒ²
         * @param {Venue[]} elements
         * @param {Number} x square meters
         * @param {Boolean} orMore flag
         */
        scale(elements, x, orMore = false) {
            this.group('scale ' + (elements.length) + ' element(s) to ' + x + 'mΒ²');
            let total = 0;
            for (let i = 0; i < elements.length; i++) {
                try {
                    let scale = Math.sqrt((x + 5) / turf.area(elements[i].geometry));
                    if (scale < 1 && orMore) {
                        continue;
                    }
                    let geometry = turf.transformScale(elements[i].geometry, scale);
                    this.wmeSDK.DataModel.Venues.updateVenue({
                        venueId: elements[i].id, geometry
                    });
                    total++;
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was scaled');
            this.groupEnd();
        }
        /**
         * Orthogonalize place(s)
         * @param {Venue[]} elements
         */
        orthogonalize(elements) {
            this.group('orthogonalize ' + (elements.length) + ' element(s)');
            let total = 0;
            // skip points
            for (let i = 0; i < elements.length; i++) {
                try {
                    let geometry = elements[i].geometry;
                    let area = turf.area(elements[i].geometry);
                    geometry = simplifyPolygon(geometry);
                    geometry = normalizeRightAngles(geometry);
                    let scale = Math.sqrt(area / turf.area(geometry));
                    this.log('Apply scale ' + scale);
                    geometry = turf.transformScale(geometry, scale);
                    if (!this.compare(elements[i].geometry.coordinates[0], geometry.coordinates[0])) {
                        this.wmeSDK.DataModel.Venues.updateVenue({
                            venueId: elements[i].id, geometry
                        });
                        total++;
                    }
                    else {
                        this.log('The geometry is the same as before, skipped');
                    }
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was orthogonalized');
            this.groupEnd();
        }
        /**
         * Smooth place(s)
         * @param {Venue[]} elements
         */
        smooth(elements) {
            this.group('smooth ' + (elements.length) + ' element(s)');
            let total = 0;
            for (let i = 0; i < elements.length; i++) {
                try {
                    let geometry = turf.polygonSmooth(elements[i].geometry).features[0].geometry;
                    if (geometry.coordinates[0].length !== elements[i].geometry.coordinates[0].length) {
                        this.wmeSDK.DataModel.Venues.updateVenue({
                            venueId: elements[i].id, geometry
                        });
                        total++;
                    }
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was smoothed');
            this.groupEnd();
        }
        /**
         * Simplify place(s)
         * @param {Venue[]} elements
         * @param {Number} tolerance
         */
        simplify(elements, tolerance = 0.00001) {
            this.group('simplify ' + (elements.length) + ' element(s) with < tolerance=' + tolerance + ' >');
            let total = 0;
            for (let i = 0; i < elements.length; i++) {
                try {
                    let geometry = turf.simplify(elements[i].geometry, { tolerance });
                    if (geometry.coordinates[0].length !== elements[i].geometry.coordinates[0].length) {
                        this.wmeSDK.DataModel.Venues.updateVenue({
                            venueId: elements[i].id, geometry
                        });
                        total++;
                    }
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was simplified');
            this.groupEnd();
        }
        /**
         * Transform the Point to circle place
         *
         * @param {Venue[]} elements
         * @param {Number} area in square meters
         * @param {Number} steps
         */
        circle(elements, area, steps = 64) {
            this.group('transform ' + (elements.length) + ' element(s) to circle');
            let total = 0;
            for (let i = 0; i < elements.length; i++) {
                try {
                    let place = elements[i];
                    let geometry = place.geometry;
                    if (geometry.type !== 'Point') {
                        geometry = turf.centroid(geometry).geometry;
                    }
                    let circle = createCirclePolygon(geometry, area, steps);
                    this.wmeSDK.DataModel.Venues.updateVenue({
                        venueId: place.id, geometry: circle.geometry
                    });
                    total++;
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was transformed');
            this.groupEnd();
            this.selectVenues(elements.map((e) => String(e.id)));
        }
        /**
         * Transform the Point(s) to square place
         *
         * @param {Venue[]} elements
         * @param {Number} area in square meters
         */
        square(elements, area) {
            this.group('transform ' + (elements.length) + ' element(s) to square');
            let total = 0;
            for (let i = 0; i < elements.length; i++) {
                try {
                    let place = elements[i];
                    let geometry = place.geometry;
                    if (geometry.type !== 'Point') {
                        geometry = turf.centroid(geometry).geometry;
                    }
                    let square = createSquarePolygon(geometry, area);
                    this.wmeSDK.DataModel.Venues.updateVenue({
                        venueId: place.id, geometry: square.geometry
                    });
                    total++;
                }
                catch (e) {
                    this.log('skipped', e);
                }
            }
            this.log(total + ' element(s) was transformed');
            this.groupEnd();
            this.selectVenues(elements.map((e) => String(e.id)));
        }
        /**
         * @param {String[]} ids of venues
         */
        selectVenues(ids) {
            this.wmeSDK.Editing.clearSelection();
            // select changed elements
            setTimeout(() => this.wmeSDK.Editing.setSelection({ selection: {
                    ids: ids,
                    objectType: 'venue'
                } }), 100);
        }
        /**
         * Create copy for place
         * @param {Venue} venue
         * @return {String}
         */
        copyPlace(venue) {
            this.log('created a copy of the POI ' + venue.name);
            let geometry = turf.transformTranslate(venue.geometry, 0.01, 0.005);
            let venueId = this.wmeSDK.DataModel.Venues.addVenue({
                category: venue.categories[0],
                geometry: geometry
            });
            venueId = String(venueId);
            this.wmeSDK.DataModel.Venues.updateVenue({
                venueId,
                name: venue.name + ' (copy)',
                // isAdLocked: venue.isAdLocked,
                // isResidential: venue.isResidential,
            });
            let address = this.wmeSDK.DataModel.Venues.getAddress({ venueId: venue.id });
            if (address?.street?.id) {
                this.wmeSDK.DataModel.Venues.updateAddress({
                    venueId,
                    streetId: address.street.id,
                });
            }
            return venueId;
        }
        /**
         * Compare two polygons point-by-point
         *
         * @param {Array} coordinates1
         * @param {Array} coordinates2
         * @return boolean
         */
        compare(coordinates1, coordinates2) {
            if (coordinates1.length !== coordinates2.length) {
                return false;
            }
            for (let i = 0; i < coordinates1.length; i++) {
                if (Math.abs(coordinates1[i][0] - coordinates2[i][0]) > .000001
                    || Math.abs(coordinates1[i][1] - coordinates2[i][1]) > .000001) {
                    return false;
                }
            }
            return true;
        }
    }

    var css_248z = ".e40 .wme-ui-panel-content {\n  display: grid;\n  grid-template-columns: repeat(6, 44px);\n  gap: 6px;\n  padding: 0;\n}\n\n.e40 .wme-ui-tab-content {\n  padding: 8px;\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0 6px;\n  align-items: center;\n}\n\n.e40 .wme-ui-tab-content button.e40 {\n  min-height: 30px;\n  line-height: 25px;\n  margin-bottom: 16px;\n}\n\n.e40 button.e40 {\n  width: 44px;\n  margin: 0;\n  padding: 2px;\n  display: flex;\n  justify-content: center;\n  border: 1px solid #eee;\n  cursor: pointer;\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n  white-space: nowrap;\n  color: #333;\n  flex-wrap: wrap;\n  align-content: center;\n}\n\n.e40 button.e40:hover {\n  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1), inset 0 0 100px 100px rgba(255, 255, 255, 0.3);\n}\n\n.e40 button.e40-M,\n.e40 button.e40-N,\n.e40 button.e40-O,\n.e40 button.e40-P,\n.e40 button.e40-R,\n.e40 button.e40-S {\n  min-height: 50px;\n}\n\n.wme-ui-panel.e40 legend {\n  cursor: pointer;\n  font-size: 12px;\n  font-weight: bold;\n  width: auto;\n  text-align: right;\n  border: 0;\n  margin: 0;\n  padding: 0 8px;\n}\n\n.wme-ui-panel.e40 fieldset {\n  border: 1px solid #ddd;\n  padding: 8px;\n  width: 100%;\n  margin-bottom: 16px;\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content {\n  display: block;\n  padding: 8px;\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content:empty,\n#panel-container .archive-panel .body:empty {\n  min-height: 20px;\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content:empty::after,\n#panel-container .archive-panel .body:empty::after {\n  color: #ccc;\n  padding: 0 8px;\n  content: \"\\2014\";\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content label {\n  white-space: normal;\n  font-weight: normal;\n  margin-top: 5px;\n  line-height: 18px;\n  font-size: 13px;\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content input[type=\"text\"] {\n  float: right;\n}\n\nsection.tab-pane .wme-ui-panel.e40 .wme-ui-fieldset-content input[type=\"number\"] {\n  float: right;\n  width: 60px;\n  text-align: right;\n}\n\n#sidebar p.e40 {\n  width: 100%;\n}\n\n#sidebar p.e40-info {\n  border-top: 1px solid #ccc;\n  color: #777;\n  font-size: x-small;\n  margin-top: 15px;\n  padding-top: 10px;\n  text-align: center;\n}\n\n#sidebar p.e40-warning {\n  color: #f77;\n}\n\n#sidebar p.e40-blue {\n  background-color: #0057B8;\n  color: white;\n  height: 32px;\n  text-align: center;\n  line-height: 32px;\n  font-size: 24px;\n  margin: 0;\n}\n\n#sidebar p.e40-yellow {\n  background-color: #FFDD00;\n  color: black;\n  height: 32px;\n  text-align: center;\n  line-height: 32px;\n  font-size: 24px;\n  margin: 0;\n}\n";

    $(document).on('bootstrap.wme', () => {
        WMEUI.addTranslation(NAME, TRANSLATION);
        WMEUI.addStyle(css_248z);
        let instance = new E40(NAME, SETTINGS, getTabButtons(), getPlaceButtons(), getPointButtons());
        setE40Instance(instance);
    });

})();