WME Places Name Normalizer

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

目前为 2025-03-25 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME Places Name Normalizer
  3. // @namespace https://gf.qytechs.cn/en/users/mincho77
  4. // @version 1.6.2
  5. // @description Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia
  6. // @author mincho77
  7. // @match https://www.waze.com/*editor*
  8. // @grant GM_xmlhttpRequest
  9. // @grant unsafeWindow
  10. // @license MIT
  11. // @run-at document-end
  12. // ==/UserScript==
  13. /*global W*/
  14.  
  15.  
  16. (() => {
  17. "use strict";
  18. if (!Array.prototype.flat) {
  19. Array.prototype.flat = function(depth = 1) {
  20. return this.reduce(function (flat, toFlatten) {
  21. return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten);
  22. }, []);
  23. };
  24. }
  25.  
  26. const SCRIPT_NAME = "PlacesNameNormalizer";
  27. const VERSION = "1.6.2";
  28. let placesToNormalize = [];
  29. let excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || ["EDS", "IPS", "McDonald's", "EPS"];
  30. let maxPlaces = 50;
  31. let normalizeArticles = true;
  32. // Expresión regular para detectar siglas (por ejemplo, "S.a.s", "L.T.D.A")
  33. const siglaRegex = /^[A-Za-z](\.[A-Za-z])+\.?$/;
  34.  
  35.  
  36.  
  37. function checkSpelling(text) {
  38. return new Promise((resolve, reject) => {
  39. GM_xmlhttpRequest({
  40. method: "POST",
  41. url: "https://api.languagetool.org/v2/check",
  42. data: `text=${encodeURIComponent(text)}&language=es`,
  43. headers: {
  44. "Content-Type": "application/x-www-form-urlencoded"
  45. },
  46. onload: function(response) {
  47. if (response.status === 200) {
  48. try {
  49. const data = JSON.parse(response.responseText);
  50. resolve(data);
  51. } catch (err) {
  52. reject(err);
  53. }
  54. } else {
  55. reject(`Error HTTP: ${response.status}`);
  56. }
  57. },
  58. onerror: function(err) {
  59. reject(err);
  60. }
  61. });
  62. });
  63. }
  64.  
  65. function applySpellCorrection(text) {
  66. return checkSpelling(text).then(data => {
  67. let corrected = text;
  68. // Ordenar los matches de mayor a menor offset
  69. const matches = data.matches.sort((a, b) => b.offset - a.offset);
  70. matches.forEach(match => {
  71. if (match.replacements && match.replacements.length > 0) {
  72. const replacement = match.replacements[0].value;
  73. corrected = corrected.substring(0, match.offset) + replacement + corrected.substring(match.offset + match.length);
  74. }
  75. });
  76. return corrected;
  77. });
  78. }
  79.  
  80.  
  81.  
  82.  
  83. function createSidebarTab() {
  84. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("PlacesNormalizer");
  85.  
  86. if (!tabPane) {
  87. console.error(`[${SCRIPT_NAME}] Error: No se pudo registrar el sidebar tab.`);
  88. return;
  89. }
  90.  
  91. tabLabel.innerText = "Normalizer";
  92. tabLabel.title = "Places Name Normalizer";
  93. tabPane.innerHTML = getSidebarHTML();
  94.  
  95. setTimeout(() => {
  96. // Llamar a la función para esperar el DOM antes de ejecutar eventos
  97. waitForDOM("#normalizer-tab", attachEvents);
  98. //attachEvents();
  99. }, 500);
  100. }
  101.  
  102.  
  103. function waitForDOM(selector, callback, interval = 500, maxAttempts = 10) {
  104. let attempts = 0;
  105. const checkExist = setInterval(() => {
  106. const element = document.querySelector(selector);
  107. if (element) {
  108. clearInterval(checkExist);
  109. callback(element);
  110. } else if (attempts >= maxAttempts) {
  111. clearInterval(checkExist);
  112. console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM después de ${maxAttempts} intentos.`);
  113. }
  114. attempts++;
  115. }, interval);
  116. }
  117.  
  118.  
  119. function getSidebarHTML() {
  120. return `
  121. <div id='normalizer-tab'>
  122. <h4>Places Name Normalizer <span style='font-size:11px;'>v${VERSION}</span></h4>
  123. <div>
  124. <input type="checkbox" id="normalizeArticles" ${normalizeArticles ? "checked" : ""}>
  125. <label for="normalizeArticles">No normalizar artículos (el, la, los, las)</label>
  126. </div>
  127. <div>
  128. <label>Máximo de Places a buscar: </label>
  129. <input type='number' id='maxPlacesInput' value='${maxPlaces}' min='1' max='500' style='width: 60px;'>
  130. </div>
  131. <div>
  132. <label>Palabras Excluidas:</label>
  133. <input type='text' id='excludeWord' style='width: 120px;'>
  134. <button id='addExcludeWord'>Añadir</button>
  135. <ul id='excludeList'>${excludeWords.map(word => `<li>${word}</li>`).join('')}</ul>
  136. </div>
  137. <button id='scanPlaces' class='btn btn-primary'>Escanear</button>
  138. </div>`;
  139. }
  140.  
  141.  
  142. function attachEvents() {
  143. console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`);
  144.  
  145. let normalizeArticlesCheckbox = document.getElementById("normalizeArticles");
  146. let maxPlacesInput = document.getElementById("maxPlacesInput");
  147. let addExcludeWordButton = document.getElementById("addExcludeWord");
  148. let scanPlacesButton = document.getElementById("scanPlaces");
  149.  
  150. if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) {
  151. console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`);
  152. return;
  153. }
  154.  
  155. normalizeArticlesCheckbox.addEventListener("change", (e) => {
  156. normalizeArticles = e.target.checked;
  157. });
  158.  
  159.  
  160. maxPlacesInput.addEventListener("input", (e) => {
  161. maxPlaces = parseInt(e.target.value, 10);
  162. });
  163.  
  164. addExcludeWordButton.addEventListener("click", () => {
  165. const wordInput = document.getElementById("excludeWord");
  166. const word = wordInput.value.trim();
  167. if (word && !excludeWords.includes(word)) {
  168. excludeWords.push(word);
  169. localStorage.setItem("excludeWords", JSON.stringify(excludeWords));
  170. updateExcludeList();
  171. wordInput.value = "";
  172. }
  173. });
  174.  
  175. scanPlacesButton.addEventListener("click", scanPlaces);
  176. }
  177.  
  178. function updateExcludeList() {
  179. const list = document.getElementById("excludeList");
  180. list.innerHTML = excludeWords.map(word => `<li>${word}</li>`).join('');
  181. }
  182.  
  183. function scanPlaces() {
  184. const allPlaces = W.model.venues.getObjectArray();
  185. console.log(`[${SCRIPT_NAME}] Iniciando escaneo de lugares...`);
  186.  
  187. if (!W || !W.model || !W.model.venues || !W.model.venues.objects) {
  188. console.error(`[${SCRIPT_NAME}] WME no está listo.`);
  189. return;
  190. }
  191.  
  192. // Obtener el nivel del editor; si no existe, usamos Infinity para incluir todos.
  193. let editorLevel = (W.model.user && typeof W.model.user.level === "number")
  194. ? W.model.user.level
  195. : Infinity;
  196.  
  197. let places = Object.values(W.model.venues.objects);
  198. console.log(`[${SCRIPT_NAME}] Total de lugares encontrados: ${places.length}`);
  199.  
  200. if (places.length === 0) {
  201. alert("No se encontraron Places en WME.");
  202. return;
  203. }
  204.  
  205. placesToNormalize = allPlaces
  206. .filter(place =>
  207. place &&
  208. typeof place.getID === "function" &&
  209. place.attributes &&
  210. typeof place.attributes.name === "string"
  211. )
  212. .map(place => ({
  213. id: place.getID(),
  214. name: place.attributes.name,
  215. attributes: place.attributes,
  216. place: place
  217. }));
  218.  
  219.  
  220. // Luego se mapea y se sigue con el flujo habitual...
  221. let placesMapped = placesToNormalize.map(place => {
  222. let originalName = place.attributes.name;
  223. let newName = normalizePlaceName(originalName);
  224. return {
  225. id: place.attributes.id,
  226. originalName,
  227. newName
  228. };
  229. });
  230.  
  231. let filteredPlaces = placesMapped.filter(p =>
  232. p.newName.trim() !== p.originalName.trim()
  233. );
  234.  
  235. console.log(`[${SCRIPT_NAME}] Lugares que cambiarán: ${filteredPlaces.length}`);
  236.  
  237. if (filteredPlaces.length === 0) {
  238. alert("No se encontraron Places que requieran cambio.");
  239. return;
  240. }
  241.  
  242. openFloatingPanel(filteredPlaces);
  243. }
  244.  
  245.  
  246. function NameChangeAction(venue, oldName, newName) {
  247. // Referencia al Place y los nombres
  248. this.venue = venue;
  249. this.oldName = oldName;
  250. this.newName = newName;
  251.  
  252. // ID único del Place
  253. this.venueId = venue.attributes.id;
  254.  
  255. // Metadatos que WME/Plugins pueden usar
  256. this.type = "NameChangeAction";
  257. this.isGeometryEdit = false; // no es una edición de geometría
  258. }
  259.  
  260. /**
  261. * 1) getActionName: nombre de la acción en el historial.
  262. */
  263. NameChangeAction.prototype.getActionName = function() {
  264. return "Update place name";
  265. };
  266.  
  267. /** 2) getActionText: texto corto que WME a veces muestra. */
  268. NameChangeAction.prototype.getActionText = function() {
  269. return "Update place name";
  270. };
  271.  
  272. /** 3) getName: algunas versiones llaman a getName(). */
  273. NameChangeAction.prototype.getName = function() {
  274. return "Update place name";
  275. };
  276.  
  277. /** 4) getDescription: descripción detallada de la acción. */
  278. NameChangeAction.prototype.getDescription = function() {
  279. return `Place name changed from "${this.oldName}" to "${this.newName}".`;
  280. };
  281.  
  282. /** 5) getT: título (a veces requerido por plugins). */
  283. NameChangeAction.prototype.getT = function() {
  284. return "Update place name";
  285. };
  286.  
  287. /** 6) getID: si un plugin llama a e.getID(). */
  288. NameChangeAction.prototype.getID = function() {
  289. return `NameChangeAction-${this.venueId}`;
  290. };
  291.  
  292. /** 7) doAction: asigna el nuevo nombre (WME llama a esto al crear la acción). */
  293. NameChangeAction.prototype.doAction = function() {
  294. this.venue.attributes.name = this.newName;
  295. this.venue.isDirty = true;
  296. if (typeof W.model.venues.markObjectEdited === "function") {
  297. W.model.venues.markObjectEdited(this.venue);
  298. }
  299. };
  300.  
  301. /** 8) undoAction: revertir al nombre anterior (Ctrl+Z). */
  302. NameChangeAction.prototype.undoAction = function() {
  303. this.venue.attributes.name = this.oldName;
  304. this.venue.isDirty = true;
  305. if (typeof W.model.venues.markObjectEdited === "function") {
  306. W.model.venues.markObjectEdited(this.venue);
  307. }
  308. };
  309.  
  310. /** 9) redoAction: rehacer (Ctrl+Shift+Z), vuelve a doAction. */
  311. NameChangeAction.prototype.redoAction = function() {
  312. this.doAction();
  313. };
  314.  
  315. /** 10) undoSupported / redoSupported: indica si se puede des/rehacer. */
  316. NameChangeAction.prototype.undoSupported = function() {
  317. return true;
  318. };
  319. NameChangeAction.prototype.redoSupported = function() {
  320. return true;
  321. };
  322.  
  323. /** 11) accept / supersede: evita fusionar con otras acciones. */
  324. NameChangeAction.prototype.accept = function() {
  325. return false;
  326. };
  327. NameChangeAction.prototype.supersede = function() {
  328. return false;
  329. };
  330.  
  331. /** 12) isEditAction: true => habilita "Guardar". */
  332. NameChangeAction.prototype.isEditAction = function() {
  333. return true;
  334. };
  335.  
  336. /** 13) getAffectedUniqueIds: objetos que se alteran. */
  337. NameChangeAction.prototype.getAffectedUniqueIds = function() {
  338. return [this.venueId];
  339. };
  340.  
  341. /** 14) isSerializable: si no implementas serialize(), pon false. */
  342. NameChangeAction.prototype.isSerializable = function() {
  343. return false;
  344. };
  345.  
  346. /** 15) isActionStackable: false => no combina con otras ediciones. */
  347. NameChangeAction.prototype.isActionStackable = function() {
  348. return false;
  349. };
  350.  
  351. /** 16) getFocusFeatures: WME/Plugins pueden usarlo para "enfocar" el objeto. */
  352. NameChangeAction.prototype.getFocusFeatures = function() {
  353. // Devolvemos el venue para indicar que ese es el foco (o un array vacío si prefieres).
  354. return [this.venue];
  355. };
  356.  
  357. /** 17) Métodos vacíos para evitar futuros "no es una función" si WME pide estos. */
  358. NameChangeAction.prototype.getFocusSegments = function() {
  359. return [];
  360. };
  361. NameChangeAction.prototype.getFocusNodes = function() {
  362. return [];
  363. };
  364. NameChangeAction.prototype.getFocusClosures = function() {
  365. return [];
  366. };
  367.  
  368. /** 18) getTimestamp: método nuevo que WME/Plugins están llamando. */
  369. NameChangeAction.prototype.getTimestamp = function() {
  370. // Devolvemos un timestamp numérico (ms desde época UNIX).
  371. return Date.now();
  372. };
  373.  
  374. function applyNormalization() {
  375. const checkboxes = document.querySelectorAll(".normalize-checkbox:checked");
  376. let changesMade = false;
  377.  
  378. if (checkboxes.length === 0) {
  379. console.log("ℹ️ No hay lugares seleccionados para normalizar.");
  380. return;
  381. }
  382.  
  383. checkboxes.forEach(cb => {
  384. const index = cb.dataset.index;
  385. const input = document.querySelector(`.new-name-input[data-index="${index}"]`);
  386. const newName = input?.value?.trim();
  387. const placeId = input?.getAttribute("data-place-id");
  388. const place = W.model.venues.getObjectById(placeId);
  389.  
  390. if (!place || !place.attributes?.name) {
  391. console.warn(`⛔ No se encontró el lugar con ID: ${placeId}`);
  392. return;
  393. }
  394.  
  395. const currentName = place.attributes.name.trim();
  396.  
  397. console.log(`🧠 Evaluando ID ${placeId}`);
  398. console.log(`🔹 Actual en mapa: "${currentName}"`);
  399. console.log(`🔸 Nuevo propuesto: "${newName}"`);
  400.  
  401. if (currentName !== newName) {
  402. try {
  403. // Esta es la forma correcta para obtener UpdateObject en WME
  404. const UpdateObject = require("Waze/Action/UpdateObject");
  405. const action = new UpdateObject(place, { name: newName });
  406. W.model.actionManager.add(action);
  407. console.log(`✅ Acción aplicada: "${currentName}" "${newName}"`);
  408. changesMade = true;
  409. } catch (error) {
  410. console.error("⛔ Error aplicando la acción de actualización:", error);
  411. }
  412. } else {
  413. console.log(`⏭ Sin cambios reales para ID ${placeId}`);
  414. }
  415. });
  416.  
  417. if (changesMade) {
  418. console.log("💾 Cambios marcados. Recuerda presionar el botón de guardar en el editor.");
  419. } else {
  420. console.log("ℹ️ No hubo cambios para aplicar.");
  421. }
  422. }
  423.  
  424.  
  425. function normalizePlaceName(name) {
  426. if (!name) return "";
  427.  
  428. const siglaRegex = /^[A-Z](\.[A-Z])+\.?$/; // Ej: S.A.S, L.T.D.A, C.I., etc.
  429.  
  430. const words = name.split(" ");
  431.  
  432. const normalizedWords = words.map(word => {
  433. // Detectar siglas tipo S.A.S o L.T.D.A
  434. if (siglaRegex.test(word.toUpperCase())) {
  435. return word.toUpperCase();
  436. }
  437.  
  438. // Capitalizar palabra normal (Title Case)
  439. return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
  440. });
  441.  
  442. name = normalizedWords.join(" ");
  443.  
  444. // Reemplazar pipes | por guiones con espacio
  445. name = name.replace(/\s*\|\s*/g, " - ");
  446.  
  447. // Limpiar espacios múltiples
  448. name = name.replace(/\s{2,}/g, " ").trim();
  449.  
  450. return name;
  451. }
  452.  
  453.  
  454. // ⬅️ Esta línea justo fuera de la función, no adentro
  455. //window.normalizePlaceName = normalizePlaceName;
  456. // Para exponer al contexto global real desde Tampermonkey
  457. unsafeWindow.normalizePlaceName = normalizePlaceName;
  458.  
  459.  
  460.  
  461. function openFloatingPanel(placesToNormalize) {
  462. console.log(`[${SCRIPT_NAME}] Creando panel flotante...`);
  463.  
  464. if (!placesToNormalize || placesToNormalize.length === 0) {
  465. console.warn(`[${SCRIPT_NAME}] No hay lugares para normalizar.`);
  466. return;
  467. }
  468.  
  469. // Elimina cualquier panel flotante previo
  470. let existingPanel = document.getElementById("normalizer-floating-panel");
  471. if (existingPanel) existingPanel.remove();
  472.  
  473. // Crear el panel flotante
  474. let panel = document.createElement("div");
  475. panel.id = "normalizer-floating-panel";
  476. panel.style.position = "fixed";
  477. panel.style.top = "100px";
  478. panel.style.left = "300px"; // deja espacio para la barra lateral
  479. panel.style.width = "calc(100vw - 400px)"; // margen adicional para que no se desborde
  480. panel.style.maxWidth = "calc(100vw - 30px)";
  481. panel.style.zIndex = 10000;
  482. panel.style.backgroundColor = "white";
  483. panel.style.border = "1px solid #ccc";
  484. panel.style.padding = "15px";
  485. panel.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)";
  486. panel.style.borderRadius = "8px";
  487.  
  488. // Contenido del panel
  489. let panelContent = `
  490. <h3 style="text-align: center;">Lugares para Normalizar</h3>
  491. <div style="max-height: 60vh; overflow-y: auto; margin-bottom: 10px;">
  492. <table style="width: 100%; border-collapse: collapse;">
  493. <thead>
  494. <tr style="border-bottom: 2px solid black;">
  495. <th><input type="checkbox" id="selectAllCheckbox"></th>
  496. <th style="text-align: left;">Nombre Original</th>
  497. <th style="text-align: left;">Nuevo Nombre</th>
  498. </tr>
  499. </thead>
  500. <tbody>
  501. `;
  502.  
  503. placesToNormalize.forEach((place, index) => {
  504. if (place && place.originalName) {
  505. const originalName = place.originalName;
  506. const newName = normalizePlaceName(originalName);
  507.  
  508. const placeId = place.id;
  509. panelContent += `
  510. <tr>
  511. <td><input type="checkbox" class="normalize-checkbox" data-index="${index}" checked></td>
  512. <td>${originalName}</td>
  513. <td><input type="text" class="new-name-input" data-index="${index}" data-place-id="${place.id}" value="${newName}" style="width: 100%;"></td>
  514. </tr>
  515. `;
  516. }
  517. });
  518. panelContent += `</tbody></table>`;
  519.  
  520. // Agregar botones al panel sin eventos inline
  521. // Ejemplo de la sección de botones en panelContent:
  522. panelContent += `
  523. <button id="applyNormalizationBtn" style="margin-top: 10px; width: 100%; padding: 8px; background: #4CAF50; color: white; border: none; cursor: pointer;">
  524. Aplicar Normalización
  525. </button>
  526. <button id="closeFloatingPanelBtn" style="margin-top: 5px; width: 100%; padding: 8px; background: #d9534f; color: white; border: none; cursor: pointer;">
  527. Cerrar
  528. </button>
  529. <button id="checkSpellingBtn" style="margin-top: 5px; width: 100%; padding: 8px; background: #007BFF; color: white; border: none; cursor: pointer;">
  530. Corregir Ortografía
  531. </button>
  532. `;
  533.  
  534. panel.innerHTML = panelContent;
  535. document.body.appendChild(panel);
  536.  
  537. // Evento para seleccionar todas las casillas
  538. document.getElementById("selectAllCheckbox").addEventListener("change", function() {
  539. let isChecked = this.checked;
  540. document.querySelectorAll(".normalize-checkbox").forEach(checkbox => {
  541. checkbox.checked = isChecked;
  542. });
  543. });
  544.  
  545. // Evento para cerrar el panel (CORREGIDO)
  546. document.getElementById("closeFloatingPanelBtn").addEventListener("click", function() {
  547. let panel = document.getElementById("normalizer-floating-panel");
  548. if (panel) panel.remove();
  549. });
  550. // Evento para corregir ortografía en cada input del panel:
  551. document.getElementById("checkSpellingBtn").addEventListener("click", function() {
  552. const inputs = document.querySelectorAll(".new-name-input");
  553. inputs.forEach(input => {
  554. const text = input.value;
  555. applySpellCorrection(text).then(corrected => {
  556. input.value = corrected;
  557. });
  558. });
  559. });
  560.  
  561. // Evento para aplicar normalización
  562. document.getElementById("applyNormalizationBtn").addEventListener("click", function() {
  563. let selectedPlaces = [];
  564. document.querySelectorAll(".normalize-checkbox:checked").forEach(checkbox => {
  565. let index = checkbox.getAttribute("data-index");
  566. let newName = document.querySelector(`.new-name-input[data-index="${index}"]`).value;
  567. selectedPlaces.push({ ...placesToNormalize[index], newName });
  568. });
  569.  
  570. if (selectedPlaces.length === 0) {
  571. alert("No se ha seleccionado ningún lugar.");
  572. return;
  573. }
  574.  
  575. applyNormalization(selectedPlaces);
  576. });
  577. }
  578.  
  579.  
  580. function waitForWME() {
  581. if (W && W.userscripts && W.model && W.model.venues) {
  582. console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`);
  583. createSidebarTab();
  584. } else {
  585. console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`);
  586. setTimeout(waitForWME, 1000);
  587. }
  588. }
  589. console.log(window.applyNormalization);
  590. window.applyNormalization = applyNormalization;
  591. waitForWME();
  592. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址