- // ==UserScript==
- // @name WME Places Name Normalizer
- // @namespace https://gf.qytechs.cn/en/users/mincho77
- // @version 1.6.2
- // @description Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia
- // @author mincho77
- // @match https://www.waze.com/*editor*
- // @grant GM_xmlhttpRequest
- // @grant unsafeWindow
- // @license MIT
- // @run-at document-end
- // ==/UserScript==
- /*global W*/
-
-
- (() => {
- "use strict";
- if (!Array.prototype.flat) {
- Array.prototype.flat = function(depth = 1) {
- return this.reduce(function (flat, toFlatten) {
- return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten);
- }, []);
- };
- }
-
- const SCRIPT_NAME = "PlacesNameNormalizer";
- const VERSION = "1.6.2";
- let placesToNormalize = [];
- let excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || ["EDS", "IPS", "McDonald's", "EPS"];
- let maxPlaces = 50;
- let normalizeArticles = true;
-
- // Expresión regular para detectar siglas (por ejemplo, "S.a.s", "L.T.D.A")
- const siglaRegex = /^[A-Za-z](\.[A-Za-z])+\.?$/;
-
-
-
- function checkSpelling(text) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "POST",
- url: "https://api.languagetool.org/v2/check",
- data: `text=${encodeURIComponent(text)}&language=es`,
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- onload: function(response) {
- if (response.status === 200) {
- try {
- const data = JSON.parse(response.responseText);
- resolve(data);
- } catch (err) {
- reject(err);
- }
- } else {
- reject(`Error HTTP: ${response.status}`);
- }
- },
- onerror: function(err) {
- reject(err);
- }
- });
- });
- }
-
- function applySpellCorrection(text) {
- return checkSpelling(text).then(data => {
- let corrected = text;
- // Ordenar los matches de mayor a menor offset
- const matches = data.matches.sort((a, b) => b.offset - a.offset);
- matches.forEach(match => {
- if (match.replacements && match.replacements.length > 0) {
- const replacement = match.replacements[0].value;
- corrected = corrected.substring(0, match.offset) + replacement + corrected.substring(match.offset + match.length);
- }
- });
- return corrected;
- });
- }
-
-
-
-
-
- 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" ${normalizeArticles ? "checked" : ""}>
- <label for="normalizeArticles">No normalizar artículos (el, la, los, las)</label>
- </div>
-
- <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() {
- const allPlaces = W.model.venues.getObjectArray();
- 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;
- }
-
- // Obtener el nivel del editor; si no existe, usamos Infinity para incluir todos.
- let editorLevel = (W.model.user && typeof W.model.user.level === "number")
- ? W.model.user.level
- : Infinity;
-
- 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;
- }
-
- placesToNormalize = allPlaces
- .filter(place =>
- place &&
- typeof place.getID === "function" &&
- place.attributes &&
- typeof place.attributes.name === "string"
- )
- .map(place => ({
- id: place.getID(),
- name: place.attributes.name,
- attributes: place.attributes,
- place: place
- }));
-
-
- // Luego se mapea y se sigue con el flujo habitual...
- let placesMapped = placesToNormalize.map(place => {
- let originalName = place.attributes.name;
- let newName = normalizePlaceName(originalName);
- return {
- id: place.attributes.id,
- originalName,
- newName
- };
- });
-
- 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;
- }
-
- openFloatingPanel(filteredPlaces);
- }
-
-
- 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() {
- const checkboxes = document.querySelectorAll(".normalize-checkbox:checked");
- let changesMade = false;
-
- if (checkboxes.length === 0) {
- console.log("ℹ️ No hay lugares seleccionados para normalizar.");
- return;
- }
-
- checkboxes.forEach(cb => {
- const index = cb.dataset.index;
- const input = document.querySelector(`.new-name-input[data-index="${index}"]`);
- const newName = input?.value?.trim();
- const placeId = input?.getAttribute("data-place-id");
- const place = W.model.venues.getObjectById(placeId);
-
- if (!place || !place.attributes?.name) {
- console.warn(`⛔ No se encontró el lugar con ID: ${placeId}`);
- return;
- }
-
- const currentName = place.attributes.name.trim();
-
- console.log(`🧠 Evaluando ID ${placeId}`);
- console.log(`🔹 Actual en mapa: "${currentName}"`);
- console.log(`🔸 Nuevo propuesto: "${newName}"`);
-
- if (currentName !== newName) {
- try {
- // Esta es la forma correcta para obtener UpdateObject en WME
- const UpdateObject = require("Waze/Action/UpdateObject");
- const action = new UpdateObject(place, { name: newName });
- W.model.actionManager.add(action);
- console.log(`✅ Acción aplicada: "${currentName}" → "${newName}"`);
- changesMade = true;
- } catch (error) {
- console.error("⛔ Error aplicando la acción de actualización:", error);
- }
- } else {
- console.log(`⏭ Sin cambios reales para ID ${placeId}`);
- }
- });
-
- if (changesMade) {
- console.log("💾 Cambios marcados. Recuerda presionar el botón de guardar en el editor.");
- } else {
- console.log("ℹ️ No hubo cambios para aplicar.");
- }
- }
-
-
- function normalizePlaceName(name) {
- if (!name) return "";
-
- const siglaRegex = /^[A-Z](\.[A-Z])+\.?$/; // Ej: S.A.S, L.T.D.A, C.I., etc.
-
- const words = name.split(" ");
-
- const normalizedWords = words.map(word => {
- // Detectar siglas tipo S.A.S o L.T.D.A
- if (siglaRegex.test(word.toUpperCase())) {
- return word.toUpperCase();
- }
-
- // Capitalizar palabra normal (Title Case)
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
- });
-
- name = normalizedWords.join(" ");
-
- // Reemplazar pipes | por guiones con espacio
- name = name.replace(/\s*\|\s*/g, " - ");
-
- // Limpiar espacios múltiples
- name = name.replace(/\s{2,}/g, " ").trim();
-
- return name;
- }
-
-
- // ⬅️ Esta línea justo fuera de la función, no adentro
- //window.normalizePlaceName = normalizePlaceName;
- // Para exponer al contexto global real desde Tampermonkey
- unsafeWindow.normalizePlaceName = normalizePlaceName;
-
-
-
- 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.left = "300px"; // deja espacio para la barra lateral
- panel.style.width = "calc(100vw - 400px)"; // margen adicional para que no se desborde
- panel.style.maxWidth = "calc(100vw - 30px)";
- panel.style.zIndex = 10000;
- panel.style.backgroundColor = "white";
- panel.style.border = "1px solid #ccc";
- panel.style.padding = "15px";
- panel.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)";
- panel.style.borderRadius = "8px";
-
- // Contenido del panel
- let panelContent = `
- <h3 style="text-align: center;">Lugares para Normalizar</h3>
- <div style="max-height: 60vh; overflow-y: auto; margin-bottom: 10px;">
- <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) {
- const originalName = place.originalName;
- const newName = normalizePlaceName(originalName);
-
- const placeId = place.id;
- 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}" data-place-id="${place.id}" value="${newName}" style="width: 100%;"></td>
- </tr>
- `;
- }
- });
- panelContent += `</tbody></table>`;
-
- // Agregar botones al panel sin eventos inline
- // Ejemplo de la sección de botones en panelContent:
- 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>
- <button id="checkSpellingBtn" style="margin-top: 5px; width: 100%; padding: 8px; background: #007BFF; color: white; border: none; cursor: pointer;">
- Corregir Ortografía
- </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 corregir ortografía en cada input del panel:
- document.getElementById("checkSpellingBtn").addEventListener("click", function() {
- const inputs = document.querySelectorAll(".new-name-input");
- inputs.forEach(input => {
- const text = input.value;
- applySpellCorrection(text).then(corrected => {
- input.value = corrected;
- });
- });
- });
-
- // 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();
- })();