WME E40 Geometry

A script that allows aligning, scaling, and copying POI geometry

이 스크립트를 설치하려면 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.10.6
// @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,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgMCCcJi6hsjAAAB1lJREFUeNrtmn9QVNcVxz/v7Q8WcPmlAgs0wIgzVTYaLdHRjlGiNeZHZdJ0zGC1tkpsK2nStBkyWwnB2mLUNNHWSdKCyUyGODaSNGCM4xRDpnb8VU39AdgOmARlZZVxRX4v7L7XP3ZZ9smPfZCQkGW/Mzuz977z3r3n3HO+95z3LgQRRBBBBBFEEEEEEcQEx+YJpGth/1+BQp+OF4gDXga+LyAYA0lnGbkNOAj8mi1c7zOEFtkjYeHt9Nj01UVLi0iLSSNEExJQi+5wOYz19vrVlkrL6lpL7dtsY41Xdwo4lFOeI08UbKzYKFPAIXcI5LPEbDJXXdx0EYDjV4+TU5FDQ0sDAkKguD8p0SkUryxmYdJCAMyvmqlpqskUyKfMmmd9LMGYwOG6wzz01kMgQoDo7msFkODDdR/yYNqDWNusJO1Ielc0RZrujwuPA2B9+XrQBKDyeHTSwIbyDQDEh8cTHxmfKeq1+ug+hW3NtoDfAZuam7wGCdGGxGhVx3n3KEcUQdDTT7jDQfL8+qD1I+8CnB73Ft0rjEZFKHidQvA7BAAGnRF7YSuho9D/akMJd/3lCdD7l52VtopNM+6jR5IIk5vJObgVdIPLhupi2JT5O/IycokNh5aW8+w8uZ1dp9+hU3Kpnp9WHX/ItPdAqH7kBuhwdqviFI2oY39WMTMiIzw9l8l5fxADyJBkeoRz699jsr7/YlTUbP6wYh+bFxUxb880aroktQ46DuCCxffs9FEekAdXwGCcQcPPDyqUV3hKeArHfnYKncrQHnMDqOEYMeJujq58WtVWVrhs17CTFoDoqAzy081KPvkiITBwItfI3PtDNPowv6KdnY3DjyJBfuZWlZ5iYN3s5Yqu98+8yO7aI2xctJPs1Axvf8EPDrOjOomOMTEAnXxcfwIMI9iDh0BIxFyez8hStfozZ+UR7/Os85dLefTvFtDBxw3LWf5bO5P7dgFNIgtMCVTaro1nDtDzUc4HapmYteY1iq6y6n39JNlzi81njiiu32uaM45JUIbUxJXMjzJ5UwCHc5hkQzSwKnWqouti02mFJmU15Yrr34nzzwOjN4Ck8jfk/QYO/eSAN29paHiHanvj0FGkCydCUDJ/fctNRZjdtJ3ENwO4K8LkNwMbJQdE8avFuYja4d8ZyLKLkn//iTbXwFn8ePEOZnj1aWbum9kczf10yGeF6gxoNb7TdXC9447Mz3GDLmCSpxkXmeReBM2XbQBhCq88vEeV6Lvn9tDmuiMzkyfx+0VPeptvHttKi6Adliy1ogZRIWCno/cO5YQunD7NyJApXy8HOF2OgS4owZbHj/OtkD5lWnm+6s/+92tBiyj4TteJNMCx7HT7WCBSP8lvCHzlJBgbbSbv22Zv+5f7MrE6VWRsgoAoCCPkWf8l2Cg5wMFZ6wUEcfjSyyX10iP7TKIHtj5agUF0K9LWeoHi+nOqlkGSZSTZn0IaRB8buWRpjAwgXyVj1zx1iZCPjebc/RQbU1K97ScP3I/DJXliXx7orj5tp+xCUggM5g0R6H3Ga+/t9FuIaUfty2pq7zuU2b3sOW/zhv0U3cZlrJnt2QoELTGGcJ8bjGTPfQyNLoyzVyr5zNGLJLnoz3yiCdeDQzFGhKLqvu1oHqsQGGX8h8b0/4+Zz99W7R/aXkI8+x4vAyDvvRW8dOkMLkWdH058GNh9LWCYonhn0dza5De8vmISlFVL+npur+RC7u3ALvUoZJKNRsWjExPuU9xXd+vKeDPAF4DUTeX1TkXX0ukPKN4pZM9eq7h+1lY9VhwgQ+8I7vbInWioojF0iOxREJiXtBCjzuCtOCvrjyFodFztsIMAJRfe4hepFu8tWeYnePZYmXsZQxMpuEdZ/Pzn+vkx4gAhja7tXapEDVoDC14SONkOPz3w8DDP1HE2t465U5I9NrbyveIVeINagE9Ob6E5y0JfSZSWuBzrM5+w+9x+1s23oPiY6bhIldXmV8NReoCAQWtQLS2OOOqH6NI6KL1wlGdmLfV2JUyew/alA8vepw5kI4tf1tzGCwR49kgu7X5e+jbe+BevffpfVdp9swwASO3/I3mXmXbX4Fleh/0E5r8uwqny1biqEHC5etj+zwLCNCOfsF4TypUeVaPw+qmXiQ+LdJe/sn3w2Qlgb63BuG0quffm8kDKd4kNj+Zmaz3/+Owwe86U4pQF1VuukPxKsnz56ctoBA3CcwKj+vrxtbqE5yeq9OdukF+Ucckupu2ehtjj7LnVZyxTrIlvHESPH6sM5oSpCd6d3OF02MWm1qaPbB3uj6JvZL3h/t4mE3iQ3cnS3qy9ANjabdhu26oE8lmSbkqvqt5U7U5WGk+QU5HD57c+D7gDEiUrS1iQtACA9FfTqb1Wu8QtUUDFhvINE+2IzEE3p74AbAEslM5MnPmjomVFTI+ZHoiHpKi7WYflqIVL1kulbGMthYMfk/sj8IiAEBlYFCDfBj4AfuN7TK4fE+mg5ETSNYgggggiiCCCCCKIIAbH/wEkSypmWfyFAwAAAABJRU5ErkJggg==
// @grant        none
// @require      https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// @require      https://update.greasyfork.org/scripts/450221/1691071/WME-Base.js
// @require      https://update.greasyfork.org/scripts/450320/1688694/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/uk/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: I18n.t(NAME).smooth,
                shortcut: 'S+49',
                callback: () => smooth()
            },
            B: {
                title: '<i class="fa fa-square-o" aria-hidden="true"></i>',
                description: I18n.t(NAME).orthogonalize,
                shortcut: 'S+50',
                callback: () => orthogonalize()
            },
            C: {
                title: '1️⃣ 📐',
                description: I18n.t(NAME).simplify + ' (tolerance = 0.00001)',
                shortcut: null,
                callback: () => simplify(0.00001)
            },
            D: {
                title: '3️⃣ 📐',
                description: I18n.t(NAME).simplify + ' (tolerance = 0.00003)',
                shortcut: null,
                callback: () => simplify(0.00003)
            },
            E: {
                title: '5️⃣ 📐',
                description: I18n.t(NAME).simplify + ' (tolerance = 0.00005)',
                shortcut: null,
                callback: () => simplify(0.00005)
            },
            F: {
                title: '<i class="fa fa-clone" aria-hidden="true"></i>',
                description: I18n.t(NAME).copy,
                shortcut: null,
                callback: () => copyPlaces()
            },
            G: {
                title: '<i class="fa fa-repeat" aria-hidden="true"></i>',
                description: I18n.t(NAME).rotate,
                shortcut: 'S+51',
                callback: () => enablePolygonRotation()
            },
            H: {
                title: '<i class="fa fa-expand" aria-hidden="true"></i>',
                description: I18n.t(NAME).scale,
                shortcut: 'S+52',
                callback: () => enablePolygonResize()
            },
            I: {
                title: '500m²',
                description: I18n.t(NAME).scale + ' 500m²',
                shortcut: 'S+53',
                callback: () => scale(500)
            },
            J: {
                title: '650m²',
                description: I18n.t(NAME).scale + ' 650m²',
                shortcut: 'S+54',
                callback: () => scale(650)
            },
            K: {
                title: '650+',
                description: I18n.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: I18n.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: I18n.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: I18n.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: I18n.t(NAME).square,
                shortcut: null,
                callback: () => square(500)
            },
            R: {
                title: '<i class="fa fa-square-o fa-2x" aria-hidden="true"></i> 650m²',
                description: I18n.t(NAME).square,
                shortcut: null,
                callback: () => square(650)
            },
            S: {
                title: '<i class="fa fa-square-o fa-2x" aria-hidden="true"></i> 1000m²',
                description: I18n.t(NAME).square,
                shortcut: null,
                callback: () => square(1000)
            },
        };
    }
    function getTabButtons() {
        return {
            A: {
                title: '<i class="fa fa-square-o" aria-hidden="true"></i>',
                description: I18n.t(NAME).orthogonalize,
                callback: () => orthogonalizeAll()
            },
            B: {
                title: '1️⃣ 📐',
                description: I18n.t(NAME).simplify,
                callback: () => simplifyAll(0.00001)
            },
            C: {
                title: '3️⃣ 📐',
                description: I18n.t(NAME).simplify,
                callback: () => simplifyAll(0.00003)
            },
            D: {
                title: '5️⃣ 📐',
                description: I18n.t(NAME).simplify,
                callback: () => simplifyAll(0.00005)
            },
            E: {
                title: '500+',
                description: I18n.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,
                }
            }
        ],
    };

    /**
     * A utility class for spherical geometry (geodesy).
     * Assumes points are [longitude, latitude] in degrees.
     */
    class GeoUtils {
        /**
         * @param {number} degrees
         * @return {number} radians
         * @private
         */
        static _toRadians(degrees) {
            return degrees * (Math.PI / 180);
        }
        /**
         * @param {number} radians
         * @return {number} degrees
         * @private
         */
        static _toDegrees(radians) {
            return radians * (180 / Math.PI);
        }
        /**
         * Normalizes an angle to the range -180 to +180 degrees.
         *
         * @param {number} degrees
         * @return {number} degrees
         */
        static _normalizeAngle(degrees) {
            return (degrees + 540) % 360 - 180;
        }
        /**
         * Calculates the initial bearing from pA to pB.
         *
         * @param {[number,number]} pA - [lon, lat] of start point.
         * @param {[number,number]} pB - [lon, lat] of end point.
         * @returns {number} Initial bearing in degrees (0-360).
         */
        static getBearing(pA, pB) {
            const latA = GeoUtils._toRadians(pA[1]);
            const lonA = GeoUtils._toRadians(pA[0]);
            const latB = GeoUtils._toRadians(pB[1]);
            const lonB = GeoUtils._toRadians(pB[0]);
            const deltaLon = lonB - lonA;
            const y = Math.sin(deltaLon) * Math.cos(latB);
            const x = Math.cos(latA) * Math.sin(latB) -
                Math.sin(latA) * Math.cos(latB) * Math.cos(deltaLon);
            const bearingRad = Math.atan2(y, x);
            // Convert from -180/+180 to 0-360
            return (GeoUtils._toDegrees(bearingRad) + 360) % 360;
        }
        /**
         * Calculates the interior angle at vertex p2.
         *
         * @param {[number,number]} p1
         * @param {[number,number]} p2
         * @param {[number,number]} p3
         */
        static findAngle(p1, p2, p3) {
            const bearing21 = GeoUtils.getBearing(p2, p1);
            const bearing23 = GeoUtils.getBearing(p2, p3);
            let angle = Math.abs(bearing21 - bearing23);
            if (angle > 180) {
                angle = 360 - angle;
            }
            return angle;
        }
        /**
         * Calculate the approximate distance between two coordinates (lat/lon)
         *
         * @param {[number,number]} pA - [lon, lat] of start point.
         * @param {[number,number]} pB - [lon, lat] of end point.
         * @return {number} The distance in meters.
         */
        static getDistance(pA, pB) {
            return GeoUtils.getAngularDistance(pA, pB) * 6371000;
        }
        /**
         * Calculates the angular distance between two points using the Haversine formula.
         *
         * @param {[number,number]} pA - [lon, lat] of start point.
         * @param {[number,number]} pB - [lon, lat] of end point.
         * @returns {number} The angular distance in radians.
         */
        static getAngularDistance(pA, pB) {
            const latA = GeoUtils._toRadians(pA[1]);
            const lonA = GeoUtils._toRadians(pA[0]);
            const latB = GeoUtils._toRadians(pB[1]);
            const lonB = GeoUtils._toRadians(pB[0]);
            const deltaLat = latB - latA;
            const deltaLon = lonB - lonA;
            const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
                Math.cos(latA) * Math.cos(latB) *
                    Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
            // c is the angular distance in radians
            return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        }
        /**
         * Calculates the destination point given a start point, bearing, and distance.
         * @param {[number,number]} startPoint - [lon, lat] of start point.
         * @param {number} bearing - Bearing in degrees (0-360).
         * @param {number} distanceRad - Angular distance in radians.
         * @returns {number[]} The destination point [lon, lat] in degrees.
         */
        static getDestination(startPoint, bearing, distanceRad) {
            const lat1 = GeoUtils._toRadians(startPoint[1]);
            const lon1 = GeoUtils._toRadians(startPoint[0]);
            const brng = GeoUtils._toRadians(bearing);
            const d = distanceRad;
            const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) +
                Math.cos(lat1) * Math.sin(d) * Math.cos(brng));
            const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(lat1), Math.cos(d) - Math.sin(lat1) * Math.sin(lat2));
            // Normalize longitude to -180 to +180
            const lon2Deg = GeoUtils._toDegrees(lon2);
            const lat2Deg = GeoUtils._toDegrees(lat2);
            return [(lon2Deg + 540) % 360 - 180, lat2Deg];
        }
        /**
         * Finds a point D on the great circle path AB such that the angle ADC equals the specified angle.
         *
         * @param {[number,number]} pA - Start of line [lon, lat]
         * @param {[number,number]} pB - End of line [lon, lat]
         * @param {[number,number]} pC - The third point [lon, lat]
         * @param {number} angle - The desired intersection angle at D in degrees (e.g., 90 for perpendicular).
         * @returns {[number,number] | null} The coordinates of D, or null if no such intersection exists.
         */
        static findIntersection(pA, pB, pC, angle) {
            // Guard: degenerate angle (0° or 180°) makes cot(D) undefined
            if (angle % 180 === 0) {
                return null;
            }
            const angleRad = GeoUtils._toRadians(angle);
            // 1. Calculate Angle A (difference in bearings)
            const bearingAB = GeoUtils.getBearing(pA, pB);
            const bearingAC = GeoUtils.getBearing(pA, pC);
            const angleA_rad = GeoUtils._toRadians(bearingAC - bearingAB);
            // 2. Calculate Side b (distance AC)
            const distb_rad = GeoUtils.getAngularDistance(pA, pC);
            // Guard: pA and pC are the same point — cot(b) is undefined
            if (distb_rad < 1e-12) {
                return null;
            }
            // 3. Solve for distance AD (Side c) using the Four-Part Formula (Cotangent Law)
            // The relation for parts (Side b, Angle A, Side c, Angle D) is:
            // sin(c) * cot(b) - cos(c) * cos(A) = sin(A) * cot(D)
            // We solve this linear combination of sin(c) and cos(c) by transforming it into:
            // R * sin(c - phi) = K
            const cot_b = 1.0 / Math.tan(distb_rad);
            const cot_D = 1.0 / Math.tan(angleRad);
            // Coefficients for harmonic addition
            // sin(c)*X - cos(c)*Y = Z
            // X = cot_b, Y = cos(angleA), Z = sin(angleA) * cot_D
            const X = cot_b;
            const Y = Math.cos(angleA_rad);
            const Z = Math.sin(angleA_rad) * cot_D;
            // Calculate auxiliary angle phi and magnitude R
            // We match form: R * sin(c - phi) = Z
            // where R * cos(phi) = X and R * sin(phi) = Y
            const R = Math.hypot(X, Y);
            const phi = Math.atan2(Y, X); // atan2(y, x) -> atan2(cosA, cot_b)
            // Check if solution exists (Z/R must be between -1 and 1)
            const sin_c_minus_phi = Z / R;
            if (Math.abs(sin_c_minus_phi) > 1) {
                return null; // The requested angle is impossible to form (e.g., triangle inequality violation)
            }
            // 4. Calculate final distance c (distAD)
            // c - phi = asin(...)
            const distAD_rad = phi + Math.asin(sin_c_minus_phi);
            // 5. Calculate coordinates of D
            return GeoUtils.getDestination(pA, bearingAB, distAD_rad);
        }
        /**
         * Calculates the coordinates of point D in a right-angled spherical triangle ADC,
         * using Angle A and the hypotenuse AC.
         * Triangle ADC has a right angle at D (angle D = 90 deg),
         * and angle A and side AC are preserved from the original triangle ABC.
         *
         * @param {[number,number]} pA - [lon, lat] of point A.
         * @param {[number,number]} pB - [lon, lat] of point B (used to calculate angle A).
         * @param {[number,number]} pC - [lon, lat] of point C.
         * @returns {[number,number]} The coordinates [lon, lat] of point D.
         */
        static findRightAngleIntersection(pA, pB, pC) {
            // 1. Calculate the required angle at A (angle A_ABC)
            // The angle at A in triangle ABC is the interior angle at pA.
            const angleA_deg = GeoUtils.findAngle(pB, pA, pC);
            const angleA_rad = GeoUtils._toRadians(angleA_deg);
            // 2. Calculate the common side AC (side 'b' in spherical triangle ADC)
            // This is the hypotenuse of the right triangle ADC.
            const distAC_rad = GeoUtils.getAngularDistance(pA, pC);
            // 3. Use Napier's Rules to find side 'c' (distance AD)
            // In right triangle ADC: D = 90 deg, angle A is known, hypotenuse b (AC) is known.
            // We want to find side 'c' (distance AD), which is adjacent to angle A.
            // The correct spherical formula relating adjacent side 'c', hypotenuse 'b', and angle 'A' is:
            // cos(A) = tan(c) / tan(b)
            // Therefore, tan(c) = cos(A) * tan(b)
            // Where:
            // c = distAD_rad (unknown side)
            // b = distAC_rad (hypotenuse)
            // A = angleA_rad (known angle)
            const tan_c = Math.cos(angleA_rad) * Math.tan(distAC_rad);
            const distAD_rad = Math.abs(Math.atan(tan_c));
            // 4. Determine the bearing from A to D
            // The bearing from A to D is the bearing from A to C, adjusted by the angle A.
            const bearingAC_deg = GeoUtils.getBearing(pA, pC);
            // The bearing A->D must be rotated from A->C such that D forms a right angle with CD.
            // This requires D to be along the great circle arc that is perpendicular to C->D.
            // Bearing from A to B
            const bearingAB_deg = GeoUtils.getBearing(pA, pB);
            // Calculate the signed difference: bearingAC - bearingAB
            const angleCAB_raw_diff = GeoUtils._normalizeAngle(bearingAC_deg - bearingAB_deg);
            let bearingAD_deg;
            // The point D is found by rotating the bearing A->C away from B, by the interior angle A.
            if (angleCAB_raw_diff >= 0) {
                // B is counter-clockwise from AC (left side)
                // D needs to be on the other side of AC
                bearingAD_deg = GeoUtils._normalizeAngle(bearingAC_deg - angleA_deg);
            }
            else {
                // B is clockwise from AC (right side)
                // D needs to be on the other side of AC
                bearingAD_deg = GeoUtils._normalizeAngle(bearingAC_deg + angleA_deg);
            }
            // 5. Calculate the destination point D
            // Start point: pA
            // Bearing: bearingAD_deg
            // Distance: distAD_rad
            return GeoUtils.getDestination(pA, bearingAD_deg, distAD_rad);
        }
    }

    /**
     * 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]
        };
    }
    /**
     * Moves vertices (P_curr) that form a near-90° angle (85-89.9 or 90.1-95)
     * to a new position (P'_curr) that forms exactly 90°.
     * @param {object} geojsonObject A GeoJSON Feature<Polygon> or Polygon geometry object.
     * @returns {object} The angle-normalized GeoJSON Polygon geometry object.
     */
    function normalizeRightAngles(geojsonObject) {
        let points = geojsonObject.coordinates[0].slice();
        let pointsAdjusted = 0;
        let totalIterations = 0;
        let changedInPass = true;
        console.log("--- Starting Angle Normalization (Near 90° adjustment) ---");
        // Iterate until no points are adjusted in a full pass
        while (changedInPass && totalIterations < 10) { // Safety limit for iterations
            changedInPass = false;
            totalIterations++;
            console.log(`[Iter ${totalIterations}] Start`);
            // Check points from index 1 up to length - 2.
            for (let i = 1; i < points.length; i++) {
                const pPrev = points[i - 1];
                const pCurr = points[i];
                const pNext = (i === points.length - 1) ? points[1] : points[i + 1];
                const angle = GeoUtils.findAngle(pPrev, pCurr, pNext);
                console.log(`[Point ${i}] Angle:`, angle.toFixed(4));
                // Check if the angle is in the target normalization ranges
                const inRange1 = angle >= 75.0 && angle <= 89.9;
                const inRange2 = angle >= 90.1 && angle <= 105.0;
                if (inRange1 || inRange2) {
                    // Round coordinates to 6 decimal places for GeoJSON compatibility
                    points[i] = GeoUtils.findRightAngleIntersection(pPrev, pCurr, pNext);
                    let new_angle = GeoUtils.findAngle(pPrev, points[i], pNext);
                    pointsAdjusted++;
                    changedInPass = true;
                    console.log(`[Point ${i}] Angle ${angle.toFixed(4)}° adjusted to ${new_angle.toFixed(4)}°.`);
                    // The loop continues in the same pass. If points[i] is adjusted,
                    // it affects the angle calculations for P_{i-1} and P_{i+1} in the next passes.
                }
            }
        }
        // Ensure the closure point is updated after all adjustments
        points[points.length - 1] = points[0];
        console.log(`--- Normalization Finished. Total points adjusted: ${pointsAdjusted} in ${totalIterations} passes. ---`);
        return {
            type: "Polygon",
            coordinates: [points]
        };
    }

    class E40 extends WMEBase {
        constructor(name, settings, tabButtons, placeButtons, pointButtons) {
            super(name, settings);
            this.initHelper();
            this.initTab(tabButtons);
            this.initPlacePanel(placeButtons);
            this.initShortcuts(placeButtons);
            this.initPointPanel(pointButtons);
            this.initLayer();
            this.initHandlers();
        }
        /**
         * Initializes the helper instance for the class
         * by creating a new WMEUIHelper object.
         */
        initHelper() {
            this.helper = new WMEUIHelper(this.name);
        }
        /**
         * Initialize the tab with buttons
         * @param {Object} buttons
         */
        initTab(buttons) {
            let tab = this.helper.createTab(I18n.t(this.name).title, {
                sidebar: this.wmeSDK.Sidebar,
                image: GM_info.script.icon
            });
            tab.addText('description', I18n.t(this.name).description);
            if (this.wmeSDK.State.getUserInfo().rank >= REQUIRED_LEVEL) {
                tab.addButtons(buttons);
            }
            else {
                tab.addText('warning', I18n.t(this.name).warning);
            }
            /** @type {WMEUIHelperFieldset} */
            let fsOptions = this.helper.createFieldset(I18n.t(this.name).options.title);
            let options = this.settings.get('options');
            for (let item in options) {
                if (options.hasOwnProperty(item)) {
                    fsOptions.addCheckbox(item, I18n.t(this.name).options[item], (event) => this.settings.set(['options', item], event.target.checked), this.settings.get('options', item));
                }
            }
            tab.addElement(fsOptions);
            tab.addDiv('text', I18n.t(this.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(I18n.t(this.name).title);
            this.placePanel.addButtons(buttons);
        }
        initPointPanel(buttons) {
            this.pointPanel = this.helper.createPanel(I18n.t(this.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')) {
                        let shortcut = {
                            callback: button.callback,
                            description: button.description,
                            shortcutId: this.id + '-' + btn,
                            shortcutKeys: button.shortcut,
                        };
                        if (shortcut.shortcutKeys && this.wmeSDK.Shortcuts.areShortcutKeysInUse({ shortcutKeys: shortcut.shortcutKeys })) {
                            this.log('Shortcut already in use');
                            shortcut.shortcutKeys = null;
                        }
                        this.wmeSDK.Shortcuts.createShortcut(shortcut);
                    }
                }
            }
        }
        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.wmeSDK.Events.trackLayerEvents({ layerName: "venues" });
                this.wmeSDK.Events.on({
                    eventName: "wme-layer-feature-mouse-enter",
                    eventHandler: ({ featureId }) => {
                        this.showVector(featureId);
                    },
                });
                this.wmeSDK.Events.on({
                    eventName: "wme-layer-feature-mouse-leave",
                    eventHandler: ({ featureId }) => {
                        let selected = this.getSelectedVenue();
                        if (selected?.id !== featureId) {
                            this.removeVector(this.wmeSDK.DataModel.Venues.getById({ venueId: featureId }));
                        }
                    },
                });
            }
        }
        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.wmeSDK.DataModel.Venues.hasPermissions({ venueId: model.id })) {
                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.wmeSDK.DataModel.Venues.hasPermissions({ venueId: model.id })) {
                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.wmeSDK.DataModel.Venues.hasPermissions({ venueId: model.id }));
            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.form-group.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.form-group.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.form-group.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 = I18n.t(NAME).title;
            if (info.length) {
                label += ' (' + info.join(', ') + ')';
            }
            let elm = document.querySelector('div.form-group.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 .controls {\n  display: grid;\n  grid-template-columns: repeat(6, 44px);\n  gap: 6px;\n  padding: 0;\n}\n\n.e40 .button-toolbar {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0 6px;\n  align-items: center;\n}\n\n.e40 .button-toolbar 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.form-group.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.form-group.e40 fieldset {\n  border: 1px solid #ddd;\n  padding: 8px;\n  width: 100%;\n  margin-bottom: 16px;\n}\n\nsection.tab-pane .form-group.e40 div.controls {\n  display: block;\n  padding: 8px;\n}\n\nsection.tab-pane .form-group.e40 div.controls:empty,\n#panel-container .archive-panel .body:empty {\n  min-height: 20px;\n}\n\nsection.tab-pane .form-group.e40 div.controls: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 .form-group.e40 div.controls 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 .form-group.e40 div.controls input[type=\"text\"] {\n  float: right;\n}\n\nsection.tab-pane .form-group.e40 div.controls 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";

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

})();