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();
})();