Greasy Fork 还支持 简体中文。

WME Places Name Normalizer

Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia

目前為 2025-03-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WME Places Name Normalizer
// @namespace    https://greasyfork.org/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();
})();