WME E40 Geometry

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    });

})();