Segment Smoothing Tool

Smooths the geometry of the selected segment in Waze Map Editor

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Segment Smoothing Tool
// @namespace    https://github.com/georgenqueiroz
// @version      0.1
// @description  Smooths the geometry of the selected segment in Waze Map Editor
// @author       George Queiroz (georgenqueiroz)
// @match        https://www.waze.com/editor*
// @match        https://*.waze.com/*/editor*
// @match        https://*.waze.com/editor*
// @license      MIT
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    
    let wmeSDK = null;

    const TRANSLATIONS = {
        en: {
            lbl_this_script_is_currently_in_beta: "This script is currently in beta.",
            lbl_select_a_segment_to_smooth: "Select a segment to smooth.",
            lbl_to_smooth: "Smooth",

            alert_no_segment_selected: "No segment selected.",
            alert_the_tool_only_works_with_one_segment_at_a_time: "The tool only works with one segment at a time.",
            alert_the_selected_item_is_not_a_segment: "The selected item is not a segment.",
            alert_this_segment_has_too_many_geometry_nodes: "This segment has too many geometry nodes.",
            alert_this_segment_is_a_straight_line: "This segment is a straight line. Please add a geometry node manually before smoothing."
        },
        pt: {
            lbl_this_script_is_currently_in_beta: "Este script está atualmente em beta.",
            lbl_select_a_segment_to_smooth: "Selecione um segmento para suavizar.",
            lbl_to_smooth: "Suavizar",
            alert_no_segment_selected: "Nenhum segmento selecionado.",
            alert_the_tool_only_works_with_one_segment_at_a_time: "A ferramenta só funciona com um segmento por vez.",
            alert_the_selected_item_is_not_a_segment: "O item selecionado não é um segmento.",
            alert_this_segment_has_too_many_geometry_nodes: "Este segmento tem muitos nós de geometria.",
            alert_this_segment_is_a_straight_line: "Este segmento é uma linha reta. Por favor, adicione um nó de geometria manualmente antes de suavizar."
        }
    };

    function _t(key) {
        let language = I18n.locale && I18n.locale.toLowerCase();
        const isPt = language && language.startsWith('pt');
        const lang = isPt ? 'pt' : 'en';

        let text = TRANSLATIONS[lang][key] || key;
        return text;
    }

    //Smoothing - Chaikin's Algorithm
    function chaikinSmooth(coords, iterations = 1) {
        if (iterations <= 0) return coords;
        if (coords.length < 3) return coords;

        let smoothed = [];

        const Ax = Number(coords[0][0]);
        const Ay = Number(coords[0][1]);
        smoothed.push([Ax, Ay]);

        for (let i = 0; i < coords.length - 1; i++) {            
            const p0x = Number(coords[i][0]);
            const p0y = Number(coords[i][1]);
            const p1x = Number(coords[i + 1][0]);
            const p1y = Number(coords[i + 1][1]);

            // Chaikin: 25% e 75%
            const Qx = 0.75 * p0x + 0.25 * p1x;
            const Qy = 0.75 * p0y + 0.25 * p1y;

            const Rx = 0.25 * p0x + 0.75 * p1x;
            const Ry = 0.25 * p0y + 0.75 * p1y;

            smoothed.push([Qx, Qy]);
            smoothed.push([Rx, Ry]);
        }

        const Bx = Number(coords[coords.length - 1][0]);
        const By = Number(coords[coords.length - 1][1]);
        smoothed.push([Bx, By]);

        return chaikinSmooth(smoothed, iterations - 1);
    }

    function getSegmentNodeLimit(segment) {
        const lengthInMeters = segment.attributes ? segment.attributes.length : (segment.length || 0);
        const limit = Math.floor(lengthInMeters);
        const hardLimit = 60;
        return Math.min(limit, hardLimit);
    }
    
    async function smoothSegmentButtonClick()
    {        
        if (!wmeSDK) {
            try {
                wmeSDK = getWmeSdk({scriptId: "smooth-segment-tool", scriptName: "Segment Smoothing Tool"});
            } catch (e) {
                alert("Error initializing WME SDK. Try reloading the page.");
                return;
            }
        }
        
        const selection = W.selectionManager.getSelectedWMEFeatures();
        
        if (selection.length === 0) {
            alert(_t("alert_no_segment_selected"));
            return;
        }

        if (selection.length > 1) {
            alert(_t("alert_the_tool_only_works_with_one_segment_at_a_time"));
            return;
        }

        const selectedItem = selection[0];

        if (selectedItem.featureType !== "segment") {
             alert(_t("alert_the_selected_item_is_not_a_segment"));
             return;
        }
        
        const segId = selectedItem.attributes ? selectedItem.attributes.id : selectedItem.id;
        const realSegment = W.model.segments.getObjectById(segId);

        if (!realSegment) {
            alert("Error retrieving segment.");
            return;
        }

        const nodeLimit = getSegmentNodeLimit(realSegment);
        if (realSegment.getGeometry().coordinates.length * 2 > nodeLimit) {
            alert(_t("alert_this_segment_has_too_many_geometry_nodes"));
            return;
        }
        
        const geometry = realSegment.getGeometry();
        const oldCoordinates = geometry.coordinates;

        if (!oldCoordinates || oldCoordinates.length < 3) {
            alert(_t("alert_this_segment_is_a_straight_line"));
            return;
        }
        
        const newCoordinates = chaikinSmooth(oldCoordinates, 1);

        console.log(`Smoothing: from ${oldCoordinates.length} points to ${newCoordinates.length} points.`);

        const cleanCoordinates = newCoordinates.map(pt => [Number(pt[0]), Number(pt[1])]);
        
        const newGeometryGeoJSON = {
            type: "LineString",
            coordinates: cleanCoordinates
        };
        
        try {
            await wmeSDK.DataModel.Segments.updateSegment({
                segmentId: segId,
                geometry: newGeometryGeoJSON
            });

            console.log("Segment smoothed successfully!");
        } catch (e) {
            console.error("Error updating segment:", e);
            alert("Error updating segment: " + e.message);
        }
    }

    function init()
    {
       if (typeof W === 'undefined' || !W.userscripts)
       {
          window.setTimeout(init, 100);
          return;
       }       

       console.log("Initialising...");

       if (W.userscripts?.state?.isReady)
       {
          createTab();
       }
       else
       {
          document.addEventListener("wme-ready", createTab, { once: true });
       }
    }

    function createHTMLTab() {
       let html = `<div style="padding: 15px; font-family: Helvetica, Arial, sans-serif;">`;       
       html += `<p style="font-size: 13px; color: #555; line-height: 1.4;">${_t("lbl_this_script_is_currently_in_beta")}</p>`;
       html += `<p style="font-size: 13px; color: #555; line-height: 1.4;">${_t("lbl_select_a_segment_to_smooth")}</p>`;
       html += `<button id="smoothSegmentButton" style="width: 100%; padding: 8px; cursor: pointer; border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 5px; display: flex; justify-content: center; align-items: center; line-height: normal;">${_t("lbl_to_smooth")}</button>`;

       html += '</div>';
       return html;
   }

    async function createTab()
    {
       let { tabLabel, tabPane } = W.userscripts.registerSidebarTab("Segment Smoothing Tool");
       tabLabel.innerText = "Segment Smoothing Tool";
       tabPane.innerHTML = createHTMLTab();

       await W.userscripts.waitForElementConnected(tabPane);

       const btn = document.getElementById("smoothSegmentButton");
       if (btn) {
           btn.addEventListener("click", smoothSegmentButtonClick);
       }
    }

    init();
})();