WME Places Name Normalizer

Normaliza nombres de lugares en Waze Map Editor (WME)

当前为 2025-03-25 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WME Places Name Normalizer
// @namespace    https://greasyfork.org/en/users/mincho77
// @version      1.5
// @description  Normaliza nombres de lugares en Waze Map Editor (WME)
// @author       mincho77
// @match        https://www.waze.com/*editor*
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==
/*global W*/

(() => {
    "use strict";

    const SCRIPT_NAME = "PlacesNameNormalizer";
    const VERSION = "1.5";
    let excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || ["EDS", "IPS", "McDonald's", "EPS"];
    let maxPlaces = 50;
    let normalizeArticles = true;

    function createSidebarTab() {
        const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("PlacesNormalizer");

        if (!tabPane) {
            console.error(`[${SCRIPT_NAME}] Error: No se pudo registrar el sidebar tab.`);
            return;
        }

        tabLabel.innerText = "Normalizer";
        tabLabel.title = "Places Name Normalizer";
        tabPane.innerHTML = getSidebarHTML();

        setTimeout(() => {
            // Llamar a la función para esperar el DOM antes de ejecutar eventos
            waitForDOM("#normalizer-tab", attachEvents);
            //attachEvents();
        }, 500);
    }


    function waitForDOM(selector, callback, interval = 500, maxAttempts = 10) {
    let attempts = 0;
    const checkExist = setInterval(() => {
        const element = document.querySelector(selector);
        if (element) {
            clearInterval(checkExist);
            callback(element);
        } else if (attempts >= maxAttempts) {
            clearInterval(checkExist);
            console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM después de ${maxAttempts} intentos.`);
        }
        attempts++;
    }, interval);
}



    function getSidebarHTML() {
        return `
            <div id='normalizer-tab'>
                <h4>Places Name Normalizer <span style='font-size:11px;'>v${VERSION}</span></h4>
               <div>
<input type="checkbox" id="normalizeArticles" checked>
<label for="normalizeArticles">
    No normalizar artículos (el, la, los, las)
</label>
                <div>
                    <label>Máximo de Places a buscar: </label>
                    <input type='number' id='maxPlacesInput' value='${maxPlaces}' min='1' max='500' style='width: 60px;'>
                </div>
                <div>
                    <label>Palabras Excluidas:</label>
                    <input type='text' id='excludeWord' style='width: 120px;'>
                    <button id='addExcludeWord'>Añadir</button>
                    <ul id='excludeList'>${excludeWords.map(word => `<li>${word}</li>`).join('')}</ul>
                </div>
                <button id='scanPlaces' class='btn btn-primary'>Escanear</button>
            </div>`;
    }

    function attachEvents() {
        console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`);

        let normalizeArticlesCheckbox = document.getElementById("normalizeArticles");
        let maxPlacesInput = document.getElementById("maxPlacesInput");
        let addExcludeWordButton = document.getElementById("addExcludeWord");
        let scanPlacesButton = document.getElementById("scanPlaces");

        if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) {
            console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`);
            return;
        }

        normalizeArticlesCheckbox.addEventListener("change", (e) => {
            normalizeArticles = e.target.checked;
        });

        maxPlacesInput.addEventListener("input", (e) => {
            maxPlaces = parseInt(e.target.value, 10);
        });

        addExcludeWordButton.addEventListener("click", () => {
            const wordInput = document.getElementById("excludeWord");
            const word = wordInput.value.trim();
            if (word && !excludeWords.includes(word)) {
                excludeWords.push(word);
                localStorage.setItem("excludeWords", JSON.stringify(excludeWords));
                updateExcludeList();
                wordInput.value = "";
            }
        });

        scanPlacesButton.addEventListener("click", scanPlaces);
    }

    function updateExcludeList() {
        const list = document.getElementById("excludeList");
        list.innerHTML = excludeWords.map(word => `<li>${word}</li>`).join('');
    }



function scanPlaces() {
     console.log(`[${SCRIPT_NAME}] Iniciando escaneo de lugares...`);

    if (!W || !W.model || !W.model.venues || !W.model.venues.objects) {
        console.error(`[${SCRIPT_NAME}] WME no está listo.`);
        return;
    }

    let places = Object.values(W.model.venues.objects);
    console.log(`[${SCRIPT_NAME}] Total de lugares encontrados: ${places.length}`);

    if (places.length === 0) {
        alert("No se encontraron Places en WME.");
        return;
    }
    // 1) Filtra places con nombre y que no estén en la lista de exclusiones
    let placesToNormalize = places
    .slice(0, maxPlaces)
    .filter(place =>
        place &&
        place.attributes &&
        place.attributes.name &&
        !excludeWords.some(excluded => place.attributes.name.includes(excluded))
    );

    // 2) Mapea a un array con originalName y newName
    let placesMapped = placesToNormalize.map(place => {
        let originalName = place.attributes.name;
        let newName = normalizePlaceName(originalName);
        return {
            id: place.attributes.id,
            originalName,
            newName
        };
    });

    // 3) Filtra solo aquellos que sí van a cambiar
    let filteredPlaces = placesMapped.filter(p =>
        p.newName.trim() !== p.originalName.trim()
    );

    console.log(`[${SCRIPT_NAME}] Lugares que cambiarán: ${filteredPlaces.length}`);

    if (filteredPlaces.length === 0) {
        alert("No se encontraron Places que requieran cambio.");
        return;
    }

    // 4) Llama al panel flotante con los que realmente cambiarán
    openFloatingPanel(filteredPlaces);
    /*openFloatingPanel(placesToNormalize.map(place => ({
        id: place.attributes.id,
        originalName: place.attributes.name,
        newName: normalizePlaceName(place.attributes.name)
    })));*/
}

function NameChangeAction(venue, oldName, newName) {
    // Referencia al Place y los nombres
    this.venue = venue;
    this.oldName = oldName;
    this.newName = newName;

    // ID único del Place
    this.venueId = venue.attributes.id;

    // Metadatos que WME/Plugins pueden usar
    this.type = "NameChangeAction";
    this.isGeometryEdit = false; // no es una edición de geometría
}

/**
 * 1) getActionName: nombre de la acción en el historial.
 */
NameChangeAction.prototype.getActionName = function() {
    return "Update place name";
};

/** 2) getActionText: texto corto que WME a veces muestra. */
NameChangeAction.prototype.getActionText = function() {
    return "Update place name";
};

/** 3) getName: algunas versiones llaman a getName(). */
NameChangeAction.prototype.getName = function() {
    return "Update place name";
};

/** 4) getDescription: descripción detallada de la acción. */
NameChangeAction.prototype.getDescription = function() {
    return `Place name changed from "${this.oldName}" to "${this.newName}".`;
};

/** 5) getT: título (a veces requerido por plugins). */
NameChangeAction.prototype.getT = function() {
    return "Update place name";
};

/** 6) getID: si un plugin llama a e.getID(). */
NameChangeAction.prototype.getID = function() {
    return `NameChangeAction-${this.venueId}`;
};

/** 7) doAction: asigna el nuevo nombre (WME llama a esto al crear la acción). */
NameChangeAction.prototype.doAction = function() {
    this.venue.attributes.name = this.newName;
    this.venue.isDirty = true;
    if (typeof W.model.venues.markObjectEdited === "function") {
        W.model.venues.markObjectEdited(this.venue);
    }
};

/** 8) undoAction: revertir al nombre anterior (Ctrl+Z). */
NameChangeAction.prototype.undoAction = function() {
    this.venue.attributes.name = this.oldName;
    this.venue.isDirty = true;
    if (typeof W.model.venues.markObjectEdited === "function") {
        W.model.venues.markObjectEdited(this.venue);
    }
};

/** 9) redoAction: rehacer (Ctrl+Shift+Z), vuelve a doAction. */
NameChangeAction.prototype.redoAction = function() {
    this.doAction();
};

/** 10) undoSupported / redoSupported: indica si se puede des/rehacer. */
NameChangeAction.prototype.undoSupported = function() {
    return true;
};
NameChangeAction.prototype.redoSupported = function() {
    return true;
};

/** 11) accept / supersede: evita fusionar con otras acciones. */
NameChangeAction.prototype.accept = function() {
    return false;
};
NameChangeAction.prototype.supersede = function() {
    return false;
};

/** 12) isEditAction: true => habilita "Guardar". */
NameChangeAction.prototype.isEditAction = function() {
    return true;
};

/** 13) getAffectedUniqueIds: objetos que se alteran. */
NameChangeAction.prototype.getAffectedUniqueIds = function() {
    return [this.venueId];
};

/** 14) isSerializable: si no implementas serialize(), pon false. */
NameChangeAction.prototype.isSerializable = function() {
    return false;
};

/** 15) isActionStackable: false => no combina con otras ediciones. */
NameChangeAction.prototype.isActionStackable = function() {
    return false;
};

/** 16) getFocusFeatures: WME/Plugins pueden usarlo para "enfocar" el objeto. */
NameChangeAction.prototype.getFocusFeatures = function() {
    // Devolvemos el venue para indicar que ese es el foco (o un array vacío si prefieres).
    return [this.venue];
};

/** 17) Métodos vacíos para evitar futuros "no es una función" si WME pide estos. */
NameChangeAction.prototype.getFocusSegments = function() {
    return [];
};
NameChangeAction.prototype.getFocusNodes = function() {
    return [];
};
NameChangeAction.prototype.getFocusClosures = function() {
    return [];
};

/** 18) getTimestamp: método nuevo que WME/Plugins están llamando. */
NameChangeAction.prototype.getTimestamp = function() {
    // Devolvemos un timestamp numérico (ms desde época UNIX).
    return Date.now();
};

function applyNormalization(selectedPlaces) {
    if (!selectedPlaces || selectedPlaces.length === 0) {
        alert("No se ha seleccionado ningún lugar.");
        return;
    }

    let changesMade = false;

    selectedPlaces.forEach((place) => {
        const wazePlace = W.model.venues.getObjectById(place.id);
        if (!wazePlace) return;

        const oldName = (wazePlace.attributes.name || "").trim();
        const newName = place.newName.trim();

        if (oldName !== newName) {
            const action = new NameChangeAction(wazePlace, oldName, newName);
            W.model.actionManager.add(action);
            console.log(`Action creada: "${oldName}" → "${newName}" (ID: ${wazePlace.attributes.id})`);
            changesMade = true;
        }
    });

    if (changesMade) {
        alert("Normalización aplicada. Revisa y guarda los cambios en WME.");
    } else {
        alert("No se detectaron cambios en los nombres seleccionados.");
    }
}
function normalizePlaceName(name) {
    if (!name) return "";

    const articles = ["el", "la", "los", "las", "del","de", "y"];
    const exclusions = ["IPS", "EDS", "McDonald's"];

    let words = name.split(" ");

    let normalizedWords = words.map((word) => {
        // Si la palabra está en la lista de exclusiones, se deja igual.
        if (exclusions.includes(word)) return word;


            // Si la primera palabra es un artículo
     // Si la palabra es un artículo
        if (articles.includes(word.toLowerCase())) {
            // Checkbox marcado => NO normalizar => devolvemos tal cual esté escrito
            if (normalizeArticles) {
                return word;
            } else {
                // Checkbox desmarcado => normalizar => capitalizar
                return word[0].toUpperCase() + word.substring(1).toLowerCase();
            }
        }

        // Para el resto de las palabras, se normalizan (primera letra mayúscula).
        return word[0].toUpperCase() + word.substring(1).toLowerCase();
    });

    return normalizedWords.join(" ");
}

 function openFloatingPanel(placesToNormalize) {
    console.log(`[${SCRIPT_NAME}] Creando panel flotante...`);

    if (!placesToNormalize || placesToNormalize.length === 0) {
        console.warn(`[${SCRIPT_NAME}] No hay lugares para normalizar.`);
        return;
    }

    // Elimina cualquier panel flotante previo
    let existingPanel = document.getElementById("normalizer-floating-panel");
    if (existingPanel) existingPanel.remove();

    // Crear el panel flotante
    let panel = document.createElement("div");
    panel.id = "normalizer-floating-panel";
    panel.style.position = "fixed";
    panel.style.top = "100px";
    panel.style.right = "20px";
    panel.style.width = "420px";
    panel.style.background = "white";
    panel.style.border = "1px solid black";
    panel.style.padding = "10px";
    panel.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.5)";
    panel.style.zIndex = "10000";
    panel.style.overflowY = "auto";
    panel.style.maxHeight = "500px";

    // Contenido del panel
    let panelContent = `
        <h3 style="text-align: center;">Lugares para Normalizar</h3>
        <table style="width: 100%; border-collapse: collapse;">
            <thead>
                <tr style="border-bottom: 2px solid black;">
                    <th><input type="checkbox" id="selectAllCheckbox"></th>
                    <th style="text-align: left;">Nombre Original</th>
                    <th style="text-align: left;">Nuevo Nombre</th>
                </tr>
            </thead>
            <tbody>
    `;

    placesToNormalize.forEach((place, index) => {
        if (place && place.originalName) {
            let originalName = place.originalName;
            let newName = place.newName;

            panelContent += `
                <tr>
                    <td><input type="checkbox" class="normalize-checkbox" data-index="${index}" checked></td>
                    <td>${originalName}</td>
                    <td><input type="text" class="new-name-input" data-index="${index}" value="${newName}" style="width: 100%;"></td>
                </tr>
            `;
        }
    });

    panelContent += `</tbody></table>`;

    // Agregar botones al panel sin eventos inline
    panelContent += `
        <button id="applyNormalizationBtn" style="margin-top: 10px; width: 100%; padding: 8px; background: #4CAF50; color: white; border: none; cursor: pointer;">
            Aplicar Normalización
        </button>
        <button id="closeFloatingPanelBtn" style="margin-top: 5px; width: 100%; padding: 8px; background: #d9534f; color: white; border: none; cursor: pointer;">
            Cerrar
        </button>
    `;

    panel.innerHTML = panelContent;
    document.body.appendChild(panel);

    // Evento para seleccionar todas las casillas
    document.getElementById("selectAllCheckbox").addEventListener("change", function() {
        let isChecked = this.checked;
        document.querySelectorAll(".normalize-checkbox").forEach(checkbox => {
            checkbox.checked = isChecked;
        });
    });

    // Evento para cerrar el panel (CORREGIDO)
    document.getElementById("closeFloatingPanelBtn").addEventListener("click", function() {
        let panel = document.getElementById("normalizer-floating-panel");
        if (panel) panel.remove();
    });

    // Evento para aplicar normalización
    document.getElementById("applyNormalizationBtn").addEventListener("click", function() {
        let selectedPlaces = [];
        document.querySelectorAll(".normalize-checkbox:checked").forEach(checkbox => {
            let index = checkbox.getAttribute("data-index");
            let newName = document.querySelector(`.new-name-input[data-index="${index}"]`).value;
            selectedPlaces.push({ ...placesToNormalize[index], newName });
        });

        if (selectedPlaces.length === 0) {
            alert("No se ha seleccionado ningún lugar.");
            return;
        }

        applyNormalization(selectedPlaces);
    });
}



    function waitForWME() {
        if (W && W.userscripts && W.model && W.model.venues) {
            console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`);
            createSidebarTab();
        } else {
            console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`);
            setTimeout(waitForWME, 1000);
        }
    }
    console.log(window.applyNormalization);
window.applyNormalization = applyNormalization;
    waitForWME();
})();