您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia
当前为
// ==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(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址