// ==UserScript==
// @name WME Places Name Normalizer
// @namespace https://gf.qytechs.cn/en/users/mincho77
// @author Mincho77
// @version 8.3.0
// @license MIT
// @description Normaliza nombres de lugares y gestiona categorías dinámicamente en WME.
// @match https://www.waze.com/editor*
// @match https://www.waze.com/*/editor*
// @include https://beta.waze.com/*
// @include https://www.waze.com/editor*
// @include https://www.waze.com/*/editor*
// @exclude https://www.waze.com/user/editor*
// @grant GM_xmlhttpRequest
// @connect sheets.googleapis.com
// @run-at document-end
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// ==/UserScript==
// Se establecen las variables de control con valores iniciales claros
let isProcessingActive = false;
let isNormalizationActive = true;
let isResultsPanelOpen = false;
let activeScanning = false; // Bandera para indicar si se encuentra en curso un escaneo
window.__PLN_DECISION_DEBUG_ON = false; // Se desactivan los logs de depuración detallados
// Función testSwapAndCap: Se configuran las palabras de swap para pruebas si no existen.
function testSwapAndCap(name) {
// Se verifica si existen palabras para intercambio; en caso contrario se inicializan
if (!window.swapWords || !window.swapWords.length) {
window.swapWords = [
{ word: "Urbanización", direction: "start" },
{ word: "Barrio", direction: "start" },
{ word: "Residencial", direction: "end" }
];
}
if (typeof applySwapMovement !== 'function') {
console.error("[TEST-SWAP] Error: applySwapMovement no está definida. Se requiere ejecutar el script primero.");
return name;
}
const result = applySwapMovement(name);
console.log(`[TEST-SWAP] Input: "${name}" | Output: "${result}"`);
return result;
}
// Se dispone la función testSwapAndCap de forma global
window.testSwapAndCap = testSwapAndCap;
(function ()
{
//window.normalizeWordInternal = normalizeWordInternal;
// Variables globales básicas
const SCRIPT_NAME = GM_info.script.name;
const VERSION = GM_info.script.version.toString();
// Variable global para el ícono de la pestaña principal
const MAIN_TAB_ICON_BASE64 = "data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q=="
let blinkingPlaces = new Set();
let blinkState = true;
let lastBlinkTime = 0;
let pendingRequests = [];
const BLINK_INTERVAL = 500; // Tiempo en milisegundos entre cada titilación (0.5 segundos)
// === Debug helpers for City Apply ===
const PLN_DEBUG = true; // poner false para silenciar
function plnLog(...args){ if (PLN_DEBUG) console.log('[WME PLN][CityApply]', ...args); }
function plnWarn(...args){ if (PLN_DEBUG) console.warn('[WME PLN][CityApply]', ...args); }
function plnErr(...args)
{
console.error('[WME PLN][CityApply]', ...args);
}
// Log de guardado global del WME
window.addEventListener('wme-save-finished', (ev)=>{
plnLog('wme-save-finished', ev && ev.detail);
});
// Variables globales para el diccionario de palabras excluidas
//Permite inicializar el diccionario de palabras intercambiadas
if (!window.swapWords)
{
const stored = localStorage.getItem("wme_swapWords");
window.swapWords = stored ? JSON.parse(stored) : [];
}
// === Swap Debug Helpers ===
(function plnSwapDebugInit()
{
try{
if (window.plnSwapDebug && window.plnSwapExplain) return;
window.__PLN_SWAP_DEBUG_ON = window.__PLN_SWAP_DEBUG_ON || false;
window.plnSwapDebug = function(on){
window.__PLN_SWAP_DEBUG_ON = !!on;
try{ localStorage.setItem('wme_pln_debug_swap', on ? '1' : '0'); }catch(_){ }
//console.log('[PLN Swap] debug', on ? 'ON' : 'OFF');
};
try { if (typeof unsafeWindow !== 'undefined') unsafeWindow.plnSwapDebug = window.plnSwapDebug; } catch(_) {}
window.plnSwapExplain = function(name){
const prev = window.__PLN_SWAP_DEBUG_ON;
window.__PLN_SWAP_DEBUG_ON = true;
console.group('[PLN Swap] TRACE for', name);
const out = applySwapRules(name);
//console.log('TRACE result =>', out);
console.groupEnd();
window.__PLN_SWAP_DEBUG_ON = prev;
return out;
};
try { if (typeof unsafeWindow !== 'undefined') unsafeWindow.plnSwapExplain = window.plnSwapExplain; } catch(_) {}
}catch(_){ /* noop */ }
})();
// === Decision Debug Helpers (pipeline end‑to‑end) ===
(function plnDecisionDebugInit(){
try{
if (window.plnDecisionDebug && window.plnExplainDecision) return;
window.__PLN_DECISION_DEBUG_ON = window.__PLN_DECISION_DEBUG_ON || false;
function noDiacritics(s){
return String(s||'')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g,'')
.replace(/\s+/g,' ')
.trim()
.toLowerCase();
}
window.plnDecisionDebug = function(on){
window.__PLN_DECISION_DEBUG_ON = !!on;
try{ localStorage.setItem('wme_pln_debug_decision', on ? '1':'0'); }catch(_){ }
//console.log('[PLN Decision] debug', on ? 'ON' : 'OFF');
};
try { if (typeof unsafeWindow !== 'undefined') unsafeWindow.plnDecisionDebug = window.plnDecisionDebug; } catch(_){ }
window.plnExplainDecision = function(name){
const origFn = (typeof window.__PLNNormalizeOriginal === 'function') ? window.__PLNNormalizeOriginal
: (typeof window.__plnNormalizeOriginal === 'function') ? window.__plnNormalizeOriginal
: (typeof window.normalizePlaceName === 'function' ? window.normalizePlaceName : null);
const input = String(name||'');
const base = origFn ? origFn(input) : input;
const baseCap = plnPostSwapCap(base);
const afterEx = plnApplyExclusions(baseCap);
const afterSw = applySwapRules(afterEx);
const renorm = origFn ? origFn(afterSw) : afterSw;
const finalCap= plnPostSwapCap(renorm);
const final = plnApplyExclusions(finalCap);
const equalExact = input === final;
const noDia = s => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
const equalNoCaseNoSpacesNoDiacritics = noDia(input) === noDia(final);
const out = { original: input, baseNormalized: base, baseCap, afterExclusions: afterEx,
afterSwap: afterSw, renormalized: renorm, finalSuggested: final,
equalExact, equalNoCaseNoSpacesNoDiacritics };
console.group('[PLN Decision] explain', name);
console.table(out);
console.groupEnd();
return out;
};
try { if (typeof unsafeWindow !== 'undefined') unsafeWindow.plnExplainDecision = window.plnExplainDecision; } catch(_){ }
}catch(_){ /* noop */ }
})();
// === Swap Engine: move configured tokens before/after ===
// Respeta el arreglo global window.swapWords guardado en localStorage.
// Cada item puede definir la palabra en `word` | `text` | `token`
// y la dirección en `position` | `where` | `dir` | `direction` con valores
// "before" | "after" o "antes" | "despues/después".
function applySwapRules(originalName)
{
try {
const DBG = false; // Cambia esto de "!!(window.__PLN_SWAP_DEBUG_ON || localStorage...)" a simplemente false
let name = String(originalName || '');
const swaps = (typeof plnCollectSwapRules==='function')
? plnCollectSwapRules()
: (Array.isArray(window.swapWords) ? window.swapWords : []);
//if (DBG) console.group('[PLN Swap] applySwapRules', { originalName, swapsCount: Array.isArray(swaps)?swaps.length:0 });
if (!swaps.length){ if (DBG){ console.warn('[PLN Swap] skip: no swaps configured'); console.groupEnd?.(); } return name; }
const normalizeSpace = s => s.replace(/\s+/g,' ').replace(/\s*-\s*/g,' - ').trim();
for (const raw of swaps) {
if (!raw){ if (DBG) console.warn('skip: null item'); continue; }
const token = String((raw.word || raw.text || raw.token || '').trim());
if (!token){ if (DBG) console.warn('skip: empty token', raw); continue; }
let where = String((raw.position || raw.where || raw.dir || raw.direction || '')).toLowerCase();
if (where === 'antes' || where === 'before' || where === 'pre') where = 'before';
if (where === 'despues' || where === 'después' || where === 'after' || where === 'post') where = 'after';
if (where !== 'before' && where !== 'after'){ if (DBG) console.warn(`skip [${token}]: invalid position`, raw); continue; }
const esc = token.replace(/[.*+?^${}()|[\]\\]/g,'\\$&').replace(/\s+/g,'\\s+');
const SEP = '[\\s,.;:()\\[\\]\\-–—\/]';
// RegEx para buscar (sin 'g') y para reemplazar (con 'g')
const reFind = new RegExp(`(?:^|${SEP})${esc}(?=$|${SEP})`, 'iu');
const reAnywhere = new RegExp(`(?:^|${SEP})${esc}(?=$|${SEP})`, 'giu');
const reStart = new RegExp(`^\\s*${esc}(?=$|${SEP})`, 'iu');
const reEnd = new RegExp(`(?:^|${SEP})${esc}\\s*$`, 'iu');
if (DBG) console.groupCollapsed(`[${token}] → ${where}`);
if (!reFind.test(name)) {
if (DBG){ console.info('no-op: token not present in name', { name, token }); console.groupEnd?.(); }
continue;
}
if ((where === 'before' && reStart.test(name)) || (where === 'after' && reEnd.test(name))) {
if (DBG){ console.info('no-op: already at target edge', { name }); console.groupEnd?.(); }
name = normalizeSpace(name);
continue;
}
const before = name;
// Eliminar apariciones previas del token como palabra independiente
name = name.replace(reAnywhere, ' ').replace(/\s{2,}/g, ' ').trim();
// Colocar en el borde solicitado y normalizar espacios
name = where === 'before' ? `${token} ${name}`.trim() : `${name} ${token}`.trim();
name = normalizeSpace(name);
// if (DBG) console.log('moved', { before, after: name });
if (DBG) console.groupEnd?.();
}
if (DBG) { console.log('result =>', name); console.groupEnd?.(); }
return name;
}
catch (e)
{
//if (window.__PLN_SWAP_DEBUG_ON) console.error('[PLN Swap] error', e);
return originalName;
}
}
// --- Post-swap capitalization helpers ---
function plnCapitalizeStart(str)
{
try { return String(str||'').replace(/^\s*([a-záéíóúñ])/iu,(m,c)=>m.replace(c,c.toUpperCase())); } catch { return str; }
}
// Capitaliza la primera letra después de " - "
function plnCapitalizeAfterHyphen(str){
try{
// soporta múltiples ocurrencias; respeta espacios normalizados " - "
return String(str||'').replace(/(\s-\s*)([a-záéíóúñ])/giu, (m, sep, ch) => sep + ch.toUpperCase());
}catch(_){ return String(str||''); }
}
// Title-case en español: stopwords en minúscula salvo si van al inicio; respeta siglas
// REEMPLAZA o AÑADE esta función en tu script
function plnTitleCaseEs(str)
{
try
{
const STOP = new Set(['de', 'del', 'la', 'las', 'el', 'los', 'y', 'e', 'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en', 'con', 'tras', 'por', 'al', 'lo']);
const isAllCaps = w => w.length > 1 && w === w.toUpperCase();
const cap = w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
let i = 0;
return String(str || '').replace(/([\p{L}\p{M}][\p{L}\p{M}\.'’]*)/gu, (m) => {
const w = m, lw = w.toLowerCase(), atStart = (i === 0);
i += w.length;
const excl = (typeof isExcludedWord === 'function') ? isExcludedWord(w) : null;
if (excl) return excl;
if (isAllCaps(w)) return w;
if (STOP.has(lw) && !atStart) {
// ✅ **LA MEJORA ESTÁ AQUÍ** ✅
// Si la palabra original ya estaba en mayúscula (ej. "La"),
// la respetamos y no la convertimos a minúscula.
if (w.charAt(0) === w.charAt(0).toUpperCase()) {
return w;
}
return lw; // De lo contrario, sí la convertimos a minúscula.
}
return cap(w);
});
}
catch
{
return str;
}
}//plnTitleCaseEs
function plnPostSwapCap(str)
{
let out = String(str||'');
out = plnTitleCaseEs(out);
out = plnCapitalizeStart(out);
out = plnCapitalizeAfterHyphen(out); // <-- asegura mayúscula tras " - "
return out.trim();
}
// === Ocultar filas ya normalizadas / sin acción requerida ===
// ✅ REEMPLAZA EL BLOQUE ANTERIOR CON ESTE ✅
function plnPruneNormalizedRowsManager() {
try {
if (window.__plnPruneRowsActive) return;
window.__plnPruneRowsActive = true;
const HIDE_CLASS = 'pln-hidden-normalized';
if (!document.getElementById('pln-hide-style')) {
const st = document.createElement('style');
st.id = 'pln-hide-style';
st.textContent = `tr.${HIDE_CLASS}{display:none !important;}`;
document.head.appendChild(st);
}
function expectedOf(s) {
try {
const orig = (typeof window.__plnNormalizeOriginal === 'function') ?
window.__plnNormalizeOriginal :
(typeof window.normalizePlaceName === 'function' ? window.normalizePlaceName : null);
let out = String(s || '');
if (orig) out = orig(out);
out = plnPostSwapCap(out);
out = plnApplyExclusions(out);
out = applySwapRules(out);
if (orig) out = orig(out);
out = plnPostSwapCap(out);
out = plnApplyExclusions(out);
return out.trim();
} catch (_) {
return String(s || '').trim();
}
}
function processRow(tr) {
if (!tr || tr.nodeType !== 1) return;
const tas = tr.querySelectorAll('textarea');
if (!tas || tas.length === 0) return;
const current = (tas[0].value || '').trim();
const suggested = (tas.length > 1 ? tas[1].value : '').trim();
if (!current) return;
const expected = expectedOf(current);
const actionDisabled = !!tr.querySelector('button[disabled], input[disabled], .disabled, [aria-disabled="true"]');
const noChange = (suggested && suggested === current) || (current === expected) || actionDisabled;
if (noChange) tr.classList.add(HIDE_CLASS);
else tr.classList.remove(HIDE_CLASS);
}
function processAll() {
document.querySelectorAll('tr').forEach(processRow);
}
// Exportamos el observador y la función para usarlos externamente
let debounceTimer;
window.plnPruneObserver = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(processAll, 250); // Mantenemos el debounce
});
window.plnPruneProcessAll = processAll;
// YA NO SE OBSERVA document.body AQUÍ
setTimeout(processAll, 300);
let ticks = 0;
const iv = setInterval(() => {
processAll();
if (++ticks > 10) clearInterval(iv);
}, 400);
window.__plnHideNormalizedRows = processAll;
} catch (_) { /* noop */ }
}
// Llamamos a la función para que prepare las herramientas
plnPruneNormalizedRowsManager();
// Exponer para pruebas rápidas en consola
// window.__pln_applySwapRulesTest = applySwapRules;
(function plnAutoSwapSuggestionInputs(){
try {
document.addEventListener('input', (ev)=>{
const t = ev.target;
if (t && t.matches && t.matches('textarea.replacement-input')) {
t.dataset.userEdited = '1'; // no pisar ediciones manuales
}
}, true);
function applyOnceTo(el)
{
if (!el || el.dataset.userEdited === '1') return;
const tr = el.closest('tr[data-place-id]') || el.closest('tr');
let currentName = '';
if (tr)
{
const tas = tr.querySelectorAll('textarea');
if (tas && tas.length) currentName = tas[0].value || '';
}
// Usa el normalizador oficial ya parcheado con swap
const norm = (typeof normalizePlaceName === 'function')
? normalizePlaceName
: (typeof window.__plnNormalizeOriginal === 'function' ? window.__plnNormalizeOriginal : null);
const expected = norm ? norm(currentName) : currentName;
if (expected && el.value !== expected) el.value = expected;
}
let debounceTimer;
const debouncedHandler = () => {
// Esta función se asegura de que solo se procesen los textareas una vez.
document.querySelectorAll('textarea.replacement-input:not([data-pln-processed])').forEach(el => {
applyOnceTo(el);
el.setAttribute('data-pln-processed', 'true'); // Marcar como procesado
});
};
const obs = new MutationObserver(muts => {
for (const m of muts) {
if (m.addedNodes.length > 0) {
clearTimeout(debounceTimer);
// Agrupa todas las llamadas en una sola ejecución 100ms después del último cambio.
debounceTimer = setTimeout(debouncedHandler, 100);
break; // Un solo nodo añadido es suficiente para activar el debounce.
}
}
});
obs.observe(document.body, { childList: true, subtree: true });
window.plnApplySwapToSuggestionInputs = function(){
document.querySelectorAll('textarea.replacement-input').forEach(applyOnceTo);
};
} catch(_) {}
})();
// Hook: encadena las reglas de swap al resultado de normalizePlaceName
(function plnPatchNormalizeForSwap(){
try {
if (window.__plnSwapPatched) return;
let tries = 0;
const iv = setInterval(() => {
tries++;
const fn = (typeof normalizePlaceName === 'function') ? normalizePlaceName : (typeof window !== 'undefined' && typeof window.normalizePlaceName === 'function' ? window.normalizePlaceName : null);
if (fn) {
clearInterval(iv);
const original = fn;
// Exponer el normalizador original para usarlo post‑swap sin recursión
try { window.__plnNormalizeOriginal = original; } catch(_) {}
// ✅ CÓDIGO CORRECTO DE REEMPLAZO
const patched = function (...args) {
const inStr = args && args.length ? String(args[0] || '') : '';
// 1. Usar SIEMPRE el motor de normalización principal y más completo del script.
// 'processPlaceName' ya maneja la capitalización, reglas especiales, y reemplazos.
const baseNormalizada = processPlaceName(inStr);
// 2. Aplicar las reglas de 'swap' (mover palabras) al resultado ya normalizado.
const swapped = applySwapRules(baseNormalizada);
// 3. Aplicar una capitalización final y limpieza post-swap.
const finalCap = plnPostSwapCap(swapped);
const finalStr = plnApplyExclusions(finalCap); // Reponer palabras excluidas al final.
// (Opcional) Bloque de depuración para trazar el flujo
try {
const DBG = window.__PLN_DECISION_DEBUG_ON || localStorage.getItem('wme_pln_debug_decision') === '1';
if (DBG) {
console.group('[PLN Decision] normalizePlaceName patched (CORREGIDO)');
console.table({
"Input": inStr,
"1. After processPlaceName": baseNormalizada,
"2. After applySwapRules": swapped,
"3. Final Result": finalStr
});
console.groupEnd();
}
} catch (_) {}
// 4. Devolver el resultado final y unificado.
return finalStr.trim();
}; // Colgar en ambos scopes por seguridad
try { window.normalizePlaceName = patched; } catch(_) {}
try { normalizePlaceName = patched; } catch(_) {}
window.__plnSwapPatched = true;
//console.log('[WME PLN] Swap rules hooked into normalizePlaceName.');
}
if (tries > 60) { clearInterval(iv); }
}, 200);
} catch(_) { /* noop */ }
})();
// Variables globales para el panel flotante
let floatingPanelElement = null;
let dynamicCategoriesLoaded = false;
const tempSelectedCategories = new Map(); // Mapa para placeId -> categoryKey seleccionada
const placesForDuplicateCheckGlobal = []; // Nueva variable global para almacenar datos de lugares para verificar duplicados
const processingPanelDimensions = { width: '400px', height: '200px' }; // Panel pequeño para procesamiento
const resultsPanelDimensions = { width: '1400px', height: '700px' }; // Panel grande para resultados
const commonWords = [//Palabras comunes en español que no deberían ser consideradas para normalización
'es', 'de', 'del', 'el', 'la', 'los', 'las', 'y', 'e',
'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en',
'con', 'tras', 'por', 'al', 'lo'
];
const tabNames = [//Definir nombres de pestañas cortos antes de la generación de botones
{ label: "Gene", icon: "⚙️" },
{ label: "Espe", icon: "🏷️" },
{ label: "Dicc", icon: "📘" },
{ label: "Reemp", icon: "🔂" }
]; let statsPanelElement = null; // Para el panel flotante de estadísticas
let editorStats = {}; // Para almacenar las estadísticas en memoria
const STATS_STORAGE_KEY = 'wme_pln_editor_stats'; // Clave para localStorage
const STATS_ENABLED_KEY = 'wme_pln_stats_enabled'; // Clave para el checkbox de visibilidad
// Cargar estadísticas inmediatamente al iniciar el script
loadEditorStats();
let wmeSDK = null; // Almacena la instancia del SDK de WME.
//Variable global para almacenar la información del usuario actual
let currentGlobalUserInfo = { id: null, name: 'Cargando...', privilege: 'N/A' };
//Novedades de cada version del script esto permitirá una pantalla la primera vez que se abra el script
const myChangelog = {
[VERSION]: {
"Novedades": [
// De la rama dev
"Reestructuración completa para mover palabras al inicio o al final, con UI para editar y borrar.",
"Obtiene la lista de palabras del diccionario desde Google Sheets hasta con 62000 palabras nuevas.",
"Las palabras de reemplazo e intercambio funcionan bajo las nuevas reglas de la WazeOpedia.",
"Los Lugares Excluidos ahora se pueden exportar e importar desde un archivo",
"Los lugares excluidos los descarta al buscar en pantalla",
],
"Correcciones": [
// Ajustes en capitalización / normalización
"Mejor inserción de guiones en reemplazos contextuales.",
"Mejoramos la exclusión de lugares: calcula los visuales, descuenta los excluidos y solo analiza los lugares reales.",
"Aplicar ciudad funciona de forma más confiable ahora se calcula basada en el segmento más cercano al place"
]
}
}; // myChangelog
// Modified GM_xmlhttpRequest wrapper that tracks requests
function makeRequest(options) {
const requestId = Date.now() + Math.random();
const wrappedOptions = {
...options,
onload: function(response) {
// Remove this request from pending list
pendingRequests = pendingRequests.filter(id => id !== requestId);
if (typeof options.onload === 'function') {
options.onload(response);
}
},
onerror: function(error) {
// Remove this request from pending list
pendingRequests = pendingRequests.filter(id => id !== requestId);
if (typeof options.onerror === 'function') {
options.onerror(error);
}
}
};
// Add to pending requests
pendingRequests.push(requestId);
// Make the actual request
return GM_xmlhttpRequest(wrappedOptions);
}
// Variables globales para el diccionario de lugares excluidos
function loadExcludedPlacesFromStorage()
{
try {
const savedExcludedPlaces = localStorage.getItem("excludedPlacesList");
if (savedExcludedPlaces) {
const parsedData = JSON.parse(savedExcludedPlaces);
excludedPlaces = new Map(parsedData);
console.log("[WME PLN] Lugares excluidos cargados:", excludedPlaces.size);
} else {
excludedPlaces = new Map();
console.log("[WME PLN] No se encontraron lugares excluidos guardados.");
}
} catch (e) {
console.error("[WME PLN] Error cargando lugares excluidos desde localStorage:", e);
excludedPlaces = new Map();
}
}//loadExcludedPlacesFromStorage
// [PLN] Cerca eléctrica: impedir que reasignaciones borren pares del usuario
(function(){
try{
// Backing stores
let __plnRW = window.replacementWords || {};
let __plnRS = (typeof window.replacementSources==='object' && window.replacementSources) ? window.replacementSources : {};
// Asegurar objeto de fuentes existente
if (!window.replacementSources || typeof window.replacementSources!=='object') window.replacementSources = (__plnRS = {});
Object.defineProperty(window, 'replacementWords', {
configurable: true,
enumerable: true,
get(){ return __plnRW; },
set(next){
try{
const prevMap = __plnRW || {};
const prevSrc = __plnRS || {};
const incoming = (next && typeof next==='object') ? { ...next } : {};
// Reinyectar SOLO entradas previas cuyo source != 'sheet' y que no estén en el nuevo mapa
for (const k in prevMap){
if (prevSrc[k] !== 'sheet' && !(k in incoming)){
incoming[k] = prevMap[k];
if (!window.replacementSources || typeof window.replacementSources!=='object') window.replacementSources = (__plnRS = {});
if (window.replacementSources[k] !== 'sheet') window.replacementSources[k] = 'user';
}
}
__plnRW = incoming;
}catch(e){ __plnRW = next || {}; }
}
});
Object.defineProperty(window, 'replacementSources', {
configurable: true,
enumerable: true,
get(){ return __plnRS; },
set(next){ __plnRS = (next && typeof next==='object') ? next : {}; }
});
}catch(_){ /* noop */ }
})();
// --- Funciones auxiliares para manejar fechas ---
// Obtiene la fecha actual en formato AAAA-MM-DD
function getCurrentDateString()
{
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Obtiene la semana actual en formato AAAA-WW (ISO 8601)
function getCurrentISOWeekString()
{
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
const week1 = new Date(date.getFullYear(), 0, 4);
const weekNumber = 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
return `${date.getFullYear()}-${String(weekNumber).padStart(2, '0')}`;
}
// Obtiene el mes actual en formato AAAA-MM
function getCurrentMonthString()
{
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return `${year}-${month}`;
}
// --- Funciones principales de estadísticas ---
// Carga las estadísticas desde localStorage
function loadEditorStats()
{
const savedStats = localStorage.getItem(STATS_STORAGE_KEY);
if (savedStats)
{
try
{
editorStats = JSON.parse(savedStats);
if (typeof editorStats !== 'object' || editorStats === null)
{
editorStats = {};
}
}
catch (e)
{
console.error('[WME PLN Stats] Error al parsear estadísticas desde localStorage:', e);
editorStats = {};
}
}
else
{
editorStats = {};
}
}
// Guarda las estadísticas en localStorage
function saveEditorStats()
{
try
{
localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(editorStats));
}
catch (e)
{
console.error('[WME PLN Stats] Error al guardar estadísticas en localStorage:', e);
}
}
// Devuelve la palabra excluida original si existe en la lista (case-insensitive, ignora tildes y espacios)
function isExcludedWord(word)
{
if (!word || !excludedWords) return null;
// Normaliza quitando tildes y pasa a minúsculas
const clean = w => w.trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
const cleanedWord = clean(word);
for (const excl of excludedWords)
{
if (clean(excl) === cleanedWord)
{
return excl; // Devuelve la versión guardada, con sus mayúsculas/tildes originales
}
}
return null;
}// isExcludedWord
// Aplica las palabras excluidas devolviendo su forma exacta guardada
function plnApplyExclusions(str){
try{
const reWord = /([\p{L}\p{M}][\p{L}\p{M}\.'’]*)/gu;
return String(str||'').replace(reWord,(m)=>{
try{
const excl = typeof isExcludedWord === 'function' ? isExcludedWord(m) : null;
return excl ? excl : m;
}catch(_){ return m; }
});
}catch(_){ return String(str||''); }
}
// FIN: Bloque De Funciones Para Estadísticas
//Calcula el área de un polígono en metros cuadrados
// Utiliza la fórmula de Shoelace (fórmula de área de Gauss
function calculateAreaNoTurf(shape)
{
if (!shape || !shape.geometry)
{
return Infinity; // retorna un valor que no active el titilado
}
try
{
// verifica si la geometría es un polígono
if (shape.geometry.type === 'Polygon')
{
// extrae las coordenadas del primer anillo del polígono
const coordinates = shape.geometry.coordinates[0]; // primer anillo del polígono
// usar la fórmula de Shoelace para calcular el área
let area = 0;
for (let i = 0; i < coordinates.length - 1; i++)
{
area += coordinates[i][0] * coordinates[i+1][1];
area -= coordinates[i][1] * coordinates[i+1][0];
}
area = Math.abs(area) / 2;
//Convierte el área a metros cuadrados
// Aproximación: 1 grado de latitud es aproximadamente 111.32 km
// 1 grado de longitud varía según la latitud, pero al nivel del equador es aproximadamente 111.32 km
// Para simplificar, usamos un valor promedio
// 1 grado de latitud = 111,319.9 metros
// 1 grado de longitud = 111,319.9 metros (en el ecuador)
// Entonces, el área en metros cuadrados es:
// area * (111319.9 * 111319.9)
// O simplemente multiplicamos por el cuadrado de la conversión de metros por grado
// Nota: Esta es una aproximación y no es precisa para áreas grandes o cerca de los polos
// Para áreas pequeñas, esta aproximación es suficiente
const metersPerDegree = 111319.9; //aproximadamente 111,319.9 metros por grado
return area * Math.pow(metersPerDegree, 2);
}
}
catch (error)
{
console.warn("[WME PLN] Error calculating area:", error);
return Infinity; // Return a value that won't trigger blinking
}
return Infinity; // Default return for non-polygon shapes
}// calculateAreaMeters
// Modifica esta función para implementar correctamente el procesamiento de titilación
// Reemplaza completamente la función processingLoop
function processingLoop() {
const currentTime = Date.now();
// Actualizar el estado de titilación cada BLINK_INTERVAL milisegundos
if (currentTime - lastBlinkTime > BLINK_INTERVAL) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
// Recorrer todos los lugares en la lista de lugares para titilar
blinkingPlaces.forEach(placeId => {
// Seleccionar específicamente el elemento con la clase area-value-element
const areaElement = document.querySelector(`tr[data-place-id="${placeId}"] .area-value-element`);
if (areaElement) {
// Solo aplicar el estilo de titilación al elemento del área
areaElement.style.opacity = blinkState ? '1' : '0.3';
}
});
}
requestAnimationFrame(processingLoop);
}// processingLoop
// Registra una edición y actualiza los contadores
function recordNormalizationEvent()
{
const userId = currentGlobalUserInfo.id;
const userName = currentGlobalUserInfo.name;
if (!userId || userId === 0 || userName === 'No detectado')
{
return;
}
// Obtiene las estadísticas del usuario o las inicializa si no existen
let userStats = editorStats[userId];
if (!userStats)
{
userStats = {
userName: userName,
total_count: 0,
monthly_count: 0,
monthly_period: "N/A",
weekly_count: 0,
weekly_period: "N/A",
daily_count: 0,
daily_period: "N/A",
last_update: 0
};
editorStats[userId] = userStats;
}
// Obtiene los periodos de tiempo actuales
const todayStr = getCurrentDateString();
const weekStr = getCurrentISOWeekString();
const monthStr = getCurrentMonthString();
// --- Lógica de reseteo de contadores ---
// Si la fecha guardada es diferente a la de hoy, resetea el contador diario.
if (userStats.daily_period !== todayStr)
{
userStats.daily_count = 0;
userStats.daily_period = todayStr;
}
// Si la semana guardada es diferente a la de hoy, resetea el contador semanal.
if (userStats.weekly_period !== weekStr)
{
userStats.weekly_count = 0;
userStats.weekly_period = weekStr;
}
// Si el mes guardado es diferente al de hoy, resetea el contador mensual.
if (userStats.monthly_period !== monthStr)
{
userStats.monthly_count = 0;
userStats.monthly_period = monthStr;
}
// --- Incrementar los contadores ---
userStats.daily_count++;
userStats.weekly_count++;
userStats.monthly_count++;
userStats.total_count++;
userStats.last_update = Date.now();
userStats.userName = userName; // Asegurarse de que el nombre esté actualizado
// Guardar los nuevos datos y actualizar la pantalla
saveEditorStats();
updateStatsDisplay();
}
// Función modificada para filtrar palabras antes de mostrarlas en la lista de selección
function openAddSpecialWordPopup(name, listType = "excludeWords") {
// Dividir el nombre en palabras
const clean = s => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'')
.replace(/\s+/g,' ').trim().toLowerCase();
const exclSet = new Set(Array.from(window.excludedWords || []).map(w => clean(w)));
let words = name.split(/\s+/)
.filter(word => word.length > 1);
words = words.filter(word => {
const lowerWord = word.toLowerCase();
// Verificar que la palabra no sea común
if (commonWords.includes(lowerWord)) return false;
// Verificar que no esté ya en palabras excluidas (insensible a mayúsculas/minúsculas)
if (exclSet.has(clean(word)))
{
return false; // Ya está en especiales
}
// Verificar que no esté en el diccionario (insensible a mayúsculas/minúsculas)
if (window.dictionaryWords && Array.from(window.dictionaryWords).some(dictWord =>
dictWord.toLowerCase() === lowerWord)) {
return false;
}
// Si la palabra coincide con algún "texto de reemplazo" (columna B) cargado desde Google Sheet,
// y ese reemplazo proviene de hoja (bloqueado), no mostrarla en la lista
try {
if (window.replacementWords) {
const isReplacementTargetFromSheet = Object.entries(window.replacementWords)
.some(([from, to]) => (to || '').toLowerCase() === lowerWord && (!window.replacementSources || window.replacementSources[from] === 'sheet'));
if (isReplacementTargetFromSheet) return false;
}
} catch (_) { /* noop */ }
return true; // Si pasa todos los filtros, mostrar la palabra
});
if (words.length === 0) {
alert("No hay palabras nuevas para agregar. Todas las palabras ya están en el diccionario o en la lista de especiales.");
return;
}
}
// Muestra los contadores en el panel flotante
function updateStatsDisplay()
{
if (!statsPanelElement || !currentGlobalUserInfo.id) return;
const userId = currentGlobalUserInfo.id;
// Obtiene los datos guardados o valores por defecto si no existen
const stats = editorStats[userId] || {
daily_count: 0,
weekly_count: 0,
monthly_count: 0,
total_count: 0
};
// Actualiza los elementos de la UI con los valores guardados
const summaryText = statsPanelElement.querySelector('#stats-summary-text');
const todayCountSpan = statsPanelElement.querySelector('#stats-count-today');
const weekCountSpan = statsPanelElement.querySelector('#stats-count-week');
const monthCountSpan = statsPanelElement.querySelector('#stats-count-month');
const totalCountSpan = statsPanelElement.querySelector('#stats-count-total');
if (summaryText) summaryText.textContent = `📊 ${stats.daily_count || 0} Places NrmliZed`;
if (todayCountSpan) todayCountSpan.textContent = stats.daily_count || 0;
if (weekCountSpan) weekCountSpan.textContent = stats.weekly_count || 0;
if (monthCountSpan) monthCountSpan.textContent = stats.monthly_count || 0;
if (totalCountSpan) totalCountSpan.textContent = stats.total_count || 0;
}
// Crea el panel de estadísticas flotante en la interfaz de usuario.
function createStatsPanel()
{
if (document.getElementById('wme-pln-stats-panel')) return;
// Contenedor principal del panel
statsPanelElement = document.createElement('div');
statsPanelElement.id = 'wme-pln-stats-panel';
Object.assign(statsPanelElement.style, {
position: 'fixed',
bottom: '60px',
left: '23%', // <-- Ancla el panel a 20px del borde izquierdo
// Se elimina la propiedad 'transform' que ya no es necesaria
backgroundColor: 'rgba(45, 45, 45, 0.9)',
color: 'white',
padding: '5px 12px',
borderRadius: '15px',
fontSize: '13px',
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
zIndex: '10000',
cursor: 'pointer',
display: 'none', // Oculto inicialmente
border: '1px solid #555',
boxShadow: '0 2px 10px rgba(0,0,0,0.5)',
userSelect: 'none',
whiteSpace: 'nowrap'
});
// Vista de resumen (la que siempre está visible)
const summaryView = document.createElement('div');
summaryView.id = 'stats-summary-view';
Object.assign(summaryView.style, {
display: 'flex',
alignItems: 'center',
gap: '6px'
});
const summaryText = document.createElement('span');
summaryText.id = 'stats-summary-text';
summaryText.textContent = '📊 0 NrmliZer Stats';
const dropdownArrow = document.createElement('span');
dropdownArrow.id = 'stats-arrow';
dropdownArrow.textContent = '▼';
dropdownArrow.style.fontSize = '10px';
summaryView.appendChild(summaryText);
summaryView.appendChild(dropdownArrow);
// Vista detallada (la que se expande)
const detailView = document.createElement('div');
detailView.id = 'stats-detail-view';
Object.assign(detailView.style, {
display: 'none',
marginTop: '8px',
paddingTop: '8px',
borderTop: '1px solid #666'
});
const list = document.createElement('ul');
Object.assign(list.style, {
margin: '0',
padding: '0',
listStyle: 'none',
textAlign: 'left'
});
// Crear elementos de la lista
const items = {
'Hoy': 'stats-count-today',
'Esta Semana': 'stats-count-week',
'Este Mes': 'stats-count-month',
'Total': 'stats-count-total'
};
for (const [label, id] of Object.entries(items)) {
const listItem = document.createElement('li');
listItem.style.marginBottom = '4px';
const countBold = document.createElement('b');
countBold.id = id;
countBold.textContent = '0';
listItem.append(`${label}: `, countBold);
list.appendChild(listItem);
}
detailView.appendChild(list);
// Ensamblar el panel
statsPanelElement.appendChild(summaryView);
statsPanelElement.appendChild(detailView);
document.body.appendChild(statsPanelElement);
// Lógica para desplegar/contraer
statsPanelElement.addEventListener('click', () => {
const isHidden = detailView.style.display === 'none';
detailView.style.display = isHidden ? 'block' : 'none';
dropdownArrow.textContent = isHidden ? '▲' : '▼';
});
// Lógica para cerrar al hacer clic fuera
document.addEventListener('click', (e) => {
if (!statsPanelElement.contains(e.target)) {
detailView.style.display = 'none';
dropdownArrow.textContent = '▼';
}
}, true);
toggleStatsPanelVisibility();
}// createStatsPanel
// Función para alternar la visibilidad del panel de estadísticas basado en el estado del checkbox.
function toggleStatsPanelVisibility()
{
if (!statsPanelElement) return;
const isEnabled = localStorage.getItem(STATS_ENABLED_KEY) === 'true';
statsPanelElement.style.display = isEnabled ? 'block' : 'none';
}// toggleStatsPanelVisibility
// FIN: Bloque De Funciones Para Estadísticas
// Función que construirá el HTML del changelog
function getChangelogHtml(versionData)
{
let html = '';
if (versionData["Novedades"] && versionData["Novedades"].length > 0)
{
html += `<h6>Novedades:</h6><ul style="margin-bottom: 10px; list-style-type: disc; margin-left: 20px;">`;
versionData["Novedades"].forEach(item => {
html += `<li>${item}</li>`;
});
html += `</ul>`;
}
if (versionData["Correcciones"] && versionData["Correcciones"].length > 0) {
html += `<h6>Correcciones:</h6><ul style="margin-bottom: 10px; list-style-type: disc; margin-left: 20px;">`;
versionData["Correcciones"].forEach(item => {
html += `<li>${item}</li>`;
});
html += `</ul>`;
}
return html;
}//getChangelogHtml
// Función para mostrar el changelog al actualizar el script
function showChangelogOnUpdate()
{
const LAST_SEEN_VERSION_KEY = `${SCRIPT_NAME}_last_seen_version`;
const lastSeenVersion = localStorage.getItem(LAST_SEEN_VERSION_KEY);
const currentScriptVersion = VERSION; // Variable global VERSION
// Obtener la versión actual del script desde GM_info
const versionData = myChangelog[currentScriptVersion];
// Verificar si hay datos de versión y si la versión actual es diferente a la última vista
if (versionData && currentScriptVersion !== lastSeenVersion)
{
const title = `${SCRIPT_NAME} v${currentScriptVersion}`;
const bodyHtml = getChangelogHtml(versionData); // Genera el HTML del cuerpo
// Crear el modal
const modal = document.createElement("div");
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.backgroundColor = "#fff";
modal.style.border = "1px solid #ccc";
modal.style.borderRadius = "8px";
modal.style.boxShadow = "0 5px 15px rgba(0,0,0,0.3)";
modal.style.padding = "20px";
modal.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
modal.style.zIndex = "20000"; // Por encima de casi todo
modal.style.width = "450px";
modal.style.maxHeight = "80vh";
modal.style.overflowY = "auto";
// Estilos adicionales para el modal
const modalTitle = document.createElement("h3");
modalTitle.textContent = title;
modalTitle.style.marginTop = "0";
modalTitle.style.marginBottom = "15px";
modalTitle.style.textAlign = "center";
modalTitle.style.color = "#333";
// Crear el cuerpo del modal con el contenido del changelog
const modalBody = document.createElement("div");
modalBody.innerHTML = bodyHtml;
// Estilos para el cuerpo del modal
const closeButton = document.createElement("button");
closeButton.textContent = "Entendido";
closeButton.style.display = "block";
closeButton.style.margin = "20px auto 0 auto";
closeButton.style.padding = "10px 20px";
closeButton.style.backgroundColor = "#007bff";
closeButton.style.color = "#fff";
closeButton.style.border = "none";
closeButton.style.borderRadius = "5px";
closeButton.style.cursor = "pointer";
//
closeButton.addEventListener("click", () => {
modal.remove();
localStorage.setItem(LAST_SEEN_VERSION_KEY, currentScriptVersion); // Guarda la versión
});
// Añadir todo al modal y al body
modal.appendChild(modalTitle);
modal.appendChild(modalBody);
modal.appendChild(closeButton);
document.body.appendChild(modal);
}
else if (!versionData)
{//
// Si no hay datos de versión, no se hace nada
localStorage.setItem(LAST_SEEN_VERSION_KEY, currentScriptVersion);
}
}//showChangelogOnUpdate
//Permite inicializar el SDK de WME
function tryInitializeSDK(finalCallback) {
let attempts = 0;
const maxAttempts = 20; // Reduced from 60 to 20
const intervalTime = 500;
let sdkAttemptInterval = null;
// Function to clear interval safely
function clearSDKInterval() {
if (sdkAttemptInterval) {
clearInterval(sdkAttemptInterval);
sdkAttemptInterval = null;
}
}
// Function to call callback safely
function safeCallback() {
clearSDKInterval();
try {
if (typeof finalCallback === 'function') {
finalCallback();
}
} catch (e) {
console.error("[WME PLN] Error in SDK callback:", e);
}
}
// Function to attempt SDK initialization
function attempt() {
// Prefer unsafeWindow.getWmeSdk in TM sandbox. Fallback to page getWmeSdk.
const getWmeSdkFn = (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.getWmeSdk === 'function')
? unsafeWindow.getWmeSdk
: (typeof getWmeSdk === 'function' ? getWmeSdk : null);
if (getWmeSdkFn) {
clearSDKInterval();
try {
wmeSDK = getWmeSdkFn({
scriptId: 'WMEPlacesNameInspector',
scriptName: SCRIPT_NAME,
});
//console.log("[WME PLN] SDK initialized successfully");
// === Export stable globals for console and other scripts ===
try {
window.WME_PLN_SDK = wmeSDK;
if (typeof unsafeWindow !== 'undefined') unsafeWindow.WME_PLN_SDK = wmeSDK;
window.WME_PLN_getSDK = function(){ return window.WME_PLN_SDK; };
window.WME_PLN_reinitSDK = function(){ tryInitializeSDK(()=>{}); };
//console.log("[WME PLN] SDK exported: window.WME_PLN_SDK, WME_PLN_getSDK(), WME_PLN_reinitSDK()");
} catch(e) {
console.warn("[WME PLN] Could not export SDK globals:", e);
}
} catch (e) {
console.error("[WME PLN] Error initializing SDK:", e);
wmeSDK = null;
}
safeCallback();
return;
}
attempts++;
if (attempts >= maxAttempts) {
console.warn(`[WME PLN] Could not find getWmeSdk() after ${maxAttempts} attempts.`);
wmeSDK = null;
safeCallback();
}
}
sdkAttemptInterval = setInterval(attempt, intervalTime);
attempt();
if (!wmeSDK && typeof window.WME_PLN_SDK !== 'undefined' && window.WME_PLN_SDK) {
try {
wmeSDK = window.WME_PLN_SDK;
window.WME_PLN_getSDK = function(){ return window.WME_PLN_SDK; };
window.WME_PLN_reinitSDK = function(){ tryInitializeSDK(()=>{}); };
//console.log("[WME PLN] Reused existing window.WME_PLN_SDK");
safeCallback();
} catch(e){ /* noop */ }
}
// Safety timeout to ensure callback is called
setTimeout(() => {
if (sdkAttemptInterval) {
console.warn("[WME PLN] Safety timeout for SDK initialization");
safeCallback();
}
}, maxAttempts * intervalTime + 1000);
}//tryInitializeSDK
// Función para obtener la ciudad de un lugar usando WME API
function getPlaceCity(venue)
{
try
{
// Intentar obtener la ciudad del venue
if (venue && venue.getAddress)
{
const address = venue.getAddress();
if (address && address.city && address.city.name)
{
return address.city.name;
}
}
// Intentar obtener la ciudad desde el SDK
if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues)
{
const venueId = venue.getID();
const venueSDK = wmeSDK.DataModel.Venues.getById({ venueId });
if (venueSDK && venueSDK.address && venueSDK.address.city && venueSDK.address.city.name) {
return venueSDK.address.city.name;
}
}
return "Sin Ciudad";
}
catch (e)
{
console.warn("[WME PLN] Error al obtener ciudad del lugar:", e);
return "Sin Ciudad";
}
}
//-----------------------------------------------------------------------------------------------------------
// 1) Con el WME SDK
// Funciones de obtención de usuario
async function getCurrentEditorViaSdk()
{
if (!wmeSDK)
{
return null;
}
if (!wmeSDK.DataModel || !wmeSDK.DataModel.User || typeof wmeSDK.DataModel.User.getCurrentUser !== 'function') {
return null;
}
try {
const user = await wmeSDK.DataModel.User.getCurrentUser();
if (user && user.id && user.name)
{
return { id: user.id, name: user.name, privilege: user.privilege };
} else {
console.warn('[WME PLN][DEBUG] SDK: getCurrentUser() devolvió datos incompletos o null:', user);
return null;
}
} catch (e) {
console.error('[WME PLN][DEBUG] SDK ERROR al obtener usuario:', e);
return null;
}
}// getCurrentEditorViaSdk
// 2) Con WazeWrap
function getCurrentEditorViaWazeWrap()
{
if (typeof WazeWrap === 'undefined') {
return null;
}
if (!WazeWrap.Login || typeof WazeWrap.Login.getLoggedInUser !== 'function') {
return null;
}
const wrapUser = WazeWrap.Login.getLoggedInUser();
if (wrapUser && wrapUser.userId && wrapUser.username) {
return { id: wrapUser.userId, name: wrapUser.username, privilege: wrapUser.privilege };
}
else
{
return null;
}
}// getCurrentEditorViaWazeWrap
// 3) Fallback a la API nativa de WME
function getCurrentEditorViaWmeInternal()
{
if (typeof W === 'undefined' || !W.loginManager)
{
return null;
}
if (W.loginManager.userId && W.loginManager.userName)
{
return { id: W.loginManager.userId, name: W.loginManager.userName, privilege: W.loginManager.userPrivilege };
}
else
{
return null;
}
}// getCurrentEditorViaWmeInternal
//-----------------------------------------------------------------------------------------------------------
// Esperar a que la API principal de Waze esté completamente cargada
async function waitForWazeAPI(callbackPrincipalDelScript)
{
let wAttempts = 0;
const wMaxAttempts = 40;
const wInterval = setInterval(async () => {
wAttempts++;
if (typeof W !== 'undefined' && W.map && W.loginManager && W.model && W.model.venues && W.userscripts && typeof W.userscripts.registerSidebarTab === 'function')
{
clearInterval(wInterval);
if (!dynamicCategoriesLoaded) // solo carga las categorías de Google Sheets si no se han cargado aún
{
try
{
await loadDynamicCategoriesFromSheet();
dynamicCategoriesLoaded = true; // <-- Marcar como cargado
}
catch (error)
{
console.error("No se pudieron cargar las categorías dinámicas:", error);
}
}
// : Esperar a que tryInitializeSDK se complete
tryInitializeSDK(() => { //
// Una vez que el SDK ha intentado inicializarse (exitosamente o no),
// y las APIs de WazeWrap y W.loginManager deberían estar cargadas,
// llamamos al callback principal del script (createSidebarTab).
callbackPrincipalDelScript();
// [PLN] Snapshot SOLO-usuario: pares previos cuyo source != 'sheet'
const __plnPreUserOnly = {};
{
const _m = window.replacementWords || {};
const _s = window.replacementSources || {};
for (const k in _m){
if (_s && _s[k] !== 'sheet') __plnPreUserOnly[k] = _m[k];
}
}
// [PLN] Cargar reemplazos de Google Sheets tras crear la UI y re-render según modo
loadReplacementsFromSheet(true).finally(() => {
// [PLN] Protector post-loader: reinsertar pares del usuario que el loader haya quitado
try {
const curMap = window.replacementWords || {};
const curSrc = window.replacementSources || {};
// Reinsertar SOLO entradas del usuario que falten tras la carga de la hoja
for (const k in __plnPreUserOnly){
if (!(k in curMap)){
curMap[k] = __plnPreUserOnly[k];
if (curSrc && typeof curSrc === 'object'){
curSrc[k] = (curSrc[k] && curSrc[k] !== 'sheet') ? curSrc[k] : 'user';
}
}
}
window.replacementWords = curMap;
window.replacementSources = curSrc;
// Canoniza (reencamina TO==FROM de hoja), sin borrar locales válidos
if (typeof plnCanonicalizeReplacementsBySheet === 'function') plnCanonicalizeReplacementsBySheet();
} catch(_) { /* noop */ }
// Re-render según modo (evita carreras con RAF)
requestAnimationFrame(() =>
{
const _el = document.getElementById("replacementsListUL") || document.querySelector("#replacementsContainer ul");
const _sel = document.getElementById("replacementModeSelector");
if (_el)
{
if (_sel && _sel.value === "swapStart" && typeof renderSwapList === "function")
{
renderSwapList(_el);
}
else
{
renderReplacementsList(_el);
}
}
});
});
}); //
}
else if (wAttempts >= wMaxAttempts) // Si no se ha cargado la API de Waze después de 20 segundos
{
clearInterval(wInterval);
console.error("[WME PLN] Waze API no se cargó completamente después de múltiples intentos."); //
callbackPrincipalDelScript(); // Llama al callback de todas formas si se agotaron los intentos
}
}, 500);
}//waitforWazeAPI
//+++++++++Funciones de Georeferenciación y Distancia++++++++++++++
// Función para calcular la distancia en metros entre dos puntos geográficos (latitud, longitud)
function calculateDistance(lat1, lon1, lat2, lon2)
{
const earthRadiusMeters = 6371e3; // Radio de la Tierra en metros
// Convertir latitudes y diferencias de longitudes de grados a radianes
const lat1Rad = lat1 * Math.PI / 180;// Convertir latitud 1 a radianes
const lat2Rad = lat2 * Math.PI / 180;// Convertir latitud 2 a radianes
const deltaLatRad = (lat2 - lat1) * Math.PI / 180;// Convertir diferencia de latitudes a radianes
const deltaLonRad = (lon2 - lon1) * Math.PI / 180;// Convertir diferencia de longitudes a radianes
// Fórmula de Haversine para calcular la distancia entre dos puntos en una esfera
// a = sin²(Δlat/2) + cos(lat1) * cos(lat2) * sin²(Δlon/2)
const a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);//
// c = 2 * atan2(√a, √(1−a))
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// Calcular la distancia final en metros
const distanceMeters = earthRadiusMeters * c;
return distanceMeters;
}//calculateDistance
//Función para obtener coordenadas de un lugar
// --- Reemplaza esta función completa ---
function getPlaceCoordinates(venueOldModel, venueSDK)
{
let lat = null;
let lon = null;
const placeId = venueOldModel ? venueOldModel.getID() : (venueSDK ? venueSDK.id : 'N/A');
// --- CORRECCIÓN ---
// PRIORIDAD 1: Usar el método recomendado getOLGeometry() del modelo antiguo, es el más estable.
if (venueOldModel && typeof venueOldModel.getOLGeometry === 'function') {
try {
const geometry = venueOldModel.getOLGeometry();
if (geometry && typeof geometry.getCentroid === 'function') {
const centroid = geometry.getCentroid();
if (centroid && typeof centroid.x === 'number' && typeof centroid.y === 'number') {
// La geometría de OpenLayers (OL) está en proyección Mercator (EPSG:3857)
// Necesitamos transformarla a coordenadas geográficas WGS84 (EPSG:4326)
if (typeof OpenLayers !== 'undefined' && OpenLayers.Projection) {
const mercatorPoint = new OpenLayers.Geometry.Point(centroid.x, centroid.y);
const wgs84Point = mercatorPoint.transform(
new OpenLayers.Projection("EPSG:3857"),
new OpenLayers.Projection("EPSG:4326")
);
lat = wgs84Point.y;
lon = wgs84Point.x;
// Validar que las coordenadas resultantes sean válidas
if (typeof lat === 'number' && typeof lon === 'number' && Math.abs(lat) <= 90 && Math.abs(lon) <= 180) {
return { lat, lon };
}
}
}
}
} catch (e) {
console.error(`[WME PLN] Error obteniendo coordenadas con getOLGeometry() para ID ${placeId}:`, e);
}
}
// PRIORIDAD 2: Fallback al objeto del SDK si el método anterior falló.
// Esto es menos ideal porque .geometry está obsoleto, pero sirve como respaldo.
if (venueSDK && venueSDK.geometry && Array.isArray(venueSDK.geometry.coordinates)) {
lon = venueSDK.geometry.coordinates[0];
lat = venueSDK.geometry.coordinates[1];
if (typeof lat === 'number' && typeof lon === 'number' && Math.abs(lat) <= 90 && Math.abs(lon) <= 180) {
return { lat, lon };
}
}
// Si todo falló, retornar nulls
console.warn(`[WME PLN] No se pudieron obtener coordenadas válidas para el ID ${placeId}.`);
return { lat: null, lon: null };
}//getPlaceCoordinates
// Nueva función robusta para agregar datos para verificación de duplicados
function addPlaceDataForDuplicateCheck(venue, venueSDK, normalizedName)
{
// Usa una variable global para almacenar los datos de lugares para comparar duplicados
if (typeof window.duplicatePlacesData === "undefined") window.duplicatePlacesData = [];
const duplicatePlacesData = window.duplicatePlacesData;
let geometry = null;
if (venueSDK && venueSDK.geometry && venueSDK.geometry.coordinates)
{
const [lon, lat] = venueSDK.geometry.coordinates;
duplicatePlacesData.push({ name: normalizedName, lat, lon, venueId: venueSDK.id });
return;
}
else if (venue && typeof venue.getOLGeometry === 'function')
{
geometry = venue.getOLGeometry();
if (geometry)
{
const lonLat = geometry.getCoordinates();
const lon = lonLat[0];
const lat = lonLat[1];
duplicatePlacesData.push({ name: normalizedName, lat, lon, venueId: venue.getID() });
return;
}
}
console.warn("[WME_PLN][WARNING] No se pudo obtener geometría válida para el lugar:", venue, venueSDK);
}
// Función para detectar nombres duplicados cercanos y generar alertas
function detectAndAlertDuplicateNames(allScannedPlacesData)
{
const DISTANCE_THRESHOLD_METERS = 50; // Umbral de distancia para considerar "cerca" (en metros)
const duplicatesGroupedForAlert = new Map(); // Almacenará {normalizedName: [{places}, {places}]}
// Paso 1: Agrupar por nombre NORMALIZADO y encontrar duplicados cercanos
allScannedPlacesData.forEach(p1 => {
if (p1.lat === null || p1.lon === null) return; // Saltar si no tiene coordenadas
// Buscar otros lugares con el mismo nombre normalizado
const nearbyMatches = allScannedPlacesData.filter(p2 => {
if (p2.id === p1.id || p2.lat === null || p2.lon === null || p1.normalized !== p2.normalized) {
return false;
}
const distance = calculateDistance(p1.lat, p1.lon, p2.lat, p2.lon);
return distance <= DISTANCE_THRESHOLD_METERS;
});
if (nearbyMatches.length > 0) {
// Si encontramos duplicados cercanos para p1, agruparlos
const groupKey = p1.normalized.toLowerCase();
if (!duplicatesGroupedForAlert.has(groupKey)) {
duplicatesGroupedForAlert.set(groupKey, new Set());
}
duplicatesGroupedForAlert.get(groupKey).add(p1); // Añadir p1
nearbyMatches.forEach(p => duplicatesGroupedForAlert.get(groupKey).add(p)); // Añadir todos sus duplicados
}
});
// Paso 2: Generar el mensaje de alerta final
if (duplicatesGroupedForAlert.size > 0)
{
let totalNearbyDuplicateGroups = 0; // Para contar la cantidad de "nombres" con duplicados
const duplicateEntriesHtml = []; // Para almacenar las líneas HTML de la alerta formateadas
duplicatesGroupedForAlert.forEach((placesSet, normalizedName) => {
const uniquePlacesInGroup = Array.from(placesSet); // Convertir Set a Array
if (uniquePlacesInGroup.length > 1) { // Solo si realmente hay más de un lugar en el grupo
totalNearbyDuplicateGroups++;
// Obtener los números de línea para cada lugar en este grupo
const lineNumbers = uniquePlacesInGroup.map(p => {
const originalPlaceInInconsistents = allScannedPlacesData.find(item => item.id === p.id);
return originalPlaceInInconsistents ? (allScannedPlacesData.indexOf(originalPlaceInInconsistents) + 1) : 'N/A';
}).filter(num => num !== 'N/A').sort((a, b) => a - b); // Asegurarse que son números y ordenarlos
// Marcar los lugares en `allScannedPlacesData` para el `⚠️` visual
uniquePlacesInGroup.forEach(p => {
const originalPlaceInInconsistents = allScannedPlacesData.find(item => item.id === p.id);
if (originalPlaceInInconsistents) {
originalPlaceInInconsistents.isDuplicate = true;
}
});
// Construir la línea para el modal
duplicateEntriesHtml.push(`
<div style="margin-bottom: 5px; font-size: 15px; text-align: left;">
<b>${totalNearbyDuplicateGroups}.</b> Nombre: <b>${normalizedName}</b><br>
<span style="font-weight: bold; color: #007bff;">Registros: [${lineNumbers.join("],[")}]</span>
</div>
`);
}
});
// Solo mostrar la alerta si realmente hay grupos de más de 1 duplicado cercano
if (duplicateEntriesHtml.length > 0) {
// Crear el modal
const modal = document.createElement("div");
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.background = "#fff";
modal.style.border = "1px solid #aad";
modal.style.padding = "28px 32px 20px 32px";
modal.style.zIndex = "20000"; // Z-INDEX ALTO para asegurar que esté encima
modal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
modal.style.fontFamily = "sans-serif";
modal.style.borderRadius = "10px";
modal.style.textAlign = "center";
modal.style.minWidth = "400px";
modal.style.maxWidth = "600px";
modal.style.maxHeight = "80vh"; // Para scroll si hay muchos duplicados
modal.style.overflowY = "auto"; // Para scroll si hay muchos duplicados
// Ícono visual
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️"; // Signo de advertencia
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
modal.appendChild(iconElement);
// Mensaje principal
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>¡Atención! Se encontraron ${duplicateEntriesHtml.length} nombres duplicados.</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
modal.appendChild(messageTitle);
const messageExplanation = document.createElement("div");
messageExplanation.textContent = `Los siguientes grupos de lugares se encuentran a menos de ${DISTANCE_THRESHOLD_METERS}m uno del otro. El algoritmo asume que son el mismo lugar, por favor revisa los registros indicados en el panel flotante:`;
messageExplanation.style.fontSize = "15px";
messageExplanation.style.color = "#555";
messageExplanation.style.marginBottom = "18px";
messageExplanation.style.textAlign = "left"; // Alinear texto explicativo a la izquierda
modal.appendChild(messageExplanation);
// Lista de duplicados
const duplicatesListDiv = document.createElement("div");
duplicatesListDiv.style.textAlign = "left"; // Alinear la lista a la izquierda
duplicatesListDiv.style.paddingLeft = "10px"; // Pequeño padding para los números
duplicatesListDiv.innerHTML = duplicateEntriesHtml.join('');
modal.appendChild(duplicatesListDiv);
// Botón OK
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
buttonWrapper.style.marginTop = "20px"; // Espacio superior
const okBtn = document.createElement("button");
okBtn.textContent = "OK";
okBtn.style.padding = "7px 18px";
okBtn.style.background = "#007bff";
okBtn.style.color = "#fff";
okBtn.style.border = "none";
okBtn.style.borderRadius = "4px";
okBtn.style.cursor = "pointer";
okBtn.style.fontWeight = "bold";
okBtn.addEventListener("click", () => modal.remove()); // Cierra el modal
buttonWrapper.appendChild(okBtn);
modal.appendChild(buttonWrapper);
document.body.appendChild(modal); // Añadir el modal al body
}
}
}
//+++++++++FIN Funciones de Georeferenciación y Distancia++++++++++++++
// === Compat: parche para Venue.getAddress() (evita firma antigua sin W.model) ===
// Motivo: "Venue.getAddress() must be called with a W.model as a first argument" (definición antigua deprecada)
// Estrategia: localizar el prototipo de Venue y envolver getAddress para que, si no recibe modelo, use W.model
(function patchVenueGetAddressDeprecated(){
let tries = 0;
const maxTries = 12;
const tick = setInterval(() => {
tries++;
try {
if (typeof W === 'undefined' || !W.model || !W.model.venues) return; // esperar a W
let sampleVenue = null;
// 1) getObjectArray si existe
if (W.model.venues && typeof W.model.venues.getObjectArray === 'function') {
const arr = W.model.venues.getObjectArray();
if (arr && arr.length) sampleVenue = arr[0];
}
// 2) repo.objects como fallback
if (!sampleVenue && W.model.venues && W.model.venues.objects) {
const ids = Object.keys(W.model.venues.objects);
if (ids.length) sampleVenue = W.model.venues.objects[ids[0]];
}
if (!sampleVenue || typeof sampleVenue.getAddress !== 'function') {
if (tries >= maxTries) clearInterval(tick);
return;
}
const proto = Object.getPrototypeOf(sampleVenue);
if (!proto || typeof proto.getAddress !== 'function' || proto.getAddress.__pln_patched) {
if (tries >= maxTries) clearInterval(tick);
return;
}
const originalGetAddress = proto.getAddress;
function patchedGetAddress(modelArg) {
// Si no se pasa modelo, usar W.model para mantener compatibilidad
if (arguments.length === 0 && typeof W !== 'undefined' && W.model) {
try { return originalGetAddress.call(this, W.model); } catch (e) { /* cae abajo */ }
}
return originalGetAddress.apply(this, arguments);
}
patchedGetAddress.__pln_patched = true;
proto.getAddress = patchedGetAddress;
//console.log('[WME PLN] Compat patch aplicado: Venue.getAddress() acepta llamada sin argumento y usa W.model.');
clearInterval(tick);
} catch (e) {
if (tries >= maxTries) clearInterval(tick);
}
}, 500);
})();
//**************************************************************************
//Nombre: plnExtractAddressIds
//Fecha modificación: 2025-08-10
//Descripción: SDK‑only. Obtiene countryID y stateID desde sdkVenue.address,
// incluso cuando Street/City están vacíos.
//**************************************************************************
function plnExtractAddressIds(venueId, sdkVenue) {
plnLog('extractIds:start', { venueId, hasSdkVenue: !!sdkVenue });
const out = { countryID: null, stateID: null, streetName: '', houseNumber: '' };
if (sdkVenue && sdkVenue.address) {
const addr = sdkVenue.address;
out.countryID = addr?.country?.id ?? addr?.countryID ?? addr?.countryId ?? null;
out.stateID = addr?.state?.id ?? addr?.stateID ?? addr?.stateId ?? null;
out.streetName = addr?.street?.name ?? addr?.streetName ?? '';
out.houseNumber = addr?.houseNumber ?? '';
}
plnLog('extractIds:fromSDK', out);
return out;
}
//**************************************************************************
//Nombre: plnResolveIdsFromCity
//Fecha modificación: 2025-08-10
//Descripción: SDK-only. A partir de cityId intenta obtener stateID y countryID
// usando los repositorios del SDK (Cities → States → Countries).
//**************************************************************************
function plnResolveIdsFromCity(cityId)
{
const out = { countryID: null, stateID: null };
try {
if (!wmeSDK || !wmeSDK.DataModel) return out;
const cityIdNum = Number(cityId);
let city = null;
try {
if (wmeSDK.DataModel.Cities?.getById) {
city = wmeSDK.DataModel.Cities.getById({ cityId: cityIdNum }); // <-- number
}
} catch(_) {}
plnLog('resolveFromCity:city', { requested: cityIdNum, found: !!city });
if (!city) return out;
let stateId = city.state?.id ?? city.stateID ?? city.stateId ?? city.attributes?.state?.attributes?.id ?? city.attributes?.state?.id ?? null;
let countryId = city.country?.id ?? city.countryID ?? city.countryId ?? city.attributes?.country?.attributes?.id ?? city.attributes?.country?.id ?? null;
if (!countryId && stateId && wmeSDK.DataModel.States?.getById) {
try {
const state = wmeSDK.DataModel.States.getById({ stateId: Number(stateId) }); // <-- number
countryId = state?.country?.id ?? state?.countryID ?? state?.countryId ?? null;
} catch(_) {}
}
if (stateId) out.stateID = Number(stateId);
if (countryId) out.countryID = Number(countryId);
plnLog('resolveFromCity:result', out);
} catch (e) {
plnErr('resolveFromCity:error', e);
}
return out;
}//plnResolveIdsFromCity
//**************************************************************************
//Nombre: plnGetBaseVenueId
//Descripción: Devuelve el ID base (antes del primer punto) como string
//**************************************************************************
function plnGetBaseVenueId(id){
return String(id).split('.')[0];
}
//**************************************************************************
//Nombre: plnGetEmptyStreetIdForCity
//Descripción: Obtiene el streetId "vacío" (No street) para una ciudad dada usando solo el SDK
// Intenta varias rutas del repositorio y hace pequeños reintentos síncronos.
//**************************************************************************
function plnGetEmptyStreetIdForCity(cityId)
{
const cidNum = Number(cityId);
try{
if (wmeSDK?.DataModel?.Streets?.getStreet){
const st = wmeSDK.DataModel.Streets.getStreet({ cityId: cidNum, streetName: '' }); // <-- number
if (st && st.id != null){ plnLog('streets:emptyFound', { cityId: cidNum, streetId: Number(st.id) }); return Number(st.id); }
}
}catch(_){}
try{
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const found = all.find(s => Number(s?.city?.id) === cidNum && (s?.isEmpty || s?.name === '' || s?.streetName === ''));
if (found){ plnLog('streets:emptyFound', { cityId: cidNum, streetId: Number(found.id) }); return Number(found.id); }
}catch(_){}
try{
for (let i=0;i<8;i++){
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const found = all.find(s => Number(s?.city?.id) === cidNum && (s?.isEmpty || s?.name === '' || s?.streetName === ''));
if (found){ plnLog('streets:emptyFound', { cityId: cidNum, streetId: Number(found.id) }); return Number(found.id); }
}
}catch(_){}
return null;
}//plnGetEmptyStreetIdForCity
/**************************************************************************
* Nombre: plnGetVenueCityIdNow
* Descripción: Lee (sincrónicamente) el cityID actual del venue vía SDK.
**************************************************************************/
function plnGetVenueCityIdNow(venueIdStr){
try{
const v = wmeSDK?.DataModel?.Venues?.getById?.({ venueId: String(venueIdStr) });
const cid = v?.address?.city?.id ?? v?.address?.cityID ?? v?.address?.cityId ?? null;
return (cid != null) ? Number(cid) : null;
}catch(_){ return null; }
}
/**************************************************************************
* Nombre: plnWaitVenueCity
* Descripción: Espera (polling corto) a que el venue muestre el cityID esperado.
**************************************************************************/
function plnWaitVenueCity(venueIdStr, expectedCityId, timeoutMs = 1500){
return new Promise(resolve=>{
const start = Date.now();
const target = Number(expectedCityId);
const tick = setInterval(()=>{
const cid = plnGetVenueCityIdNow(venueIdStr);
if (cid === target){ clearInterval(tick); return resolve(true); }
if (Date.now() - start > timeoutMs){ clearInterval(tick); return resolve(false); }
}, 120);
});
}
/**************************************************************************
* Nombre: plnFindBridgeCityIdInState
* Descripción: Busca una ciudad del mismo estado que tenga "No street" (vacía)
**************************************************************************/
function plnFindBridgeCityIdInState(stateId){
try{
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const match = all.find(s =>
(s?.isEmpty || s?.name === '' || s?.streetName === '') &&
Number(s?.city?.state?.id ?? s?.city?.stateID ?? s?.city?.stateId) === Number(stateId)
);
return match?.city?.id != null ? Number(match.city.id) : null;
}catch(_){ return null; }
}
/**************************************************************************
* Nombre: plnApplyCityOnce
* Descripción: Aplica una ciudad UNA vez:
* - Si existe street vacío en esa ciudad → usa streetId
* - Si no, usa country/state/city + emptyStreet:true
* Devuelve: 'streetId' o { type:'ids', ids } o null si no pudo construir args.
**************************************************************************/
function plnApplyCityOnce(venueIdStr, cityIdNum, houseNumber){
// Ruta 1: street vacío específico
const emptyStreetId = plnGetEmptyStreetIdForCity(cityIdNum);
if (emptyStreetId != null){
const args = { venueId: venueIdStr, streetId: Number(emptyStreetId) };
if (houseNumber) args.houseNumber = houseNumber;
plnLog('apply:updateAddress(args)', args);
wmeSDK.DataModel.Venues.updateAddress(args);
setTimeout(()=>{ try{ plnTryAutoApplyAddressPanel?.(); }catch{} }, 200);
return 'streetId';
}
// Ruta 2: IDs completos con emptyStreet:true
const ids = plnResolveIdsFromCity(cityIdNum);
plnLog('apply:fallbackIds', ids);
if (ids.countryID && ids.stateID){
const args2 = {
venueId: venueIdStr,
countryID: Number(ids.countryID),
stateID: Number(ids.stateID),
cityID: Number(cityIdNum),
emptyStreet: true
};
if (houseNumber) args2.houseNumber = houseNumber;
plnLog('apply:updateAddress(args2)', args2);
wmeSDK.DataModel.Venues.updateAddress(args2);
setTimeout(()=>{ try{ plnTryAutoApplyAddressPanel?.(); }catch{} }, 200);
return { type:'ids', ids };
}
return null;
}
// ===== INICIO: REEMPLAZAR ESTA FUNCIÓN COMPLETA =====
async function plnApplyCityToVenue(venueId, selectedCityId, selectedCityName)
{
plnLog('apply:start', { venueId, selectedCityId, selectedCityName });
if (!wmeSDK?.DataModel?.Venues?.updateAddress){
plnErr('apply:sdkNotReady');
return;
}
try{
const venueIdStr = String(venueId);
const cityIdNum = Number(selectedCityId) || 0;
// Intento obtener houseNumber (no bloqueante), es una buena práctica mantenerlo.
let houseNumber = '';
try{
const v0 = wmeSDK.DataModel.Venues.getById?.({ venueId: venueIdStr });
if (v0?.address?.houseNumber) houseNumber = String(v0.address.houseNumber);
}catch(_){ /* noop */ }
// MODIFICACIÓN CLAVE: Se elimina la lógica de espera y el "Plan B (bridge)".
// Simplemente llamamos a la función que aplica la ciudad y confiamos en que funciona.
const attemptKind = plnApplyCityOnce(venueIdStr, cityIdNum, houseNumber);
if (attemptKind) {
// Si attemptKind no es nulo, significa que se pudo construir y enviar la solicitud al SDK.
// Asumimos el éxito aquí, ya que la espera en la UI es el punto de fallo.
plnLog('apply:doneWithSDK: optimistic success');
// El llamado a plnTryAutoApplyAddressPanel ya está dentro de plnApplyCityOnce,
// por lo que se ejecutará automáticamente.
return;
}
// Si plnApplyCityOnce devuelve null, significa que no pudo encontrar los IDs necesarios.
// Solo en este caso, lanzamos el error.
plnErr('apply:noSdkVenueOrAddress', { reason: "Could not resolve IDs for city.", cityIdNum });
}catch(e){
plnErr('apply:sdkBranchError', e);
}
}
// ===== FIN: REEMPLAZAR ESTA FUNCIÓN COMPLETA =====
// DEBUG: Detectar llamadas a getElementById('') y registrar stack
(function patchGetElementByIdDebug(){
try {
const _origGetById = document.getElementById.bind(document);
document.getElementById = function(id){
if (id === '') {
console.warn('[WME PLN][DEBUG] getElementById(\"\") llamada. Stack:', new Error().stack);
return null;
}
return _origGetById(id);
};
} catch(_){}
})();
//**************************************************************************
//Nombre: plnTryAutoApplyAddressPanel
//Fecha modificación: 2025-08-09
//Descripción: Si el editor de Dirección está abierto, pulsa "Apply/Aplicar" para que la UI deje de mostrar "No address".
//Notas: No guarda en servidor; solo confirma los campos dentro del panel de dirección.
//**************************************************************************
function plnTryAutoApplyAddressPanel() {
plnLog('ui:autoApply:begin');
try {
// Buscar botones candidatos con texto Apply/Aplicar que estén visibles
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => b && b.offsetParent !== null) // visibles
.filter(b => /^(apply|aplicar)$/i.test((b.textContent || b.innerText || '').trim()));
plnLog('ui:autoApply:candidateButtons', btns.length);
for (const b of btns) {
// Asegurar que pertenece al editor de dirección: debe haber inputs de calle/house en su contenedor
const container = (function findContainer(node){
let n = node;
for (let i = 0; i < 6 && n; i++) {
if (n.querySelector && (
n.querySelector('input[placeholder*="street" i]') ||
n.querySelector('input[placeholder*="calle" i]') ||
n.querySelector('input[placeholder*="house" i]') ||
n.querySelector('input[placeholder*="número" i]')
)) return n;
n = n.parentElement;
}
return null;
})(b);
if (!container) continue; // botón Apply no es del editor de dirección
// Evitar doble click si el botón está deshabilitado
if (b.disabled) continue;
// Log texto del resumen de dirección antes y después
const beforeSummary = (function(){
const el = Array.from(document.querySelectorAll('div,span')).find(x => /no (address|street|city)/i.test((x.textContent||'')));
return el ? el.textContent.trim() : null;
})();
plnLog('ui:autoApply:beforeClickSummary', beforeSummary);
// Click seguro
b.click();
setTimeout(()=>{
const afterSummary = (function(){
const el = Array.from(document.querySelectorAll('div,span')).find(x => /no (address|street|city)/i.test((x.textContent||'')));
return el ? el.textContent.trim() : null;
})();
plnLog('ui:autoApply:afterClickSummary', afterSummary);
}, 300);
// console.log('[WME PLN] Address panel Apply pulsado automáticamente para reflejar cambios.');
return; // una sola vez
}
} catch (e) {
console.warn('[WME PLN] No se pudo auto-aplicar el panel de dirección:', e);
}
}
//**************************************************************************
//Nombre: registerCityModalHelpers
//Fecha modificación: 2025-08-09
//Hora: 17:00
//Autor: mincho77
//Entradas: ninguna
//Salidas: ninguna
//Prerrequisitos si existen: DOM disponible
//Descripción: Delegado de click para botón 'Aplicar Ciudad' y observador para mostrar ID junto al nombre de ciudad.
//**************************************************************************
function registerCityModalHelpers()
{
let _plnCityClickTs = 0; // Allen-style: debounce timestamp for "Aplicar ciudad"
function findCityContainer(start)
{
let node = start;
for (let i = 0; i < 8 && node; i++){
if (node.querySelector && node.querySelector('input[type="radio"]'))
{
const hasCityRadios = node.querySelector(
'input[type="radio"][name*="city"], input[type="radio"][name^="city-"], input[type="radio"][name^="city_selection"], input[type="radio"][name^="city-selection-"]'
);
if (hasCityRadios) return node;
}
node = node.parentElement;
}
return document.body;
}
// Añade "(ID: ####)" a cada opción si aún no lo tiene
function annotateCityIds(container){
try{
const radios = container.querySelectorAll('input[type="radio"][name*="city"], input[type="radio"][name^="city-"], input[type="radio"][name^="city_selection"], input[type="radio"][name^="city-selection-"]');
radios.forEach(r => {
const lbl = container.querySelector(`label[for="${r.id}"]`) || r.closest('label');
if (!lbl) return;
if (/\(ID:\s*\d+\)/.test(lbl.textContent)) return; // ya está
lbl.appendChild(document.createTextNode(` (ID: ${r.value})`));
});
}catch(_){ /* noop */ }
}
// Resuelve el venueId como string; nunca devuelve número
function resolveVenueIdString(btn, container, selected)
{
// 1) data-venue-id en botón o contenedor
let id = btn.getAttribute('data-venue-id') || container.getAttribute('data-venue-id');
if (id) return String(id);
// 2) name del radio: city-selection-<venueId>
const nameAttr = selected?.name || '';
let m = nameAttr.match(/city[-_]?selection[-_]?([0-9.]+)/i);
if (m && m[1]) return String(m[1]);
// 3) atributo id del radio
const selId = selected?.id || '';
m = selId.match(/city[-_]?selection[-_]?([0-9.]+)/i);
if (m && m[1]) return String(m[1]);
// 4) URL ?venues=186384446. ...
const h = String(location.href || '');
m = h.match(/[?&]venues=([0-9.]+)/);
if (m && m[1]) return String(m[1]);
return '';
}
// Click en "Aplicar ciudad"
document.addEventListener('click', function (e)
{
const btn = e.target.closest('button');
if (!btn) return;
const txt = (btn.textContent || btn.innerText || '').trim().toLowerCase();
plnLog('ui:click', { buttonText: txt });
if (!/^(aplicar ciudad|apply city)$/i.test(txt)) return;
// Allen-style: Debounce to avoid double handling when WME/UI fires multiple events
const _nowTs = Date.now();
if (_nowTs - _plnCityClickTs < 400) { // 400ms debounce window
plnLog('ui:debounce');
return;
}
_plnCityClickTs = _nowTs;
e.preventDefault();
e.stopImmediatePropagation?.();
e.stopPropagation?.();
const container = findCityContainer(btn);
annotateCityIds(container);
let selected = container.querySelector(
'input[type="radio"][name*="city"]:checked, input[type="radio"][name^="city-"]:checked, input[type="radio"][name^="city_selection"]:checked, input[type="radio"][name^="city-selection-"]:checked'
) || container.querySelector('input[type="radio"]:checked');
plnLog('ui:selectedRadio', { exists: !!selected, name: selected && selected.name, value: selected && selected.value });
if (!selected){ alert('Selecciona una ciudad.'); return; }
const selectedCityId = Number(selected.value);
// Lee el nombre mostrado (limpia “(ID: 123) 3.7 km” si existe)
let selectedCityName = '';
const label = container.querySelector(`label[for="${selected.id}"]`) || selected.closest('label');
if (label)
{
selectedCityName = (label.textContent || '').trim();
selectedCityName = selectedCityName.replace(/\s*\(ID:\s*\d+\)\s*[\d\.,]*\s*(km|m)?/i,'').trim();
}
const venueIdStr = resolveVenueIdString(btn, container, selected);
plnLog('ui:resolvedData', { venueId: venueIdStr, selectedCityId, selectedCityName });
plnApplyCityToVenue(venueIdStr, selectedCityId, selectedCityName);
// NUEVO BLOQUE: Cambiar el ícono a chulito en la tabla de resultados
setTimeout(() => {
// Busca la fila correspondiente por data-place-id
const row = document.querySelector(`tr[data-place-id="${venueIdStr}"]`);
if (row) {
const cityStatusIcon = row.querySelector('.city-status-icon');
if (cityStatusIcon) {
cityStatusIcon.innerHTML = '✅';
cityStatusIcon.style.color = 'green';
cityStatusIcon.title = `Ciudad: ${selectedCityName}`; // Actualiza el tooltip con el nombre de la ciudad
}
}
}, 500); // Espera breve para asegurar que el cambio de ciudad se haya aplicado
}, true);
}
try { window.WME_PLN_registerCityModalHelpers = registerCityModalHelpers; } catch(_){}
try { registerCityModalHelpers(); } catch(e){ console.warn('[WME PLN] registerCityModalHelpers init error', e); }
// Permite crear un panel flotante en WME
function updateScanProgressBar(currentIndex, totalPlaces)
{
if (totalPlaces === 0) // Si no hay lugares, no actualiza la barra de progreso
return;
let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100); // Calcular el porcentaje de progreso
progressPercent = Math.min(progressPercent, 100);
const progressBarInnerTab = document.getElementById("progressBarInnerTab"); // Actualizar la barra de progreso
const progressBarTextTab = document.getElementById("progressBarTextTab"); // Actualizar el texto de la barra de progreso
if (progressBarInnerTab && progressBarTextTab) // Asegurarse de que los elementos existen antes de intentar actualizarlos
{
progressBarInnerTab.style.width = `${progressPercent}%`; // Actualizar el ancho de la barra de progreso
const currentDisplay = Math.min(currentIndex + 1, totalPlaces); // Mostrar el número actual de lugares procesados
progressBarTextTab.textContent = `Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`; // Actualizar el texto de la barra de progreso
}
}//updateScanProgressBar
// Función auxiliar para actualizar el contador de inconsistencias en la cabecera
function updateInconsistenciesCount(delta) {
const resultsCounterDiv = document.querySelector("#wme-place-inspector-panel .results-counter-display"); // Nuevo ID/Clase
if (resultsCounterDiv) {
let currentCount = parseInt(resultsCounterDiv.dataset.currentCount || resultsCounterDiv.textContent.match(/\d+/)?.[0] || '0', 10);
currentCount = Math.max(0, currentCount + delta); // Asegura que no sea negativo
resultsCounterDiv.dataset.currentCount = currentCount; // Guardar el valor real en un data attribute
const totalOriginal = parseInt(resultsCounterDiv.dataset.totalOriginal || '0', 10);
const maxRenderLimit = parseInt(resultsCounterDiv.dataset.maxRenderLimit || '0', 10);
if (currentCount === 0) {
resultsCounterDiv.innerHTML = `<span style="color: green;">✔</span> Todos los lugares visibles están ${totalOriginal > 0 ? "procesados o excluidos" : "correctamente normalizados"}.`;
// Si todo está procesado, podrías querer ocultar la tabla o mostrar un mensaje de éxito grande.
const outputDiv = document.querySelector("#wme-place-inspector-output");
if (outputDiv) outputDiv.innerHTML = `<div style='color:green; padding:10px;'>✔ Todos los lugares visibles están correctamente normalizados o excluidos.</div>`;
} else if (totalOriginal > maxRenderLimit && maxRenderLimit > 0) {
// Si se aplicó un límite, actualiza el mensaje mostrando el conteo actual de visibles.
resultsCounterDiv.innerHTML = `<span style="color: #ff0000;">Inconsistencias encontradas: <b>${currentCount}</b> de <b>${totalOriginal}</b></span>. Mostrando <span style="color: #ff0000;"><b>${currentCount}</b></span> (límite de ${maxRenderLimit} aplicado).`;
} else {
// Mensaje normal sin límite aplicado
resultsCounterDiv.innerHTML = `<span style="color: #ff0000;">Inconsistencias encontradas: <b>${currentCount}</b> de <b>${totalOriginal}</b></span>. Mostrando <span style="color: #ff0000;"><b>${currentCount}</b></span>.`;
}
}
}//updateInconsistenciesCount
// Permite crear un panel flotante en WME
function escapeRegExp(string)
{
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}//escapeRegExp
// Función para cargar palabras del diccionario desde Google Sheets (Hoja "Dictionary")
async function loadDictionaryWordsFromSheet(forceReload = false)
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = "Dictionary!A2:B";
// usa window.dictionaryWords y window.dictionaryIndex para almacenar las palabras y su índice
// Si no existen, las inicializa como un Set y un objeto vacío
if (!window.dictionaryWords) window.dictionaryWords = new Set();
if (!window.dictionaryIndex) window.dictionaryIndex = {};
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
return new Promise((resolve) =>
{
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY")
{
console.warn('[WME PLN] SPREADSHEET_ID o API_KEY no configurados para el diccionario.');
resolve();
return;
}
// verifica si hay datos en caché
// Si hay datos en caché y no se fuerza la recarga, los usa
// Si no hay datos en caché o se fuerza la recarga, hace la solicitud
const cachedData = localStorage.getItem("wme_pln_dictionary_cache");
if (!forceReload && cachedData)
{
try {
const { data, timestamp } = JSON.parse(cachedData);
// usar caché si tiene menos de 24 horas
if (data && timestamp && (Date.now() - timestamp < 24 * 60 * 60 * 1000))
{
//console.log('[WME PLN] Usando datos en caché. Tiempo restante para expirar:', ((timestamp + 24 * 60 * 60 * 1000) - Date.now())/1000/60, 'minutos');
// console.log('[WME PLN] Usando diccionario en caché');
// restaura las palabras y el índice del diccionario desde la caché
window.dictionaryWords = new Set(data.words);
window.dictionaryIndex = data.index;
updateDictionaryWordsCountLabel();
resolve();
return;
}
} catch (e) {
console.warn('[WME PLN] Error al leer caché del diccionario:', e);
}
}
makeRequest({
method: "GET",
url: url,
timeout: 10000,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
let newWordsAdded = 0;
if (data.values) {
data.values.forEach(row => {
const word = (row[0] || '').trim();
if (word && !window.dictionaryWords.has(word.toLowerCase())) {
window.dictionaryWords.add(word.toLowerCase());
const firstChar = word.charAt(0).toLowerCase();
if (!window.dictionaryIndex[firstChar])
window.dictionaryIndex[firstChar] = [];
window.dictionaryIndex[firstChar].push(word.toLowerCase());
newWordsAdded++;
}
});
// Cache the dictionary
try {
localStorage.setItem("wme_pln_dictionary_cache", JSON.stringify({
data: {
words: Array.from(window.dictionaryWords),
index: window.dictionaryIndex
},
timestamp: Date.now()
}));
} catch (e) {
console.warn('[WME PLN] Error al guardar caché del diccionario:', e);
}
// también guarda en localStorage para uso rápido
try {
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
} catch (e) {
console.error("[WME PLN] Error guardando diccionario en localStorage:", e);
}
updateDictionaryWordsCountLabel();
//console.log(`[WME PLN] Diccionario cargado: ${newWordsAdded} palabras nuevas añadidas.`);
}
} catch (e) {
console.error('[WME PLN] Error al procesar datos del diccionario:', e);
}
}
resolve();
},
// Añade esto en ambas funciones, justo después del try/catch en onload:
onerror: function (error) {
console.error('[WME PLN] Error de red al cargar datos desde Google Sheets:', error);
//console.log('[WME PLN] URL que falló:', url);
resolve(); // Resolver la promesa para no bloquear
},
ontimeout: function () {
console.error('[WME PLN] Timeout al cargar diccionario');
resolve();
}
});
});
}//loadDictionaryWordsFromSheet
//Función Para Cargar Categorías Desde Google Sheets
//Función Para Cargar Categorías Desde Google Sheets
async function loadDynamicCategoriesFromSheet()
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = "Categories!A2:E";
window.dynamicCategoryRules = []; // Definimos la variable global para guardar las reglas
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
return new Promise((resolve) => {
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") {
console.warn('[WME PLN] No se ha configurado SPREADSHEET_ID o API_KEY. Se omitirá la carga de categorías dinámicas.');
resolve();
return;
}
// Check for cached data first
const cachedData = localStorage.getItem("wme_pln_categories_cache");
if (cachedData) {
try {
const { data, timestamp } = JSON.parse(cachedData);
// Use cache if less than 24 hours old
if (data && timestamp && (Date.now() - timestamp < 24 * 60 * 60 * 1000)) {
//console.log('[WME PLN] Usando categorías en caché. Reconstruyendo RegExp...');
// ===================== INICIO DE LA CORRECCIÓN =====================
// Se itera sobre los datos de la caché para reconstruir las expresiones regulares
window.dynamicCategoryRules = data.map(rule => {
if (rule.keyword) { // Asegurarse de que la regla tenga keywords
const keywords = rule.keyword.split(';').map(k => k.trim()).filter(k => k.length > 0);
const regexParts = keywords.map(k => `\\b${escapeRegExp(k)}\\b`);
const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
// Devolver la regla con la propiedad compiledRegex correctamente creada
return { ...rule, compiledRegex: combinedRegex };
}
return rule; // Devuelve la regla sin cambios si no tiene keyword
});
// ===================== FIN DE LA CORRECCIÓN =====================
window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length);
resolve();
return;
}
} catch (e) {
console.warn('[WME PLN] Error al leer caché de categorías:', e);
}
}
makeRequest({
method: "GET",
url: url,
timeout: 10000, // Add timeout
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
if (data.values) {
// El procesamiento de los datos de la API ya era correcto
window.dynamicCategoryRules = data.values.map(row => {
const keyword = (row[0] || '').toLowerCase().trim();
const keywords = keyword.split(';').map(k => k.trim()).filter(k => k.length > 0);
const regexParts = keywords.map(k => `\\b${escapeRegExp(k)}\\b`);
const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
return {
keyword: keyword,
categoryKey: row[1] || '',
icon: row[2] || '⚪',
desc_es: row[3] || 'Sin descripción',
desc_en: row[4] || 'No description',
compiledRegex: combinedRegex
};
});
window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length);
// La lógica para guardar en caché también es correcta
try {
localStorage.setItem("wme_pln_categories_cache", JSON.stringify({
data: window.dynamicCategoryRules,
timestamp: Date.now()
}));
} catch (e) {
console.warn('[WME PLN] Error al guardar caché de categorías:', e);
}
//console.log('[WME PLN] Categorías cargadas desde API');
}
} catch (e) {
console.error('[WME PLN] Error al procesar datos de categorías:', e);
}
} else {
console.warn(`[WME PLN] Error HTTP ${response.status} al cargar categorías`);
}
resolve();
},
onerror: function (error) {
console.error('[WME PLN] Error de red al cargar categorías:', error);
resolve();
},
ontimeout: function () {
console.error('[WME PLN] Timeout al cargar categorías');
resolve();
}
});
});
}//loadDynamicCategoriesFromSheet
// Función para encontrar la categoría de un lugar basado en su nombre
function findCategoryForPlace(placeName)
{
if (!placeName || typeof placeName !== 'string' || !window.dynamicCategoryRules || window.dynamicCategoryRules.length === 0) // Si el nombre del lugar es inválido o no hay reglas de categoría cargadas, devuelve un array vacío de sugerencias.
return [];
const lowerCasePlaceName = placeName.toLowerCase();// Convertir el nombre del lugar a minúsculas para comparaciones insensibles a mayúsculas
const allMatchingRules = []; // Este array almacenará todas las reglas de categoría que coincidan.
const placeWords = lowerCasePlaceName.split(/\s+/).filter(w => w.length > 0); // Descomponer el nombre del lugar en palabras
const SIMILARITY_THRESHOLD_FOR_KEYWORDS = 0.95; // Puedes ajustar este umbral (ej. 0.90 para 90% de similitud)
// PASO 0: Normalizar el nombre del lugar eliminando diacríticos y caracteres especiales
for (const rule of window.dynamicCategoryRules)
{
if (!rule.compiledRegex) continue; // Si la regla no tiene una expresión regular compilada (lo cual no debería pasar si se cargó correctamente), salta a la siguiente regla.
// **PASO 1: Búsqueda por Regex Exacta
if (rule.compiledRegex.test(lowerCasePlaceName))
{
if (!allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey)) {
allMatchingRules.push(rule);
}
// Si Ya Añadimos La Regla Por Regex Exacta, Pasar A La Siguiente Regla Para Ahorrar Cálculos De Similitud
continue;
}
// **PASO 2: Búsqueda por Similitud para CADA palabra del lugar vs CADA palabra clave de la regla**
const ruleKeywords = rule.keyword.split(';').map(k => k.trim().toLowerCase()).filter(k => k.length > 0);
let foundSimilarityForThisRule = false; // Bandera para saber si ya encontramos una buena similitud para esta regla, para no seguir buscando más palabras clave de la regla.
for (const pWord of placeWords) // Cada palabra del nombre del lugar
{ // Cada palabra del nombre del lugar
if (foundSimilarityForThisRule) break; // Si ya encontramos una buena similitud para esta regla, pasamos a la siguiente.
for (const rKeyword of ruleKeywords)
{ // Cada palabra clave de la regla
// Asegurarse de que rKeyword no sea una expresión regular, sino la palabra literal para Levenshtein
const similarity = calculateSimilarity(pWord, rKeyword); // Calcular la similitud entre la palabra del lugar y la palabra clave de la regla
if (similarity >= SIMILARITY_THRESHOLD_FOR_KEYWORDS && !allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey)) // Si la similitud es alta y aún no hemos añadido esta categoría
{
allMatchingRules.push(rule);
foundSimilarityForThisRule = true; // Marcamos que ya la encontramos para esta regla
break; // Salimos del bucle de rKeyword y pWord
}
}
}
}
//console.log(`[WME PLN][DEBUG] findCategoryForPlace para "${placeName}" devolvió: `, allMatchingRules);
return allMatchingRules;
}//findCategoryForPlace
// Permite obtener el icono de una categoría
function getWazeLanguage()
{
// 1. Intento principal con el SDK (método recomendado)
if (wmeSDK && typeof wmeSDK.getWazeLocale === 'function')
{
const locale = wmeSDK.getWazeLocale(); // ej: 'es-419'
if (locale)
return locale.split('-')[0].toLowerCase(); // -> 'es'
}
// 2. Fallback al objeto global 'W' si el SDK falla
if (typeof W !== 'undefined' && W.locale)
return W.locale.split('-')[0].toLowerCase();
// 3. Último recurso si nada funciona
return 'es';
}//getWazeLanguage
//Permite obtener el icono y descripción de una categoría
function getCategoryDetails(categoryKey)
{
const lang = getWazeLanguage();
// 1. Intento con la hoja de Google (window.dynamicCategoryRules)
if (window.dynamicCategoryRules && window.dynamicCategoryRules.length > 0)
{
const rule = window.dynamicCategoryRules.find(r => r.categoryKey.toUpperCase() === categoryKey.toUpperCase());
if (rule)
{
const description = (lang === 'es' && rule.desc_es) ? rule.desc_es : rule.desc_en;
return { icon: rule.icon, description: description };
}
}
// 2. Fallback a la lista interna del script si no se encontró en la hoja
const hardcodedInfo = getCategoryIcon(categoryKey); // Llama a la función original
if (hardcodedInfo && hardcodedInfo.icon !== '⚪' && hardcodedInfo.icon !== '❓')
{
// La función original devuelve un título "Español / English", lo separamos.
const descriptions = hardcodedInfo.title.split(' / ');
const description = (lang === 'es' && descriptions[0]) ? descriptions[0] : descriptions[1] || descriptions[0];
return { icon: hardcodedInfo.icon, description: description };
}
// 3. Si no se encuentra en ninguna parte, devolver un valor por defecto.
const defaultDescription = lang === 'es' ? `Categoría no encontrada (${categoryKey})` : `Category not found (${categoryKey})`;
return { icon: '⚪', description: defaultDescription };
}//getCategoryDetails
// Función para eliminar diacríticos de una cadena
function removeDiacritics(str)
{
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}//removeDiacritics
// Función para validar una palabra excluida
// Modificación de isValidExcludedWord para hacer verificaciones más estrictas
function isValidExcludedWord(newWord) {
if (!newWord) // Si la palabra está vacía, no es válida
return { valid: false, msg: "La palabra no puede estar vacía." };
const lowerNewWord = newWord.toLowerCase(); // Convertir a minúsculas para comparaciones insensibles a mayúsculas
if (newWord.length === 1) // No permitir palabras de un solo caracter
return { valid: false, msg: "No se permite agregar palabras de un solo caracter." };
if (/[-']/.test(newWord)) // Permitir palabras con "-" o "'" sin separarlas
return { valid: true };
if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord)) // No permitir caracteres especiales solos
return { valid: false, msg: "No se permite agregar solo caracteres especiales." };
// VERIFICACIÓN MÁS ESTRICTA DEL DICCIONARIO
// Verificar tanto coincidencia exacta como insensible a mayúsculas/minúsculas
if (window.dictionaryWords) {
// Verificar si la palabra existe en el diccionario (insensible a mayúsculas/minúsculas)
const dictionaryHasWordInsensitive = Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord);
if (dictionaryHasWordInsensitive) {
return { valid: false, msg: "La palabra ya existe en el diccionario (sin considerar mayúsculas/minúsculas). No se puede agregar a especiales." };
}
// Verificar coincidencia exacta (esto ya está en código original)
const dictionaryHasWordExact = Array.from(window.dictionaryWords).some(w => w === newWord);
if (dictionaryHasWordExact) {
return { valid: false, msg: "La palabra (con esta capitalización exacta) ya existe en el diccionario. No se puede agregar a especiales." };
}
}
// Verificar si la palabra es una palabra común
if (commonWords.includes(lowerNewWord))
return { valid: false, msg: "Esa palabra es muy común y no debe agregarse a la lista." };
// Verificar si la palabra ya está en la lista de excluidas (tanto exacta como insensible a mayúsculas/minúsculas)
if (excludedWords)
{
// Verificar coincidencia exacta
if (excludedWords.has(newWord))
{
return { valid: false, msg: "La palabra (con esta capitalización exacta) ya está en la lista." };
}
// Verificar coincidencia insensible a mayúsculas/minúsculas
const firstChar = lowerNewWord.charAt(0);
const candidatesForFirstChar = excludedWordsMap.get(firstChar);
if (candidatesForFirstChar)
{
for (const existingWord of candidatesForFirstChar)
{
if (existingWord.toLowerCase() === lowerNewWord)
{
return { valid: false, msg: "Esta palabra ya existe en la lista (con diferente capitalización)." };
}
}
}
}
return { valid: true };
}
/*function isValidExcludedWord(newWord)
{
if (!newWord) // Si la palabra está vacía, no es válida
return { valid : false, msg : "La palabra no puede estar vacía." };
const lowerNewWord = newWord.toLowerCase(); // Convertir a minúsculas para comparaciones insensibles a mayúsculas
if (newWord.length === 1) // No permitir palabras de un solo caracter
return { valid: false, msg: "No se permite agregar palabras de un solo caracter." };
if (/[-']/.test(newWord)) // Permitir palabras con "-" o "'" sin separarlas
return { valid: true };
if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord)) // No permitir caracteres especiales solos
return { valid : false, msg : "No se permite agregar solo caracteres especiales." };
// La verificación del diccionario ahora será sensible a mayúsculas/minúsculas
// Si la palabra existe EXACTAMENTE como está en el diccionario, no permitirla como excluida.
// Si el diccionario tiene 'sos' y la excluida es 'SOS', SÍ se permitirá.
if (window.dictionaryWords && Array.from(window.dictionaryWords).some(w => w === newWord)) // Comparación sensible a mayúsculas/minúsculas
return { valid : false, msg :"La palabra (con esta capitalización exacta) ya existe en el diccionario. No se puede agregar a especiales." };
// Verificar si la palabra es una palabra común
if (commonWords.includes(lowerNewWord)) // No permitir palabras comunes
return { valid: false, msg: "Esa palabra es muy común y no debe agregarse a la lista." };
// Verificar si la palabra ya está en la lista de excluidas
if (excludedWords && Array.from(excludedWords).some(w => w === newWord)) // No permitir duplicados exactos en excluidas
return { valid : false, msg : "La palabra (con esta capitalización exacta) ya está en la lista." };
return { valid : true };
*///isValidExcludeWord
// La función removeEmoticons con una regex más segura o un paso extraremoveEmoticons solo para emojis (sin afectar números)
function removeEmoticons(text)
{
if (!text || typeof text !== 'string')
{
return '';
}
// Esta es una regex moderna y más robusta que utiliza propiedades de Unicode.
// \p{Emoji_Presentation}: Coincide con emojis que se muestran como imágenes por defecto (Ej: 😊, 🏨, 🚗).
// \p{Extended_Pictographic}: Coincide con un conjunto más amplio de símbolos que pueden ser emojis.
// El flag 'u' es CRUCIAL para que la sintaxis \p{...} funcione.
const emojiRegex = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
let cleanedText = text.replace(emojiRegex, '');
// Limpieza final de espacios extra que puedan quedar.
return cleanedText.trim().replace(/\s{2,}/g, ' ');
}// removeEmoticons
// Modify aplicarReemplazosGenerales
function aplicarReemplazosGenerales(name)
{
if (typeof window.skipGeneralReplacements === "boolean" && window.skipGeneralReplacements)
return name;
// Paso 1: Eliminar emoticones al inicio de los reemplazos generales.
name = removeEmoticons(name);
const reglas = [
// Nueva regla: reemplazar | por espacio, guion y espacio
{ buscar: /\|/g, reemplazar: " - " },
// Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor
{ buscar: /\s*\/\s*/g, reemplazar: " - " },
// Corrección: Para buscar [P] o [p] literalmente
{ buscar: /\[[Pp]\]/g, reemplazar: "" },
// 1. Convertir guiones pegados a palabras o con un solo espacio a ' - '
// Esto convierte "Palabra-Otra" -> "Palabra - Otra"
// y "Palabra -Otra" -> "Palabra - Otra"
// y "Palabra- Otra" -> "Palabra - Otra"
{ buscar: /(\p{L}|\p{N})\s*-\s*(\p{L}|\p{N})/gu, reemplazar: "$1 - $2" },
// 2. Limpiar guiones que no estén entre palabras, convirtiéndolos en un solo ' - '
// Esto estandariza " -- " a " - ", y " - " a " - ".
// Asegura que siempre haya un espacio en cada lado del guion si no es un guion interno.
{ buscar: /\s*-\s*/g, reemplazar: " - " },
{ buscar: /\s{2,}/g, reemplazar: ' ' }, // Asegura espacios únicos antes de trim
];
reglas.forEach(regla => { // Itera sobre cada regla de reemplazo
if (regla.buscar.source === '\\|') // Si la regla es para el carácter '|', usa replaceAll
name = name.replace(regla.buscar, regla.reemplazar);
else
name = name.replace(regla.buscar, regla.reemplazar);
});
name = name.replace(/\s{2,}/g, ' ').trim(); // Asegura el recorte final y espacios únicos
// ****** INICIO DE LA MODIFICACIÓN: Limpieza de guiones dobles ******
// Eliminar guiones dobles si están seguidos, reemplazándolos por un solo guion
// Maneja '--', ' - - ', etc. y los convierte a un solo ' - '
name = name.replace(/\s*-\s*-\s*/g, ' - ');
name = name.replace(/--/g, '-'); // También para guiones pegados 'a--b' -> 'a-b'
// ****** FIN DE LA MODIFICACIÓN ******
return name;
}// aplicarReemplazosGenerales
// función auxiliar para capitalizar cada palabra en una frase
function capitalizeEachWord(phrase)
{
if (!phrase || typeof phrase !== 'string') return "";
return phrase.split(/\s+/) // Dividir por uno o más espacios
.map((word, index) => { // Añadir index
if (word.length === 0) return "";
// La capitalización de palabras comunes y otras reglas complejas
// ya las maneja normalizeWordInternal. Aquí solo capitalizamos la primera letra
// si no es un caso especial.
// Llamar a normalizeWordInternal para asegurar consistencia
return normalizeWordInternal(word, index === 0, false); // Pasar isFirstWordInSequence
})
.join(' '); // Unir de nuevo con un solo espacio
}
//Permite aplicar reglas especiales de capitalización y puntuación a un nombre
function aplicarReglasEspecialesNombre(newName)
{
// Regla de capitalización después de GUION
newName = newName.replace(/-(\s*)([^\s]+)/g, (match, spaces, nextWord) =>
{
// Esta lógica reutiliza la función principal de normalización para la palabra que sigue al guion.
// Se pasa 'true' para isFirstWordInSequence, lo que le indica a la función que debe
// capitalizar palabras comunes como "de", "la", "el", etc.
const normalizedNextWord = normalizeWordInternal(nextWord, true, false);
return `-${spaces}${normalizedNextWord}`;
});
// Capitalizar Después De Punto
newName = newName.replace(/\.\s+([a-z])/g, (match, letter) => `. ${letter.toUpperCase()}`);
// Capitalizar Después De Paréntesis De Apertura
newName = newName.replace(/(\(\s*)([a-zA-Z])/g, (match, P1, P2) =>
{
return P1 + P2.toUpperCase();
});
// Asegura que la última letra de una cadena esté en mayúsculas si es una letra sola al final
newName = newName.replace(/\s([a-zA-Z])$/, (match, letter) => ` ${letter.toUpperCase()}`);
// Asegurarse de que no haya espacios dobles creados y trim final
newName = newName.replace(/\s{2,}/g, ' ').trim();
return newName;
}//aplicarReglasEspecialesNombre
// Permite normalizar un nombre de lugar
function processPlaceName(originalName)
{
//console.log(`[WME PLN] --- INICIANDO ANÁLISIS PARA: "${originalName}" ---`);
//console.log(`[WME PLN - processPlaceName] Recibido para normalizar: "${originalName}"`); // LOG INICIO
let processedName = originalName.trim();
// Primero, reemplazamos el pipe por un espacio, para que las palabras se separen correctamente.
// Hacemos esto ANTES de dividir en palabras para que normalizeWordInternal las vea por separado.
processedName = processedName.replace(/\|/g, ' - '); // Reemplaza | por un espacio
processedName = processedName.replace(/\s{2,}/g, ' ').trim(); // Limpia espacios dobles que puedan generarse
//console.log(`[WME PLN - processPlaceName] Después de reemplazo de pipe: "${processedName}"`); // LOG PIPE REEMPLAZADO
// Si el nombre está vacío después de los reemplazos, no hacemos nada más.
const words = processedName.split(/\s+/).filter(word => word.length > 0);
//console.log(`[WME PLN - processPlaceName] Palabras extraídas:`, words); // LOG PALABRAS EXTRAÍDAS
// PASO 1: Normalización palabra por palabra (capitalización, reglas especiales)
const normalizedWords = words.map((word, index) => {
if (word === '-') return '-';
const excl = isExcludedWord(word);
if (excl)
{
if (excl.endsWith('-') && excl.replace(/-+$/, '').length > 0) {
return excl.replace(/-+$/, '');
}
return excl;
}
const lower = (word || '').toLowerCase();
if (commonWords.includes(lower))
{
// minúscula, salvo si es la 1ª palabra o si viene justo después de "-" o "("
const prevIsHyphen = index > 0 && words[index - 1] === '-';
const prevIsOpenParen = index > 0 && words[index - 1] === '(';
if (index === 0 || prevIsHyphen || prevIsOpenParen) {
return lower.charAt(0).toUpperCase() + lower.slice(1);
}
return lower;
}
return normalizeWordInternal(word, index === 0, false);
});
processedName = normalizedWords.join(" ");
//console.log(`[WME PLN] [Paso 1] Después de normalizar cada palabra: "${processedName}"`); // LOG PALABRAS NORMALIZADAS
//console.log(`[WME PLN] [Paso 1] Después de normalizar cada palabra: "${processedName}"`);
// PASO 2: Aplicar reglas especiales de nombre (capitalización después de guion, etc.)
// Aquí es donde `aplicarReglasEspecialesNombre` manejará el guion y capitalizará "Bolívar".
processedName = aplicarReglasEspecialesNombre(processedName);
//console.log(`[WME PLN] [Paso 2] Después de aplicar reglas especiales: "${processedName}"`);
// PASO 3: Procesar comillas y paréntesis
processedName = postProcessQuotesAndParentheses(processedName);
//console.log(`[WME PLN] [Paso 3] Después de procesar comillas/paréntesis: "${processedName}"`);
// PASO 4: Aplicar reemplazos definidos por el usuario
if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0)
{
//console.log("[WME PLN] [Paso 4] ANTES de aplicarReemplazosDefinidos:", processedName, replacementWords);
processedName = aplicarReemplazosDefinidos(processedName, replacementWords);
//console.log("[WME PLN] [Paso 4]DESPUÉS de aplicarReemplazosDefinidos:", processedName);
}
//console.log(`[WME PLN] [Paso 4] DESPUÉS de aplicar reemplazos: "${processedName}"`);
// PASO 5: Aplicar reemplazos generales (barras, corchetes, etc.)
// La regla para el pipe '|' en `aplicarReemplazosGenerales` ahora es redundante si solo se busca el pipe,
// pero es inofensiva si ya lo reemplazamos. Asegúrate de que no haya otras reglas en aplicarReemplazosGenerales
// que interfieran negativamente.
processedName = aplicarReemplazosGenerales(processedName); // Esto ya no afectaría el `|`
//console.log(`[WME PLN] [Paso 5] Después de aplicar reemplazos generales: "${processedName}"`);
//console.log(`[WME PLN] [Paso 6] Después de corregir tildes: "${processedName}"`);
// ******************************************************************************
// PASO FINAL: Mover palabras según la configuración de swap (inicio o final)
processedName = applySwapMovement(processedName);
//console.log(`[WME PLN] [Paso 7] Después de applyWordsToStartMovement: "${processedName}"`);
let finalName = processedName.replace(/\s{2,}/g, ' ').trim();
// Regla para eliminar un guion (-) si está al final del nombre, posiblemente con espacios.
// Ejemplo: "Tiendas D1 - " -> "Tiendas D1"
// Ejemplo: "Tiendas D1-" -> "Tiendas D1"
// Ejemplo: "Tiendas D1- " -> "Tiendas D1"
finalName = finalName.replace(/\s*-\s*$/, ''); // Elimina '-' con espacios a su alrededor si está al final
// Quitar el punto final si existe.
if (finalName.endsWith('.'))
{
finalName = finalName.slice(0, -1);
}
//console.log(`[WME PLN - pPN] Resultado final de pPN: "${finalName}" y processedName: "${processedName}"`); // LOG FINAL
return finalName;
}// processPlaceName
// Permite minimizar el panel de estadísticas
function minimizeStatsPanel()
{
if (statsPanelElement)
{
statsPanelElement.style.display = 'block';
statsPanelElement.style.width = '120px';
statsPanelElement.style.height = '28px';
statsPanelElement.style.overflow = 'hidden';
statsPanelElement.style.left = '8px';
statsPanelElement.style.bottom = '8px';
statsPanelElement.style.cursor = 'pointer';
statsPanelElement.querySelector('#stats-summary-view').style.display = 'flex';
statsPanelElement.querySelector('#stats-detail-view').style.display = 'none';
}
}// minimizeStatsPanel
// Permite maximizar el panel de estadísticas
function maximizeStatsPanel()
{
if (statsPanelElement)
{
statsPanelElement.style.display = 'block';
statsPanelElement.style.width = '';
statsPanelElement.style.height = '';
statsPanelElement.style.overflow = '';
statsPanelElement.style.left = '23%';
statsPanelElement.style.bottom = '60px';
statsPanelElement.style.cursor = 'pointer';
statsPanelElement.querySelector('#stats-summary-view').style.display = 'flex';
}
}// maximizeStatsPanel
//Permite normalizar una palabra individual
// Versión MODIFICADA
// FUNCIÓN MODIFICADA
function updateApplyButtonState(row, originalName)
{
// Encontrar los elementos necesarios dentro de la fila
const inputReplacement = row.querySelector('.replacement-input'); // Usaremos una clase para identificarlo
const applyButton = row.querySelector('button[title="Aplicar sugerencia"]');
const applyButtonWrapper = applyButton?.parentElement;
if (!inputReplacement || !applyButton || !applyButtonWrapper) return;
const nameIsDifferent = inputReplacement.value.trim() !== originalName.trim();
const categoryWasChanged = row.dataset.categoryChanged === 'true';
const addressWasChanged = row.dataset.addressChanged === 'true'; // <-- NUEVA COMPROBACIÓN
// Si el nombre, la categoría O la dirección han cambiado, habilitar el botón
if (nameIsDifferent || categoryWasChanged || addressWasChanged)
{
// Habilitar botón
applyButton.disabled = false;
applyButton.style.opacity = "1";
// Quitar el chulo verde de éxito si existe
const successIcon = applyButtonWrapper.querySelector('span');
if (successIcon)
{
successIcon.remove();
}
}
else
{
// Deshabilitar botón
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
}
}// updateApplyButtonState
//Permite aplicar reemplazos definidos por el usuario a un texto
function aplicarReemplazosDefinidos(text, replacementRules)
{
let newText = text;
if (typeof replacementRules !== 'object' || replacementRules === null || Object.keys(replacementRules).length === 0)
{
return newText;
}
const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);
for (const fromKey of sortedFromKeys)
{
const toValue = replacementRules[fromKey];
// CORRECCIÓN: Asegurar que fromKey es una string antes de pasarla a escapeRegExp
const escapedFromKey = escapeRegExp(String(fromKey));
let regex;
const wordCharSet = '[\\p{L}\\p{N}_-]';
if (toValue.endsWith(' -'))
{
regex = new RegExp(`(^|[^\\p{L}\\p{N}_\\-])(${escapedFromKey})(\\s+)(${wordCharSet}+)?(?=$|[^\\p{L}\\p{N}_-])`, 'giu');
}
else
{
regex = new RegExp(`(^|[^\\p{L}\\p{N}_-])(${escapedFromKey})(?=$|[^\\p{L}\\p{N}_-])`, 'giu');
}
// CORRECCIÓN CLAVE: Usar la sintaxis '...args' para capturar todos los argumentos
// y luego extraerlos de forma robusta.
newText = newText.replace(regex, (match, ...args) =>
{
// El último argumento de `args` es la cadena original completa.
// El penúltimo argumento de `args` es el offset.
const originalString = args[args.length - 1]; // Captura el string original
const offset = args[args.length - 2]; // Captura el offset
// Los grupos de captura vienen antes del offset y originalString.
// Reasignar los grupos de captura según el tipo de regex.
let delimitadorPrevio, matchedFromKey, capturedSpaces, nextWordIfCaptured;
if (toValue.endsWith(' -')) {
// Para la regex con 4 grupos de captura
delimitadorPrevio = args[0]; // p1
matchedFromKey = args[1]; // p2
capturedSpaces = args[2]; // p3
nextWordIfCaptured = args[3]; // p4
} else {
// Para la regex con 2 grupos de captura
delimitadorPrevio = args[0]; // p1
matchedFromKey = args[1]; // p2
// Los demás serán undefined si se intenta acceder a ellos, lo cual es correcto.
}
const offsetOfMatchInCurrentText = offset;
const stringBeingProcessedActual = originalString; // Ya es la cadena correcta
// --- Lógica Anti-Duplicación de palabra anterior ---
const textoAntesDelMatch = stringBeingProcessedActual.substring(0, offsetOfMatchInCurrentText + delimitadorPrevio.length);
const palabrasAntes = textoAntesDelMatch.trim().split(/\s+/);
const ultimaPalabraAntes = palabrasAntes.length > 0 ? palabrasAntes[palabrasAntes.length - 1] : "";
const palabrasDelReemplazo = toValue.trim().split(/\s+/);
const primeraPalabraReemplazo = palabrasDelReemplazo.length > 0 ? palabrasDelReemplazo[0] : "";
if (ultimaPalabraAntes && primeraPalabraReemplazo)
{
const semejanza = calculateSimilarity(ultimaPalabraAntes, primeraPalabraReemplazo);
if (semejanza > 0.9)
{
return match;
}
}
// --- Lógica para evitar auto-reemplazo infinito (ej: Terpel -> Terpel -) ---
if (toValue.toLowerCase().startsWith(fromKey.toLowerCase()) && toValue.length > fromKey.length)
{
const suffix = toValue.substring(fromKey.length);
const textAfterMatchRaw = stringBeingProcessedActual.substring(offsetOfMatchInCurrentText + match.length);
const textAfterMatchTrimmed = textAfterMatchRaw.trim();
if (textAfterMatchTrimmed.startsWith(suffix.trim()))
{
return match;
}
}
// --- Lógica específica para el reemplazo que termina en ' -' ---
if (toValue.endsWith(' -'))
{
return delimitadorPrevio + toValue + (nextWordIfCaptured || '');
}
// --- Para otros reemplazos que no terminan en ' -' ---
return delimitadorPrevio + toValue;
});
}
return newText;
}//aplicarReemplazosDefinidos
//Permite crear un panel flotante en WME
function getVisiblePlaces()
{
if (typeof W === 'undefined' || !W.map || !W.model || !W.model.venues)
{// Si Waze Map Editor no está completamente cargado, retornar un array vacío
console.warn('[WME_PLN][WARNING] Waze Map Editor no está completamente cargado.');
return [];
}
// Obtener los lugares visibles en el mapa
const venues = W.model.venues.objects;
const visiblePlaces = Object.values(venues).filter(venue => { // Filtrar los lugares que están visibles en el mapa
const olGeometry = venue.getOLGeometry?.();// Obtener la geometría del lugar
const bounds = olGeometry?.getBounds?.(); // Obtener los límites del lugar
return bounds && W.map.getExtent().intersectsBounds(bounds);
});
return visiblePlaces;
}// getVisiblePlaces
// Devuelve la lista de places sin los excluidos y el total omitido.
function filterOutExcludedPlaces(placesList)
{
if (!Array.isArray(placesList) || placesList.length === 0)
return { filtered: [], excludedCount: 0 };
const filtered = [];
let excludedCount = 0;
for (const place of placesList)
{
try
{
const placeId = place?.getID?.();
if (placeId && excludedPlaces.has(placeId))
{
excludedCount++;
continue;
}
}
catch (err)
{
console.warn('[WME PLN] Error comprobando lugar excluido:', err);
}
filtered.push(place);
}
return { filtered, excludedCount };
}// filterOutExcludedPlaces
//Permite renderizar los lugares en el panel flotante
function renderPlacesInFloatingPanel(places, scanStats = {})
{
// Limpiar la lista global de duplicados antes de llenarla de nuevo
placesForDuplicateCheckGlobal.length = 0;
createFloatingPanel("processing"); // Mostrar panel en modo "procesando"
const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10); //Obtiene el número total de lugares a procesar
const totalVisibleProvided = typeof scanStats.totalVisibleCount === 'number' ? scanStats.totalVisibleCount : null;
const excludedProvided = typeof scanStats.excludedCount === 'number' ? scanStats.excludedCount : null;
const skipExcludedFiltering = scanStats.skipExcludedFiltering === true;
let workingPlaces = Array.isArray(places) ? [...places] : [];
const initialVisiblePlacesCount = totalVisibleProvided ?? workingPlaces.length;
let excludedByInitialFilterCount = excludedProvided ?? 0;
if (!skipExcludedFiltering)
{
const { filtered, excludedCount } = filterOutExcludedPlaces(workingPlaces);
workingPlaces = filtered;
if (excludedProvided === null)
excludedByInitialFilterCount = excludedCount;
}
else if (excludedProvided === null)
{
excludedByInitialFilterCount = Math.max(0, initialVisiblePlacesCount - workingPlaces.length);
}
const filteredPlacesCount = workingPlaces.length;
const limitedPlacesCount = Math.min(filteredPlacesCount, maxPlacesToScan);
if (filteredPlacesCount > maxPlacesToScan) // Limitar el número de lugares a escanear
workingPlaces = workingPlaces.slice(0, maxPlacesToScan); // Limitar el número de places a escanear
places = workingPlaces;
const lockRankEmojis = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣"]; // Definir los emojis de nivel de bloqueo
// Permite obtener el nombre de la categoría de un lugar, ya sea del modelo antiguo o del SDK
function getPlaceCategoryName(venueFromOldModel, venueSDKObject)
{ // Acepta ambos tipos de venue
let categoryId = null;
let categoryName = null;
// Intento 1: Usar el venueSDKObject si está disponible y tiene la info
if (venueSDKObject)
{
if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id)
{// Si venueSDKObject tiene mainCategory con ID
categoryId = venueSDKObject.mainCategory.id; // source = "SDK (mainCategory.id)";
//Limpiar comillas aquí
if (typeof categoryId === 'string') categoryId = categoryId.replace(/'/g, '');
if (venueSDKObject.mainCategory.name) // Si mainCategory tiene nombre
categoryName = venueSDKObject.mainCategory.name;// source = "SDK (mainCategory.name)";
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
else if (Array.isArray(venueSDKObject.categories) && venueSDKObject.categories.length > 0)
{// Si venueSDKObject tiene un array de categorías y al menos una categoría
const firstCategorySDK = venueSDKObject.categories[0]; // source = "SDK (categories[0])";
if (typeof firstCategorySDK === 'object' && firstCategorySDK.id)
{// Si la primera categoría es un objeto con ID
categoryId = firstCategorySDK.id;
// Limpiar comillas aquí
if (typeof categoryId === 'string') categoryId = categoryId.replace(/'/g, '');
if (firstCategorySDK.name) // Si la primera categoría tiene nombre
categoryName = firstCategorySDK.name;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
else if (typeof firstCategorySDK === 'string') // Si la primera categoría es una cadena (nombre de categoría)
{
categoryName = firstCategorySDK;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
}
else if (venueSDKObject.primaryCategoryID)
{
categoryId = venueSDKObject.primaryCategoryID;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
}
if (categoryName)
{// Si se obtuvo el nombre de categoría del SDK
return categoryName;
}
// Intento 2: Usar W.model si no se obtuvo del SDK
if (!categoryId && venueFromOldModel && venueFromOldModel.attributes && Array.isArray(venueFromOldModel.attributes.categories) && venueFromOldModel.attributes.categories.length > 0)
categoryId = venueFromOldModel.attributes.categories[0];
if (!categoryId)// Si no se pudo obtener el ID de categoría de ninguna fuente
return "Sin categoría";
let categoryObjWModel = null; // Intentar obtener el objeto de categoría del modelo Waze
if (typeof W !== 'undefined' && W.model)
{// Si Waze Map Editor está disponible
if (W.model.venueCategories && typeof W.model.venueCategories.getObjectById === "function") // Si venueCategories está disponible en W.model
categoryObjWModel = W.model.venueCategories.getObjectById(categoryId);
if (!categoryObjWModel && W.model.categories && typeof W.model.categories.getObjectById === "function") // Si no se encontró en venueCategories, intentar en categories
categoryObjWModel = W.model.categories.getObjectById(categoryId);
}
if (categoryObjWModel && categoryObjWModel.attributes && categoryObjWModel.attributes.name)
{// Si se encontró el objeto de categoría en W.model
let nameToReturn = categoryObjWModel.attributes.name;
// Limpiar comillas aquí
if (typeof nameToReturn === 'string') nameToReturn = nameToReturn.replace(/'/g, '');
return nameToReturn;
}
if (typeof categoryId === 'number' || (typeof categoryId === 'string' && categoryId.trim() !== ''))
{// Si no se pudo obtener el nombre de categoría de ninguna fuente, devolver el ID
return `${categoryId}`; // Devuelve el ID si no se encuentra el nombre.
}
return "Sin categoría";
}//getPlaceCategoryName
//Permite obtener el tipo de lugar (área o punto) y su icono
function getPlaceTypeInfo(venueSDKObject) // <--- AHORA RECIBE venueSDKObject
{
let isArea = false;
let icon = "⊙"; // Icono por defecto para punto
let title = "Punto"; // Título por defecto para punto
if (venueSDKObject && venueSDKObject.geometry && venueSDKObject.geometry.type)
{
const geometryType = venueSDKObject.geometry.type;
if (geometryType === 'Polygon' || geometryType === 'MultiPolygon')
{
isArea = true;
icon = "⭔"; // Icono para área
title = "Área"; // Título para área
}
// Para otros tipos como 'Point', 'LineString', etc., se mantienen los valores por defecto (Punto).
}
return { isArea, icon, title };
}// getPlaceTypeInfo
//Permite procesar un lugar y generar un objeto con sus detalles
function shouldForceSuggestionForReview(word)
{
if (typeof word !== 'string') // Si la palabra no es una cadena, no forzar sugerencia por esta regla
return false;
const lowerWord = word.toLowerCase(); // Convertir la palabra a minúsculas para evitar problemas de mayúsculas/minúsculas
const hasTilde = /[áéíóúÁÉÍÓÚ]/.test(word); // Verificar si la palabra tiene alguna tilde (incluyendo mayúsculas acentuadas)
if (!hasTilde) // Si no tiene tilde, no forzar sugerencia por esta regla
return false; // Si no hay tilde, no forzar sugerencia por esta regla
const problematicSubstrings = ['c', 's', 'x', 'cc', 'sc', 'cs', 'g', 'j', 'z','ñ']; // Lista de patrones de letras/combinaciones que, junto con una tilde, fuerzan la sugerencia (insensible a mayúsculas debido a lowerWord)
for (const sub of problematicSubstrings)
{// Verificar si la palabra contiene alguna de las letras/combinaciones problemáticas
if (lowerWord.includes(sub))
return true; // Tiene tilde y una de las letras/combinaciones problemáticas
}
return false; // Tiene tilde, pero no una de las letras/combinaciones problemáticas
}//shouldForceSuggestionForReview
// Procesa un lugar y genera un objeto con sus detalles
async function getPlaceCityInfo(venueFromOldeModel, venueSDKObject) {
let cityName = null;
let source = "desconocida";
// --- Función auxiliar para obtener el nombre de la ciudad desde un cityID ---
function getCityNameFromId(cityId) {
if (!cityId || typeof W === 'undefined' || !W.model || !W.model.cities || !W.model.cities.getObjectById) {
return null;
}
const cityObject = W.model.cities.getObjectById(cityId);
if (cityObject && cityObject.attributes && typeof cityObject.attributes.name === 'string' && cityObject.attributes.name.trim() !== '') {
return cityObject.attributes.name.trim();
}
return null;
}
try {
// --- INTENTO 1: OBTENER CIUDAD A TRAVÉS DEL streetID (EL MÉTODO MÁS COMÚN) ---
const streetID = venueFromOldeModel?.attributes?.streetID || venueSDKObject?.address?.street?.id || null;
if (streetID) {
const streetObject = W.model.streets.getObjectById(streetID);
if (streetObject && streetObject.attributes && streetObject.attributes.cityID) {
cityName = getCityNameFromId(streetObject.attributes.cityID);
if (cityName) {
source = "derivada de la calle";
}
}
}
// --- INTENTO 2: OBTENER CIUDAD DESDE EL cityID DIRECTO (FALLBACK) ---
if (!cityName) {
const cityID = venueFromOldeModel?.attributes?.cityID || venueSDKObject?.address?.city?.id || null;
if (cityID) {
cityName = getCityNameFromId(cityID);
if (cityName) {
source = "explícita";
}
}
}
} catch (e) {
console.error("[WME PLN] Error crítico en getPlaceCityInfo:", e);
cityName = null;
}
// --- Lógica final para devolver el ícono y el título ---
if (cityName) {
return {
icon: '✅',
title: `Ciudad: ${cityName} (${source})`,
hasCity: true
};
} else {
return {
icon: '🚩',
title: "Sin ciudad asignada. Haz clic para buscar y asignar la más cercana.",
hasCity: false
};
}
}// getPlaceCityInfo
/*async function getPlaceCityInfo(venueFromOldModel, venueSDKObject)
{
let hasExplicitCity = false; // Indica si hay una ciudad explícita definida
let explicitCityName = null; // Nombre de la ciudad explícita, si se encuentra
let hasStreetInfo = false; // Indica si hay información de calle disponible
let cityAssociatedWithStreet = null; // Nombre de la ciudad asociada a la calle, si se encuentra
// 1. Check for EXPLICIT city SDK
if (venueSDKObject && venueSDKObject.address) {
//console.log("[DEBUG] venueSDKObject.address:", venueSDKObject.address);
if (venueSDKObject.address.city && typeof venueSDKObject.address.city.name === 'string' && venueSDKObject.address.city.name.trim() !== '') {
// Si hay una ciudad explícita en el SDK
explicitCityName = venueSDKObject.address.city.name.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "SDK (address.city.name)";
//console.log("[DEBUG] Ciudad explícita encontrada en SDK (address.city.name):", explicitCityName);
} else if (typeof venueSDKObject.address.cityName === 'string' && venueSDKObject.address.cityName.trim() !== '') {
// Si hay una ciudad explícita en el SDK (cityName)
explicitCityName = venueSDKObject.address.cityName.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "SDK (address.cityName)";
//console.log("[DEBUG] Ciudad explícita encontrada en SDK (address.cityName):", explicitCityName);
} else {
//console.log("[DEBUG] No se encontró ciudad explícita en SDK.");
}
}
if (!hasExplicitCity && venueFromOldModel && venueFromOldModel.attributes)
{
//console.log("[DEBUG] venueFromOldModel.attributes:", venueFromOldModel.attributes);
const cityID = venueFromOldModel.attributes.cityID;
//console.log("[DEBUG] cityID del modelo antiguo:", cityID);
if (cityID && typeof W !== 'undefined' && W.model && W.model.cities && W.model.cities.getObjectById)
{
//console.log("[DEBUG] Intentando obtener el objeto de ciudad con cityID:", cityID);
const cityObject = W.model.cities.getObjectById(cityID); // Obtener el objeto de ciudad del modelo Waze
//console.log("[DEBUG] cityObject obtenido:", cityObject);
if (cityObject && cityObject.attributes && typeof cityObject.attributes.name === 'string' && cityObject.attributes.name.trim() !== '') {
// Si el objeto de ciudad tiene un nombre válido
explicitCityName = cityObject.attributes.name.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "W.model.cities (cityID)";
//console.log("[DEBUG] Ciudad explícita encontrada en modelo antiguo (cityID):", explicitCityName);
} else {
//console.log("[DEBUG] cityObject no tiene un nombre válido.");
}
}
else
{
//console.log("[DEBUG] cityID no válido o W.model.cities.getObjectById no disponible.");
}
}
// 2. Check for STREET information (and any city derived from it) // SDK street check
if (venueSDKObject && venueSDKObject.address)
if ((venueSDKObject.address.street && typeof venueSDKObject.address.street.name === 'string' && venueSDKObject.address.street.name.trim() !== '') ||
(typeof venueSDKObject.address.streetName === 'string' && venueSDKObject.address.streetName.trim() !== ''))
hasStreetInfo = true; // source = "SDK (address.street.name or streetName)";
if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.streetID)
{// Old Model street check (if not found via SDK or to supplement)
hasStreetInfo = true; // Street ID exists in old model
const streetID = venueFromOldModel.attributes.streetID; // Obtener el streetID del modelo antiguo
if (typeof W !== 'undefined' && W.model && W.model.streets && W.model.streets.getObjectById)
{// Si hay un streetID en el modelo antiguo
const streetObject = W.model.streets.getObjectById(streetID); // Obtener el objeto de calle del modelo Waze
if (streetObject && streetObject.attributes && streetObject.attributes.cityID)
{// Si el objeto de calle tiene un cityID asociado
const cityIDFromStreet = streetObject.attributes.cityID;// Obtener el cityID de la calle
if (W.model.cities && W.model.cities.getObjectById)
{// Si W.model.cities está disponible y tiene el método getObjectById
const cityObjectFromStreet = W.model.cities.getObjectById(cityIDFromStreet);// Obtener el objeto de ciudad asociado a la calle
// Si el objeto de ciudad tiene un nombre válido
if (cityObjectFromStreet && cityObjectFromStreet.attributes && typeof cityObjectFromStreet.attributes.name === 'string' && cityObjectFromStreet.attributes.name.trim() !== '')
cityAssociatedWithStreet = cityObjectFromStreet.attributes.name.trim(); // Nombre de la ciudad asociada a la calle
}
}
}
}
// --- 3. Determine icon, title, and returned hasCity based on user's specified logic ---
let icon;
let title;
const returnedHasCityBoolean = hasExplicitCity; // To be returned, indicates if an *explicit* city is set.
const hasAnyAddressInfo = hasExplicitCity || hasStreetInfo; // Determina si hay alguna información de dirección (ciudad explícita o calle).
if (hasAnyAddressInfo)
{// Si hay información de dirección (ciudad explícita o calle)
if (hasExplicitCity)
{
// Tiene ciudad explícita
icon = "🏙️";
title = `Ciudad: ${explicitCityName}`;
}
else if (cityAssociatedWithStreet)
{
// No tiene ciudad explícita, pero la calle sí está asociada a ciudad
icon = "🏙️";
title = `Ciudad (por calle): ${cityAssociatedWithStreet}`;
}
else {
// No hay ciudad explícita ni ciudad por calle
icon = "🚫";
title = "Sin ciudad asignada";
}
return {
icon: icon || "❓",
title: title || "Info no disponible",
hasCity: (hasExplicitCity || !!cityAssociatedWithStreet) // Ahora true si tiene ciudad por calle
};
}
else
{ // No tiene ni ciudad explícita ni información de calle
icon = "🚫";
title = "El campo dirección posee inconsistencias"; // Título para "no tiene ciudad ni calle"
}
return {
icon: icon || "❓", // Usar '?' si icon es undefined/null/empty
title: title || "Info no disponible", // Usar "Info no disponible" si title es undefined/null/empty
hasCity: returnedHasCityBoolean || false // Asegurarse de que sea un booleano
};
}*///getPlaceCityInfo
//Renderizar barra de progreso en el TAB PRINCIPAL justo después del slice
const tabOutput = document.querySelector("#wme-normalization-tab-output");
if (tabOutput)
{// Si el tab de salida ya existe, limpiar su contenido
// Reiniciar el estilo del mensaje en el tab al valor predeterminado
tabOutput.style.color = "#000";
tabOutput.style.fontWeight = "normal";
// Crear barra de progreso visual
const progressBarWrapperTab = document.createElement("div");
progressBarWrapperTab.style.margin = "10px 0";
progressBarWrapperTab.style.marginTop = "10px";
progressBarWrapperTab.style.height = "18px";
progressBarWrapperTab.style.backgroundColor = "transparent";
// Crear el contenedor de la barra de progreso
const progressBarTab = document.createElement("div");
progressBarTab.style.height = "100%";
progressBarTab.style.width = "0%";
progressBarTab.style.backgroundColor = "#007bff";
progressBarTab.style.transition = "width 0.2s";
progressBarTab.id = "progressBarInnerTab";
progressBarWrapperTab.appendChild(progressBarTab);
// Crear texto de progreso
const progressTextTab = document.createElement("div");
progressTextTab.style.fontSize = "12px";
progressTextTab.style.marginTop = "5px";
progressTextTab.id = "progressBarTextTab";
tabOutput.appendChild(progressBarWrapperTab);
tabOutput.appendChild(progressTextTab);
}
// Asegurar que la barra de progreso en el tab se actualice desde el principio
const progressBarInnerTab = document.getElementById("progressBarInnerTab"); // Obtener la barra de progreso del tab
const progressBarTextTab = document.getElementById("progressBarTextTab"); // Obtener el texto de progreso del tab
if (progressBarInnerTab && progressBarTextTab)
{// Si ambos elementos existen, reiniciar su estado
progressBarInnerTab.style.width = "0%";
progressBarTextTab.textContent = `Progreso: 0% (0/${places.length})`; // Reiniciar el texto de progreso
}
// --- PANEL FLOTANTE: limpiar y preparar salida ---
const output = document.querySelector("#wme-place-inspector-output");//
if (!output)
{// Si el panel flotante no está disponible, mostrar un mensaje de error
console.error("[WME_PLN][ERROR]❌ Panel flotante no está disponible");
return;
}
output.innerHTML = ""; // Limpia completamente el contenido del panel flotante
if (window.WME_PLN_SDK && window.WME_PLN_SDK.UI && typeof window.WME_PLN_SDK.UI.createElement === 'function') {
// Usar el SDK de WME para crear el div de procesamiento
const sdkDiv = window.WME_PLN_SDK.UI.createElement('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '10px'
}
});
const spinner = window.WME_PLN_SDK.UI.createElement('span', {
className: 'loader-spinner',
style: {
width: '16px',
height: '16px',
border: '2px solid #ccc',
borderTop: '2px solid #007bff',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite'
}
});
const infoDiv = window.WME_PLN_SDK.UI.createElement('div');
const processingText = window.WME_PLN_SDK.UI.createElement('div', {
id: 'processingText'
});
processingText.textContent = 'Procesando lugares visibles';
const dotsSpan = window.WME_PLN_SDK.UI.createElement('span', { className: 'dots' });
dotsSpan.textContent = '.';
processingText.appendChild(dotsSpan);
const processingStep = window.WME_PLN_SDK.UI.createElement('div', {
id: 'processingStep',
style: {
fontSize: '13px',
color: '#555'
}
});
processingStep.textContent = 'Inicializando escaneo...';
infoDiv.appendChild(processingText);
infoDiv.appendChild(processingStep);
sdkDiv.appendChild(spinner);
sdkDiv.appendChild(infoDiv);
output.appendChild(sdkDiv);
} else {
// Fallback al HTML tradicional si el SDK no está disponible
output.innerHTML = "<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:13px; color:#555;'>Inicializando escaneo...</div></div></div>";
}
//output.innerHTML = "<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:13px; color:#555;'>Inicializando escaneo...</div></div></div>";
// Asegurar que el panel flotante tenga un alto mínimo
const processingStepLabel = document.getElementById("processingStep");
if (processingStepLabel)
{
const infoParts = [`Visibles: ${initialVisiblePlacesCount}`];
// Agregar información adicional si está disponible
if (excludedByInitialFilterCount > 0)
infoParts.push(`Excluidos que serán omitidos: ${excludedByInitialFilterCount}`);
infoParts.push(`Restantes tras excluir: ${filteredPlacesCount}`);
infoParts.push(`Se analizarán: ${limitedPlacesCount}`);
processingStepLabel.textContent = infoParts.join(' · ');
}
// Animación de puntos suspensivos
const dotsSpan = output.querySelector(".dots");
if (dotsSpan)
{// Si el span de puntos existe, iniciar la animación de puntos
const dotStates = ["", ".", "..", "..."];
let dotIndex = 0;
window.processingDotsInterval = setInterval(() => {dotIndex = (dotIndex + 1) % dotStates.length;
dotsSpan.textContent = dotStates[dotIndex];}, 500);
}
output.style.height = "calc(55vh - 40px)";
if (!places.length)
{// Si no hay places, mostrar mensaje y salir
output.appendChild(document.createTextNode("No hay places visibles para analizar."));
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)// Si ya existe un overlay de escaneo, removerlo
existingOverlay.remove();
return;
}
// Procesamiento incremental para evitar congelamiento
let inconsistents = []; // Array para almacenar inconsistencias encontradas
let index = 0; // Índice para iterar sobre los lugares
const scanBtn = document.querySelector("button[type='button']"); // Remover ícono de ✔ previo si existe
if (scanBtn)
{// Si el botón de escaneo existe, remover el ícono de ✔ previo si está presente
const existingCheck = scanBtn.querySelector("span");
if (existingCheck) // Si hay un span dentro del botón, removerlo
existingCheck.remove();
}
// --- Sugerencias por palabra global para toda la ejecución ---
let sugerenciasPorPalabra = {};
// Convertir excludedWords a array solo una vez al inicio del análisis, seguro ante undefined
const excludedArray = (typeof excludedWords !== "undefined" && Array.isArray(excludedWords)) ? excludedWords : (typeof excludedWords !== "undefined" ? Array.from(excludedWords) : []);
async function processNextPlace()
{
// ID del lugar actual que se está procesando
const currentPlaceForLog = places[index];
const currentVenueId = currentPlaceForLog ? currentPlaceForLog.getID() : 'ID Desconocido';
//console.log(`\n[WME PLN - processNextPlace] --- INICIANDO PROCESAMIENTO PARA LUGAR ID: ${currentVenueId} (Índice: ${index}) ---`); // <--- USAR currentVenueId
//console.log(`[WME PLN - processNextPlace] Total de lugares a procesar: ${places.length}`);
// Inicialización de variables de estado
let cityInfo = {
icon: "❓",
title: "Información de ciudad no disponible",
hasCity: false
};
let resolvedEditorName = "Desconocido";
let lastEditorIdForComparison = null;
let currentLoggedInUserId = currentGlobalUserInfo.id;
let wasEditedByMe = false;
let shouldSkipThisPlace = false;
let skipReasonLog = "";
// Declaración de avoidMyEdits, typeInfo, areaMeters al inicio
const avoidMyEdits = document.getElementById("chk-avoid-my-edits")?.checked ?? false; // <-- MOVIDO AQUÍ
let typeInfo = { isArea: false, icon: "⊙", title: "Punto" };
let areaMeters = null;
// --- Obtener venueSDK lo antes posible ---
let venueSDK = null;
if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById)
try
{
venueSDK = await wmeSDK.DataModel.Venues.getById({ venueId: currentVenueId });
}
catch (sdkError)
{
console.error(`[WME_PLN] Error al obtener venueSDK para ID ${currentVenueId}:`, sdkError);
}
let originalNameRaw;
if (venueSDK && venueSDK.name)
originalNameRaw = venueSDK.name;
else
originalNameRaw = currentPlaceForLog && currentPlaceForLog.attributes ? (currentPlaceForLog.attributes.name?.value || currentPlaceForLog.attributes.name || '') : '';
originalNameRaw = originalNameRaw.trim();
const nameForProcessing = removeEmoticons(originalNameRaw);
// Asegurarse de que typeInfo y areaMeters se obtengan si venueSDK está disponible
if (venueSDK) {
typeInfo = getPlaceTypeInfo(venueSDK);
areaMeters = calculateAreaMeters(venueSDK);
}
//console.log(`[WME PLN - processNextPlace] Nombre Original Raw (de Waze/SDK): "${originalNameRaw}"`);
//console.log(`[WME PLN - processNextPlace] Nombre para Procesamiento (sin emojis): "${nameForProcessing}"`);
//console.log(`[WME PLN - DEBUG] Place ID: ${currentVenueId}, Name: "${originalNameRaw}"`);
//console.log(`[WME PLN - DEBUG] -> typeInfo:`, typeInfo);
//console.log(`[WME PLN - DEBUG] -> areaMeters:`, areaMeters);
//Obtener el ID del usuario actual (si está disponible globalmente de forma confiable)
const useFullPipeline = true; // Siempre usar el pipeline completo para este flujo
const applyGeneralReplacements = useFullPipeline || (document.getElementById("chk-general-replacements")?.checked ?? true); // Aplicar reemplazos generales por defecto
const checkExcludedWords = useFullPipeline || (document.getElementById("chk-check-excluded")?.checked ?? false); // Verificar palabras excluidas por defecto
const checkDictionaryWords = true;// Siempre verificar palabras del diccionario para este flujo
const restoreCommas = document.getElementById("chk-restore-commas")?.checked ?? false;// Restaurar comas por defecto
const similarityThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "81") / 100;// Umbral de similitud por defecto (convertido a porcentaje)
// 2. Condición de salida principal (todos los lugares procesados)
if (index >= places.length)
{
finalizeRender(inconsistents, places, sugerenciasPorPalabra);
return;
}
// 1. Verificar si el lugar actual es válido y tiene un ID
const venueFromOldModel = places[index];
const currentVenueNameObj = venueFromOldModel?.attributes?.name;
const nameValue = typeof currentVenueNameObj === 'object' && currentVenueNameObj !== null && typeof currentVenueNameObj.value === 'string' ? currentVenueNameObj.value.trim() !== ''
? currentVenueNameObj.value : undefined : typeof currentVenueNameObj === 'string' && currentVenueNameObj.trim() !== '' ? currentVenueNameObj : undefined;
//
/* if (!places[index] || typeof places[index] !== 'object' || !venueFromOldModel || typeof venueFromOldModel !== 'object' || !venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '')
{
console.warn(`[WME_PLN] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
return;
}*/
// 3. Salto temprano si el venue es inválido o no tiene nombre
/* if (!venueFromOldModel || typeof venueFromOldModel !== 'object' || !venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '')
{
// console.warn(`[WME_PLN] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
updateScanProgressBar(index, places.length); // Actualizar barra de progreso antes de saltar al siguiente lugar
index++;
// console.log(`[WME_PLN] Saltando al siguiente place (sin nombre/inválido). Próximo índice: ${index}`);
setTimeout(() => processNextPlace(), 0);
return;
}*/
// Se usa la variable limpia de emojis para generar el nombre normalizado.
const originalName = nameForProcessing; // 'originalName' ahora es explícitamente para el pipeline de procesamiento.
const normalizedName = processPlaceName(originalName); // Normalizar el nombre del lugar
//console.log(`[WME PLN - processNextPlace] Nombre Original para pipeline (sin emojis): "${originalName}"`);
//console.log(`[WME PLN - processNextPlace] Nombre Normalizado (después de processPlaceName): "${normalizedName}"`);
// 4. Verificar si el nombre ya está normalizado (sin emojis) y no requiere cambios
const { lat: placeLat, lon: placeLon } = getPlaceCoordinates(venueFromOldModel, venueSDK); // Obtener las coordenadas del lugar
// `isNameEffectivelyNormalized` debe calcularse DESPUÉS de `processPlaceName` y `aplicarReemplazosGenerales`
// y todas las transformaciones que definen el `suggestedName` final.
// Por lo tanto, esta línea no debería estar aquí para definir la variable `isNameEffectivelyNormalized` globalmente
// o para las condiciones de salto que se evalúan ANTES de que `suggestedName` sea final.
// Asegúrate de que `isNameEffectivelyNormalized` se calcula una vez al final, como en código original.
// Lógica unificada y robusta para obtener resolvedEditorName, lastEditorIdForComparison y calcular wasEditedByMe
resolvedEditorName = "Desconocido"; // Reinicializar para cada place
lastEditorIdForComparison = null; // Reinicializar para cada place
// Obtener el ID del usuario actual de forma robusta
if (venueSDK && venueSDK.modificationData)
{
const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '')
{
resolvedEditorName = updatedByDataFromSDK; // El nombre del editor es una cadena
}
else if (typeof updatedByDataFromSDK === 'number')
{
lastEditorIdForComparison = updatedByDataFromSDK; // El ID numérico es la fuente principal
resolvedEditorName = `ID ${updatedByDataFromSDK}`; // Nombre temporal
if (W && W.model && W.model.users)
{
const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
if (userObjectW && userObjectW.userName)
{ // Si el usuario está en el modelo Waze
resolvedEditorName = userObjectW.userName; // Obtener nombre real del usuario si está en el modelo
}
}
}
}
else if (venueFromOldModel && venueFromOldModel.attributes && (venueFromOldModel.attributes.updatedBy !== null && venueFromOldModel.attributes.updatedBy !== undefined))
{
// Fallback al modelo antiguo si el SDK no dio datos de editor
const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
lastEditorIdForComparison = oldModelUpdatedBy; // El ID numérico es la fuente principal
resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Nombre temporal
if (W && W.model && W.model.users)
{
const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
{
resolvedEditorName = userObjectW.userName; // Obtener nombre real del usuario si está en el modelo
}
}
}
else
{
resolvedEditorName = "N/D"; // No hay información de editor
}
// Calcular wasEditedByMe de forma robusta aquí mismo
wasEditedByMe = false; // Resetear para este place
if (currentLoggedInUserId !== null && currentLoggedInUserId !== undefined && resolvedEditorName !== "N/D")
{ // Solo si tenemos un nombre de usuario logueado y el resolvedEditorName no es N/D
if (lastEditorIdForComparison !== null && lastEditorIdForComparison !== undefined && typeof lastEditorIdForComparison === 'number')
{
// PRIORIDAD 1: Comparar IDs numéricos si ambos están disponibles y son válidos
if (typeof currentLoggedInUserId === 'number')
{ // Si el ID también es numérico
wasEditedByMe = (lastEditorIdForComparison === currentLoggedInUserId); //
}
else
{ // Si el ID es string (userName) y el del place es number
wasEditedByMe = (String(lastEditorIdForComparison) === currentLoggedInUserId); // Convertir solo el del place a string
}
}
else if (resolvedEditorName && typeof resolvedEditorName === 'string')
{
// PRIORIDAD 2: Si no hay ID numérico del editor del place, pero sí su nombre, comparar por nombre
wasEditedByMe = (resolvedEditorName.toLowerCase() === String(currentLoggedInUserId).toLowerCase()); //
}
}
//console.log(`[WME_PLN] Nombre sugerido final: "${suggestedName}"`); // Comentario opcional.
// --- Lógica para generar sugerencias del diccionario ---
const originalWords = originalName.split(/\s+/).filter(word => word.length > 0);
let sugerenciasLugar = {};
const suggestedName = normalizedName; // Usa el resultado correcto y final de processPlaceName
const suggestedNameWords = suggestedName.split(/\s+/).filter(word => word.length > 0);
originalWords.forEach((originalWord, wordIndex) => {
//console.log(`\n[WME PLN - processNextPlace] Procesando palabra original: "${originalWord}" (Índice: ${wordIndex})`);
if (!originalWord) return;
const lowerOriginalWord = originalWord.toLowerCase();
const cleanedLowerNoDiacritics = removeDiacritics(lowerOriginalWord);
//console.log(`[WME PLN - processNextPlace] lowerOriginalWord: "${lowerOriginalWord}", cleanedLowerNoDiacritics: "${cleanedLowerNoDiacritics}"`);
let tildeCorrectionSuggested = false; // Bandera para saber si ya sugerimos tilde para esta palabra
const currentSuggestedWord = suggestedNameWords[wordIndex] || '';
const lowerCurrentSuggestedWord = currentSuggestedWord.toLowerCase();
const currentSuggestedWordHasDiacritics = /[áéíóúÁÉÍÓÚüÜñÑ]/.test(lowerCurrentSuggestedWord);
//console.log(`[WME PLN - processNextPlace] currentSuggestedWord (del nombre sugerido): "${currentSuggestedWord}", lowerCurrentSuggestedWord: "${lowerCurrentSuggestedWord}", currentSuggestedWordHasDiacritics: ${currentSuggestedWordHasDiacritics}`);
// *******************************************************************
// PASO 1: PRIORIDAD - SUGERIR CORRECCIÓN DE TILDES
if (window.dictionaryWords && window.dictionaryWords.size > 0) {
//console.log(`[WME PLN - processNextPlace] Iniciando búsqueda de corrección de tildes para: "${originalWord}"`);
const firstChar = lowerOriginalWord.charAt(0);
const candidatesForTildeCheck = window.dictionaryIndex[firstChar] ? Array.from(window.dictionaryIndex[firstChar]) : [];
//console.log(`[WME PLN - processNextPlace] Candidatos del diccionario (letra '${firstChar}'):`, candidatesForTildeCheck);
for (const dictWord of candidatesForTildeCheck) {
const lowerDictWord = dictWord.toLowerCase();
const cleanedDictWordNoDiacritics = removeDiacritics(lowerDictWord);
//console.log(`[WME PLN - processNextPlace] Comparando con dictWord: "${dictWord}", lowerDictWord: "${lowerDictWord}", cleanedDictWordNoDiacritics: "${cleanedDictWordNoDiacritics}"`);
const originalHasDiacritics = /[áéíóúÁÉÍÓÚüÜñÑ]/.test(lowerOriginalWord);
const conditionMet = cleanedDictWordNoDiacritics === cleanedLowerNoDiacritics &&
lowerDictWord !== lowerCurrentSuggestedWord &&
!currentSuggestedWordHasDiacritics &&
/[áéíóúÁÉÍÓÚüÜñÑ]/.test(lowerDictWord) &&
!originalHasDiacritics; // <-- Solo si la original NO tiene tilde console.log(`[WME PLN - processNextPlace] Condición de tilde cumplida: ${conditionMet}`);
if (conditionMet) {
let suggestedTildeWord = normalizeWordInternal(dictWord, true, false);
//console.log(`[WME PLN - processNextPlace] ✅ ¡Tilde sugerida! Original: "${originalWord}" -> Sugerencia: "${suggestedTildeWord}"`);
if (!sugerenciasLugar[originalWord]) sugerenciasLugar[originalWord] = [];
sugerenciasLugar[originalWord].push({
word: suggestedTildeWord,
similarity: 0.999,
fuente: 'dictionary_tilde'
});
tildeCorrectionSuggested = true;
break;
}
}
if (!tildeCorrectionSuggested)
{
//console.log(`[WME PLN - processNextPlace] No se encontró sugerencia de tilde para "${originalWord}" en el diccionario.`);
}
}
// *******************************************************************
// *******************************************************************
// PASO 2: OTRAS SUGERENCIAS DEL DICCIONARIO (SOLO SI NO SE SUGIRIÓ CORRECCIÓN DE TILDE)
if (!tildeCorrectionSuggested && checkDictionaryWords && window.dictionaryWords) {
//console.log(`[WME PLN - processNextPlace] Buscando otras sugerencias del diccionario para: "${originalWord}" (No se sugirió tilde).`);
const similarDictionary = findSimilarWords(cleanedLowerNoDiacritics, window.dictionaryIndex, similarityThreshold);
if (similarDictionary.length > 0) {
const finalSuggestions = similarDictionary.filter(d =>
d.word.toLowerCase() !== lowerOriginalWord && // No es la misma palabra original
d.word.toLowerCase() !== lowerCurrentSuggestedWord && // No es la misma palabra que ya está en el sugerido
!sugerenciasLugar[originalWord]?.some(s => s.word === normalizeWordInternal(d.word, true, false)) // No duplica una sugerencia de tilde ya agregada
);
//console.log(`[WME PLN - processNextPlace] Otras sugerencias filtradas:`, finalSuggestions);
if (finalSuggestions.length > 0) {
if (!sugerenciasLugar[originalWord]) sugerenciasLugar[originalWord] = [];
finalSuggestions.forEach(dictSuggestion => {
if (!sugerenciasLugar[originalWord].some(s => s.word === normalizeWordInternal(dictSuggestion.word, true, false))) {
sugerenciasLugar[originalWord].push({ ...dictSuggestion, fuente: 'dictionary' });
}
});
}
}
}
// *******************************************************************
});
// console.log(`[WME_PLN] Nombre sugerido después de trim/espacios múltiples: "${suggestedName}"`);
// 6.1 --- QUITAR PUNTO FINAL SI EXISTE ---
if (suggestedName.endsWith('.'))
{
suggestedName = suggestedName.slice(0, -1);
// console.log(`[WME_PLN] Nombre sugerido después de quitar punto final: "${suggestedName}"`);
}
// 6.2 --- QUITAR ESPACIOS MÚLTIPLES ---
//console.log(`[WME_PLN] Evaluando lógica de salto...`);
const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
// Comparación estricta: solo colapsa espacios. Respeta mayúsculas, tildes y orden
const cleanedOriginalName = String(nameForProcessing||'').replace(/\s+/g,' ').trim();
const cleanedSuggestedName = String(suggestedName||'').replace(/\s+/g,' ').trim();
const equalExact = (cleanedOriginalName === cleanedSuggestedName);
const equalCaseInsensitive = (cleanedOriginalName.toLowerCase() === cleanedSuggestedName.toLowerCase());
const equalNoDiacritics = (removeDiacritics(cleanedOriginalName.toLowerCase()) === removeDiacritics(cleanedSuggestedName.toLowerCase()));
let isNameEffectivelyNormalized = equalExact; // solo si es EXACTO tras colapsar espacios
// console.group('[WME PLN - decision]');
// console.log('originalRaw:', originalNameRaw);
// console.log('originalForProcessing:', nameForProcessing);
// console.log('cleanedOriginal:', cleanedOriginalName);
// console.log('cleanedSuggested:', cleanedSuggestedName);
// console.log('equalExact:', equalExact, 'equalCaseInsensitive:', equalCaseInsensitive, 'equalNoDiacritics:', equalNoDiacritics);
// console.groupEnd();
// PASO 1: Comprobar si se debe excluir por ser una edición tuya DENTRO del rango de fecha.
if (avoidMyEdits && wasEditedByMe)
{
// Es un lugar editado por mí y el filtro está activo.
const dateFilterValue = document.getElementById("dateFilterSelect")?.value || "all";
const placeEditDate = (venueSDK && venueSDK.modificationData && venueSDK.modificationData.updatedOn)
? new Date(venueSDK.modificationData.updatedOn)
: null;
// Comprobar si la fecha de edición del lugar está dentro del rango seleccionado
if (placeEditDate && isDateWithinRange(placeEditDate, dateFilterValue)) {
// Está DENTRO del rango, por lo tanto, se omite. La decisión es final.
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP MY OWN EDIT - In Range: ${dateFilterValue}]`;
}
// Si está FUERA del rango, no hacemos nada aquí. Dejamos que 'shouldSkipThisPlace' siga siendo 'false'
// y pase al siguiente filtro de abajo.
}
// Condición de Salto 2: Lugar está en la lista de excluidos (por ID).
//console.log(`[WME PLN - SKIP] Verificando exclusión para ID: "${currentVenueId}"`); // <--- USAR currentVenueId
//console.log(`[WME PLN - SKIP] 'excludedPlaces' contiene ID: ${excludedPlaces.has(currentVenueId)}`); // true/false // <--- USAR currentVenueId
//console.log(`[WME PLN - SKIP] Contenido de 'excludedPlaces' (primeros 5 entries):`, Array.from(excludedPlaces.entries()).slice(0, 5)); // Ver algunos IDs guardados
if (!shouldSkipThisPlace && excludedPlaces.has(currentVenueId)) { // <--- USAR currentVenueId
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP EXCLUDED PLACE]`;
}
// PASO 2: Comprobar si el lugar ya está normalizado.
// Esta regla se aplica a TODOS los lugares que NO fueron omitidos en el PASO 1.
// (Incluye los lugares de otros editores y propias ediciones fuera del rango de fecha).
if (!shouldSkipThisPlace && isNameEffectivelyNormalized) {
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP NORMALIZED]`;
}
// PASO ADICIONAL DE SALTO: Si es un área y no se pudo calcular su área
if (!shouldSkipThisPlace && typeInfo.isArea && areaMeters === null)
{ // <-- typeInfo se usa aquí
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP AREA_CALC_FAILED]`;
}
//console.log(`[WME PLN - processNextPlace] LUGAR NO SALTADO. Procediendo. ID: ${currentVenueId}. Nombre: "${originalNameRaw}"`);
//console.log(`[WME PLN - SKIP] Verificando exclusión para ID: "${currentVenueId}"`);
//console.log(`[WME PLN - SKIP] 'excludedPlaces' contiene ID: ${excludedPlaces.has(currentVenueId)}`); // true/false
//console.log(`[WME PLN - SKIP] Contenido de 'excludedPlaces' (primeros 5 entries):`, Array.from(excludedPlaces.entries()).slice(0, 5)); // Ver algunos IDs guardados
// PASO 2.5: Comprobar si el lugar está en la lista de excluidos
if (!shouldSkipThisPlace && excludedPlaces.has(currentVenueId))
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP EXCLUDED PLACE]`;
}
// --- Salto temprano si se determinó omitir el lugar ---
if (shouldSkipThisPlace)
{
//console.log(`[WME PLN - processNextPlace] LUGAR SALTADO. Razón: ${skipReasonLog}. ID: ${currentVenueId}. Nombre: "${originalNameRaw}"`); // <--- USAR currentVenueId
// if (skipReasonLog) console.log(`[WME_PLN] ${skipReasonLog} Descartado "${originalName}" (ID: ${currentVenueId})`); //Añadir ID al log
const updateFrequency = 3; // Actualiza cada 3 lugares la barra de progreso
if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length)
{
updateScanProgressBar(index, places.length);
}
index++;
setTimeout(() => processNextPlace(), 0); // Continúa con el siguiente lugar
return;
}
else
{
//console.log(`[WME PLN - processNextPlace] LUGAR NO SALTADO. Procediendo. ID: ${currentVenueId}. Nombre: "${originalNameRaw}"`); // <--- USAR currentVenueId
}
//console.log(`[WME_PLN] Decisión de salto: ${shouldSkipThisPlace} (${skipReasonLog})`);
// 8. Registrar o no en la lista de inconsistentes
//console.log(`[WME_PLN] Registrando lugar con inconsistencias...`);
// *** Si Llegamos Aquí, El Lugar No Se Salta Y Necesitamos Su Info Completa Para La Tabla ***
if (processingStepLabel)
{
processingStepLabel.textContent = "Registrando lugar(es) con inconsistencias...";
}
// Lógica de Categorías (solo para lugares no saltados)
const shouldRecommendCategories = document.getElementById("chk-recommend-categories")?.checked ?? true;
let currentCategoryKey;
let currentCategoryIcon;
let currentCategoryTitle;
let currentCategoryName;
let dynamicSuggestions;
try
{
const lang = getWazeLanguage();
currentCategoryKey = getPlaceCategoryName(venueFromOldModel, venueSDK);
const categoryDetails = getCategoryDetails(currentCategoryKey);
currentCategoryIcon = categoryDetails.icon;
currentCategoryTitle = categoryDetails.description;
currentCategoryName = categoryDetails.description;
if (shouldRecommendCategories)
dynamicSuggestions = findCategoryForPlace(originalName);
else
dynamicSuggestions = [];
}
catch (e)
{
console.error("[WME PLN] Error procesando las categorías:", e);
currentCategoryName = "Error";
currentCategoryIcon = "❓";
currentCategoryTitle = "Error al obtener categoría";
dynamicSuggestions = [];
currentCategoryKey = "UNKNOWN";
}
// Lógica unificada y robusta para obtener resolvedEditorName, lastEditorIdForComparison y calcular wasEditedByMe
resolvedEditorName = "Desconocido"; // Reinicializar para cada place
lastEditorIdForComparison = null; // Reinicializar para cada place
if (venueSDK && venueSDK.modificationData)
{
const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '')
{
resolvedEditorName = updatedByDataFromSDK; // El nombre del editor es una cadena
}
else if (typeof updatedByDataFromSDK === 'number')
{
lastEditorIdForComparison = updatedByDataFromSDK; // El ID numérico es la fuente principal
resolvedEditorName = `ID ${updatedByDataFromSDK}`; // Nombre temporal
if (W && W.model && W.model.users)
{
const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
if (userObjectW && userObjectW.userName)
{
resolvedEditorName = userObjectW.userName; // Obtener nombre real del usuario si está en el modelo
}
}
}
}
else if (venueFromOldModel && venueFromOldModel.attributes && (venueFromOldModel.attributes.updatedBy !== null && venueFromOldModel.attributes.updatedBy !== undefined))
{
// Fallback al modelo antiguo si el SDK no dio datos de editor
const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
lastEditorIdForComparison = oldModelUpdatedBy; // El ID numérico es la fuente principal
resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Nombre temporal
if (W && W.model && W.model.users)
{
const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
{
resolvedEditorName = userObjectW.userName; // Obtener nombre real del usuario si está en el modelo
}
}
}
else
{
resolvedEditorName = "N/D"; // No hay información de editor
}
wasEditedByMe = false; // Resetear para este place
// Calcular wasEditedByMe de forma robusta aquí mismo
if (currentLoggedInUserId !== null && currentLoggedInUserId !== undefined && resolvedEditorName !== "N/D")
{ // Solo si tenemos un nombre de usuario logueado y el resolvedEditorName no es N/D
if (lastEditorIdForComparison !== null && lastEditorIdForComparison !== undefined && typeof lastEditorIdForComparison === 'number')
{
// PRIORIDAD 1: Comparar IDs numéricos si ambos están disponibles y son válidos
if (typeof currentLoggedInUserId === 'number')
{ // Si el ID también es numérico
wasEditedByMe = (lastEditorIdForComparison === currentLoggedInUserId);
}
else
{ // Si el ID es string (userName) y el del place es number
wasEditedByMe = (String(lastEditorIdForComparison) === currentLoggedInUserId); // Convertir solo el del place a string
}
}
else if (resolvedEditorName && typeof resolvedEditorName === 'string')
{
// PRIORIDAD 2: Si no hay ID numérico del editor del place, pero sí su nombre, comparar por nombre
wasEditedByMe = (resolvedEditorName.toLowerCase() === String(currentLoggedInUserId).toLowerCase());
}
}
// Obtener información de la ciudad (esto ya estaba bien, solo reubicado)
try
{
cityInfo = await getPlaceCityInfo(venueFromOldModel, venueSDK);
}
catch (e)
{
console.error(`[WME_PLN] Error al obtener información de la ciudad para el venue ID ${currentVenueId}:`, e);
}
//Determinar nivel de bloqueo correspondiente
let lockRank = 0; // Valor por defecto
if (venueSDK && venueSDK.lockRank !== undefined && venueSDK.lockRank !== null)
lockRank = venueSDK.lockRank;
else if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.lockRank !== undefined && venueFromOldModel.attributes.lockRank !== null)
lockRank = venueFromOldModel.attributes.lockRank;
let lockRankEmoji;
// Lógica corregida: 1 al 6 muestra su respectivo emoji; 0 (desbloqueado) o cualquier otro valor muestra 0️⃣
if (lockRank >= 0 && lockRank <= 5)
lockRankEmoji = lockRankEmojis[lockRank+1]; // Usa el emoji para el nivel exacto (1 al 6)
else
lockRankEmoji = lockRankEmojis[0]; // Para 0 (desbloqueado), Auto (si no fue 1-6), o cualquier otro caso
//console.log(`[WME_PLN][DEBUG] Assigned LockRankEmoji: ${lockRankEmoji}`);
// Agregar a la lista de inconsistencias
inconsistents.push({
lockRankEmoji: lockRankEmoji,
id: currentVenueId,
original: originalNameRaw,
normalized: suggestedName,
editor: resolvedEditorName, // Usamos el nombre del editor resuelto
cityIcon: cityInfo.icon,
cityTitle: cityInfo.title,
hasCity: cityInfo.hasCity,
venueSDKForRender: venueSDK,
currentCategoryName: currentCategoryName,
currentCategoryIcon: currentCategoryIcon,
currentCategoryTitle: currentCategoryTitle,
currentCategoryKey: currentCategoryKey,
dynamicCategorySuggestions: dynamicSuggestions,
// Asegurarse de incluir lat y lon obtenidos de getPlaceCoordinates
lat: placeLat,
lon: placeLon,
typeInfo: typeInfo, // Guardar el objeto completo para su uso en el render
areaMeters: areaMeters // Ya se calcula con venueSDK
});
// 9. Agregar datos del lugar para la verificación de duplicados
sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;// Guardar sugerencias por palabra para este lugar
// 10. Finalizar procesamiento del 'place' actual y pasar al siguiente
const updateFrequency = 5;
if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length)
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
}
// console.log("[WME_PLN] Iniciando primer processNextPlace...");
try
{
setTimeout(() => { processNextPlace(); }, 10);
}
catch (error)
{
console.error("[WME_PLN][ERROR_CRITICAL] Fallo al iniciar processNextPlace:", error, error.stack);
enableScanControls();
const outputFallback = document.querySelector("#wme-place-inspector-output");
if (outputFallback)
{
outputFallback.innerHTML = `<div style='color:red; padding:10px;'><b>Error Crítico:</b> El script de normalización encontró un problema grave y no pudo continuar. Revise la consola para más detalles (F12).<br>Detalles: ${error.message}</div>`;
}
const scanBtn = document.querySelector("button[type='button']"); // Asumiendo que es el botón de Start Scan
if (scanBtn)
{
scanBtn.disabled = false;
scanBtn.textContent = "Start Scan... (Error Previo)";
}
if (window.processingDotsInterval)
{
clearInterval(window.processingDotsInterval);
}
}// processNextPlace
// Función para re-aplicar la lógica de palabras excluidas al texto normalizado
function reapplyExcludedWordsLogic(text, excludedWordsSet)
{
if (typeof text !== 'string' || !excludedWordsSet || excludedWordsSet.size === 0)
{
return text;
}
const wordsInText = text.split(/\s+/);
const processedWordsArray = wordsInText.map(word =>
{
if (word === "") return "";
const wordWithoutDiacriticsLower = removeDiacritics(word.toLowerCase());
// Encontrar la palabra excluida que coincida (insensible a may/min y diacríticos)
const matchingExcludedWord = Array.from(excludedWordsSet).find(
w_excluded => removeDiacritics(w_excluded.toLowerCase()) === wordWithoutDiacriticsLower);
if (matchingExcludedWord)
{
// Si coincide, DEVOLVER LA FORMA EXACTA DE LA LISTA DE EXCLUIDAS
return matchingExcludedWord;
}
// Si no, devolver la palabra como estaba (ya normalizada por pasos previos)
return word;
});
return processedWordsArray.join(' ');
}// reapplyExcludedWordsLogic
//Función para finalizar renderizado una vez completado el análisis
function finalizeRender(inconsistents, placesArr, allSuggestions)
{ // Limpiar el mensaje de procesamiento y spinner al finalizar el análisis
//const typeInfo = venueSDK?.typeInfo || {};
enableScanControls();
// Detener animación de puntos suspensivos si existe
if (window.processingDotsInterval)
{
clearInterval(window.processingDotsInterval);
window.processingDotsInterval = null;
}
// Refuerza el restablecimiento del botón de escaneo al entrar
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
scanBtn.textContent = "Start Scan...";
scanBtn.disabled = false;
scanBtn.style.opacity = "1";
scanBtn.style.cursor = "pointer";
}
// Verificar si el botón de escaneo existe
const output = document.querySelector("#wme-place-inspector-output");
if (!output)
{
// console.error("[WME_PLN]❌ No se pudo montar el panel flotante. Revisar estructura del DOM.");
alert("Hubo un problema al mostrar los resultados. Intenta recargar la página.");
return;
}
// Limpiar el mensaje de procesamiento y spinner
const undoRedoHandler = function()
{// Maneja el evento de deshacer/rehacer
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
reactivateAllActionButtons(); // No necesitamos setTimeout aquí si renderPlacesInFloatingPanel es síncrono.
});
}
else
{
//console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
};
// Objeto para almacenar referencias de listeners para desregistro
if (!window._wmePlnUndoRedoListeners)
{
window._wmePlnUndoRedoListeners = {};
}
// Desregistrar listeners previos si existen
if (window._wmePlnUndoRedoListeners.undo)
{
W.model.actionManager.events.unregister("afterundoaction", null, window._wmePlnUndoRedoListeners.undo);
}
if (window._wmePlnUndoRedoListeners.redo)
{
W.model.actionManager.events.unregister("afterredoaction", null, window._wmePlnUndoRedoListeners.redo);
}
// Registrar nuevos listeners
W.model.actionManager.events.register("afterundoaction", null, undoRedoHandler);
W.model.actionManager.events.register("afterredoaction", null, undoRedoHandler);
// Almacenar referencias para poder desregistrar en el futuro
window._wmePlnUndoRedoListeners.undo = undoRedoHandler;
window._wmePlnUndoRedoListeners.redo = undoRedoHandler;
// Esta llamada se hace ANTES de limpiar el output. El primer argumento es el estado, el segundo es el número de inconsistencias.
createFloatingPanel("results", inconsistents.length);
// Limpiar el mensaje de procesamiento y spinner
if (output)
{
// Mostrar el panel flotante al terminar el procesamiento se usa para mostrar los resultados y llamados al console.log
}
// Limitar a 30 resultados y mostrar advertencia si excede
const maxRenderLimit = 30;
const totalInconsistentsOriginal = inconsistents.length; // Guardar el total original
let isLimited = false; // Declarar e inicializar isLimited
// Si hay más de 30 resultados, limitar a 30 y mostrar mensaje
if (totalInconsistentsOriginal > maxRenderLimit)
{
inconsistents = inconsistents.slice(0, maxRenderLimit);
isLimited = true; // Establecer isLimited a true si se aplica el límite
// Mostrar mensaje de advertencia si se aplica el límite
if (!sessionStorage.getItem("popupShown"))
{
const modalLimit = document.createElement("div"); // Renombrado a modalLimit para claridad
modalLimit.style.position = "fixed";
modalLimit.style.top = "50%";
modalLimit.style.left = "50%";
modalLimit.style.transform = "translate(-50%, -50%)";
modalLimit.style.background = "#fff";
modalLimit.style.border = "1px solid #ccc";
modalLimit.style.padding = "20px";
modalLimit.style.zIndex = "10007"; // <<<<<<< Z-INDEX AUMENTADO
modalLimit.style.width = "400px";
modalLimit.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)";
modalLimit.style.borderRadius = "8px";
modalLimit.style.fontFamily = "sans-serif";
// Fondo suave azul y mejor presentación
modalLimit.style.backgroundColor = "#f0f8ff";
modalLimit.style.border = "1px solid #aad";
modalLimit.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)";
// --- Insertar ícono visual de información arriba del mensaje ---
const iconInfo = document.createElement("div"); // Renombrado
iconInfo.innerHTML = "ℹ️";
iconInfo.style.fontSize = "24px";
iconInfo.style.marginBottom = "10px";
modalLimit.appendChild(iconInfo);
// Contenedor del mensaje
const message = document.createElement("p");
message.innerHTML = `Se encontraron <strong>${
totalInconsistentsOriginal}</strong> lugares con nombres no normalizados.<br><br>Solo se mostrarán los primeros <strong>${
maxRenderLimit}</strong>.<br><br>Una vez corrijas estos, presiona nuevamente <strong>'Start Scan...'</strong> para continuar con el análisis del resto.`;
message.style.marginBottom = "20px";
modalLimit.appendChild(message);
// Botón de aceptar
const acceptBtn = document.createElement("button");
acceptBtn.textContent = "Aceptar";
acceptBtn.style.padding = "6px 12px";
acceptBtn.style.cursor = "pointer";
acceptBtn.style.backgroundColor = "#007bff";
acceptBtn.style.color = "#fff";
acceptBtn.style.border = "none";
acceptBtn.style.borderRadius = "4px";
acceptBtn.addEventListener("click", () => {sessionStorage.setItem("popupShown", "true");
modalLimit.remove();
});
modalLimit.appendChild(acceptBtn);
document.body.appendChild(modalLimit); // Se añade al body, así que el z-index debería funcionar globalmente
}
}
// Llamar a la función para detectar y alertar nombres duplicados
detectAndAlertDuplicateNames(inconsistents);
// Crear un contenedor para los elementos fijos de la cabecera del panel de resultados
const fixedHeaderContainer = document.createElement("div");
fixedHeaderContainer.style.background = "#fff"; // Fondo para que no se vea el scroll debajo
fixedHeaderContainer.style.padding = "0 10px 8px 10px"; // Padding para espacio y que no esté pegado
fixedHeaderContainer.style.borderBottom = "1px solid #ccc"; // Un borde para separarlo de la tabla
fixedHeaderContainer.style.zIndex = "11"; // Asegurarse de que esté por encima de la tabla
// Añadir Estas Dos Líneas Clave Al FixedHeaderContainer
fixedHeaderContainer.style.position = "sticky"; // Hacer Que Este Contenedor Sea Sticky
fixedHeaderContainer.style.top = "0"; // Pegado A La Parte Superior Del Contenedor De Scroll
// =======================================================
// INICIO DEL BLOQUE CORREGIDO
// =======================================================
// 1. Contenedor Flex para el texto y el botón
const headerControlsContainer = document.createElement("div");
headerControlsContainer.style.display = "flex";
// LA SIGUIENTE LÍNEA ES EL CAMBIO PRINCIPAL:
headerControlsContainer.style.justifyContent = "flex-start"; // Alinea los elementos al inicio
headerControlsContainer.style.alignItems = "center";
headerControlsContainer.style.gap = "15px"; // Mantiene el espacio entre texto y botón
const resultsCounter = document.createElement("div");
resultsCounter.className = "results-counter-display";
resultsCounter.style.fontSize = "13px";
resultsCounter.style.color = "#555";
resultsCounter.style.textAlign = "left";
resultsCounter.dataset.currentCount = inconsistents.length;
resultsCounter.dataset.totalOriginal = totalInconsistentsOriginal;
resultsCounter.dataset.maxRenderLimit = maxRenderLimit;
if (totalInconsistentsOriginal > 0) {
if (isLimited) {
resultsCounter.innerHTML = `<span style="color: #ff0000;">Inconsistencias encontradas: <b>${totalInconsistentsOriginal}</b></span>. Mostrando las primeras <span style="color: #ff0000;"><b>${inconsistents.length}</b></span>.`;
} else {
resultsCounter.innerHTML = `Inconsistencias encontradas: <b style="color: #ff0000;">${inconsistents.length}</b> de <b style="color: #ff0000;">${totalInconsistentsOriginal}</b>. Mostrando <b style="color: #ff0000;">${inconsistents.length}</b>.`;
}
headerControlsContainer.appendChild(resultsCounter);
} else {
const outputDiv = document.querySelector("#wme-place-inspector-output");
if (outputDiv) {
outputDiv.innerHTML = `<div style='color:green; padding:10px;'>✔ Todos los lugares visibles están correctamente normalizados o excluidos.</div>`;
}
}
// 2. Lógica del botón (sin cambios respecto a la corrección anterior)
// En la función donde creas el botón toggle
let toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (!toggleBtn) {
toggleBtn = document.createElement("button");
toggleBtn.id = 'pln-toggle-hidden-btn';
toggleBtn.style.padding = "5px 10px";
toggleBtn.style.marginLeft = "15px";
toggleBtn.dataset.state = 'hidden'; // IMPORTANTE: Iniciar en 'hidden' para ocultar automáticamente
toggleBtn.addEventListener('click', function() {
const currentState = this.dataset.state;
if (currentState === 'shown') {
// Cambiar a ocultos
this.textContent = "Mostrar procesados";
document.body.classList.add('pln-hide-normalized-rows');
this.dataset.state = 'hidden';
} else {
// Cambiar a visibles
this.textContent = "Ocultar procesados";
document.body.classList.remove('pln-hide-normalized-rows');
this.dataset.state = 'shown';
}
});
// Establecer texto inicial según el estado
toggleBtn.textContent = "Mostrar procesados";
}
// Sincronizar el texto del botón con su estado actual cada vez que se renderiza
if (toggleBtn.dataset.state === 'shown') {
toggleBtn.textContent = 'Ocultar Normalizados';
} else {
toggleBtn.textContent = 'Mostrar Normalizados';
}
if (totalInconsistentsOriginal > 0) {
headerControlsContainer.appendChild(toggleBtn);
}
fixedHeaderContainer.appendChild(headerControlsContainer);
// =======================================================
// FIN DEL BLOQUE CORREGIDO
// =======================================================
if (inconsistents.length === 0) {
if (totalInconsistentsOriginal === 0) {
const checkIcon = document.createElement("div");
checkIcon.innerHTML = "";
checkIcon.style.marginTop = "10px";
checkIcon.style.fontSize = "14px";
checkIcon.style.color = "green";
output.appendChild(checkIcon);
const successMsg = document.createElement("div");
successMsg.textContent = "";
successMsg.style.marginTop = "10px";
successMsg.style.fontSize = "14px";
successMsg.style.color = "green";
successMsg.style.fontWeight = "bold";
output.appendChild(successMsg);
}
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay) existingOverlay.remove();
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent = `Progreso: 100% (${placesArr.length}/${placesArr.length})`;
}
const outputTab = document.getElementById("wme-normalization-tab-output");
if (outputTab)
{
outputTab.innerHTML = `✔ Todos los nombres están normalizados. Se analizaron ${placesArr.length} lugares.`;
outputTab.style.color = "green";
outputTab.style.fontWeight = "bold";
}
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
scanBtn.textContent = "Start Scan...";
scanBtn.disabled = false;
scanBtn.style.opacity = "1";
scanBtn.style.cursor = "pointer";
const iconCheck = document.createElement("span");
iconCheck.textContent = " ✔";
iconCheck.style.marginLeft = "8px";
iconCheck.style.color = "green";
scanBtn.appendChild(iconCheck);
}
return;
}
if (output)
{
output.style.display = 'flex';
output.style.flexDirection = 'column';
output.style.position = 'relative';
output.appendChild(fixedHeaderContainer);
}
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.fontSize = "12px";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
[
"N°",
"Perma",
"Tipo/Ciudad",
"LL",
"Editor",
"Nombre Actual",
"⚠️",
"Nombre Sugerido",
"Sugerencias<br>de reemplazo",
"Categoría",
"Categoría<br>Recomendada",
"Acción"
].forEach(header =>
{
const th = document.createElement("th");
th.innerHTML = header;
th.style.borderBottom = "1px solid #ccc";
th.style.padding = "4px";
th.style.textAlign = "center";
th.style.fontSize = "14px";
if (header === "N°")
{
th.style.width = "30px";
}
else if (header === "LL")
{
th.title = "Nivel de Bloqueo (Lock Level)";
th.style.width = "40px";
}
else if (header === "Perma" || header === "Tipo/Ciudad")
{
th.style.width = "65px";
}
else if (header === "⚠️")
{
th.title = "Alertas y advertencias";
th.style.width = "30px";
}
else if (header === "Categoría")
{
th.style.width = "130px";
}
else if (header === "Categoría<br>Recomendada" || header === "Sugerencias<br>de reemplazo")
{
th.style.width = "180px";
}
else if (header === "Editor")
{
th.style.width = "100px";
}
else if (header === "Acción")
{
th.style.width = "100px";
}
else if (header === "Nombre Actual" || header === "Nombre Sugerido")
{
th.style.width = "270px";
}
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
thead.style.position = "sticky";
thead.style.top = "0";
thead.style.background = "#f1f1f1";
thead.style.zIndex = "10";
headerRow.style.backgroundColor = "#003366";
headerRow.style.color = "#ffffff";
const tbody = document.createElement("tbody");
inconsistents.forEach(({ lockRankEmoji, id, original, normalized, editor, cityIcon, cityTitle, hasCity, currentCategoryName, currentCategoryIcon, currentCategoryTitle, currentCategoryKey, dynamicCategorySuggestions, venueSDKForRender, isDuplicate = false, duplicatePartners = [], typeInfo, areaMeters }, index) =>
{
const progressPercent = Math.floor(((index + 1) / inconsistents.length) * 100);
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = `${progressPercent}%`;
progressBarTextTab.textContent = `Progreso: ${progressPercent}% (${index + 1}/${inconsistents.length})`;
}
const row = document.createElement("tr");
row.querySelectorAll("td").forEach(td => td.style.verticalAlign = "top");
row.dataset.placeId = id;
const numberCell = document.createElement("td");
numberCell.textContent = index + 1;
numberCell.style.textAlign = "center";
numberCell.style.padding = "4px";
row.appendChild(numberCell);
const permalinkCell = document.createElement("td");
const link = document.createElement("a");
link.href = "#";
link.addEventListener("click", (e) =>
{
e.preventDefault();
const venueObj = W.model.venues.getObjectById(id);
const venueSDKForUse = venueSDKForRender;
let targetLat = null;
let targetLon = null;
if (venueSDKForUse && venueSDKForUse.geometry && Array.isArray(venueSDKForUse.geometry.coordinates) && venueSDKForUse.geometry.coordinates.length >= 2) {
targetLon = venueSDKForUse.geometry.coordinates[0];
targetLat = venueSDKForUse.geometry.coordinates[1];
}
if ((targetLat === null || targetLon === null) && venueObj && typeof venueObj.getOLGeometry === 'function') {
try {
const geometryOL = venueObj.getOLGeometry();
if (geometryOL && typeof geometryOL.getCentroid === 'function') {
const centroidOL = geometryOL.getCentroid();
if (typeof OpenLayers !== 'undefined' && OpenLayers.Projection) {
const transformedPoint = new OpenLayers.Geometry.Point(centroidOL.x, centroidOL.y).transform(
new OpenLayers.Projection("EPSG:3857"),
new OpenLayers.Projection("EPSG:4326")
);
targetLat = transformedPoint.y;
targetLon = transformedPoint.x;
} else {
targetLat = centroidOL.y;
targetLon = centroidOL.x;
}
}
} catch (e) {
console.error("[WME PLN] Error al obtener/transformar geometría OL para navegación:", e);
}
}
let navigated = false;
if (venueObj && W.selectionManager && typeof W.selectionManager.select === "function")
{
W.selectionManager.select(venueObj);
navigated = true;
}
else if (venueObj && W.selectionManager && typeof W.selectionManager.setSelectedModels === "function")
{
W.selectionManager.setSelectedModels([venueObj]);
navigated = true;
}
if (!navigated)
{
const confirmOpen = confirm(`El lugar "${original}" (ID: ${id}) no se pudo seleccionar o centrar directamente. ¿Deseas abrirlo en una nueva pestaña del editor?`);
if (confirmOpen)
{
const wmeUrl = `https://www.waze.com/editor?env=row&venueId=${id}`;
window.open(wmeUrl, '_blank');
}
else
{
showTemporaryMessage("El lugar podría estar fuera de vista o no cargado.", 4000, 'warning');
}
}
else
{
showTemporaryMessage("Presentando detalles del lugar...", 2000, 'info');
}
});
link.title = "Seleccionar lugar en el mapa";
link.textContent = "🔗";
permalinkCell.appendChild(link);
permalinkCell.style.padding = "4px";
permalinkCell.style.fontSize = "18px";
permalinkCell.style.textAlign = "center";
permalinkCell.style.width = "65px";
row.appendChild(permalinkCell);
const typeCityCell = document.createElement("td");
typeCityCell.style.padding = "4px";
typeCityCell.style.width = "65px";
typeCityCell.style.verticalAlign = "middle";
const cellContentWrapper = document.createElement("div");
cellContentWrapper.style.display = "flex";
cellContentWrapper.style.justifyContent = "space-around";
cellContentWrapper.style.alignItems = "center";
const typeContainer = document.createElement("div");
typeContainer.style.display = "flex";
typeContainer.style.flexDirection = "column";
typeContainer.style.alignItems = "center";
typeContainer.style.justifyContent = "center";
typeContainer.style.gap = "2px";
const typeIconSpan = document.createElement("span");
typeIconSpan.textContent = typeInfo.icon;
typeIconSpan.style.fontSize = "20px";
let tooltipText = `Tipo: ${typeInfo.title}`;
typeIconSpan.title = tooltipText;
typeContainer.appendChild(typeIconSpan);
if (typeInfo.isArea && areaMeters !== null && areaMeters !== undefined)
{
const areaSpan = document.createElement("span");
const areaFormatted = areaMeters.toLocaleString('es-ES', { maximumFractionDigits: 0 });
areaSpan.textContent = `${areaFormatted} m²`;
areaSpan.style.fontSize = "10px";
areaSpan.style.fontWeight = "bold";
areaSpan.style.textAlign = "center";
areaSpan.style.lineHeight = "1";
areaSpan.style.whiteSpace = "nowrap";
if (areaMeters < 400)
{
areaSpan.style.color = "red";
areaSpan.classList.add("area-blink");
}
else
{
areaSpan.style.color = "blue";
}
areaSpan.title = `Área: ${areaFormatted} m²`;
typeContainer.appendChild(areaSpan);
}
cellContentWrapper.appendChild(typeContainer);
const cityStatusIconSpan = document.createElement("span");
cityStatusIconSpan.className = 'city-status-icon';
cityStatusIconSpan.style.fontSize = "18px";
cityStatusIconSpan.style.cursor = "pointer";
if (hasCity)
{
cityStatusIconSpan.innerHTML = '✅';
cityStatusIconSpan.style.color = 'green';
cityStatusIconSpan.title = cityTitle;
}
else
{
cityStatusIconSpan.innerHTML = '🚩';
cityStatusIconSpan.style.color = 'red';
cityStatusIconSpan.title = cityTitle;
cityStatusIconSpan.addEventListener("click", async () =>
{
const coords = getPlaceCoordinates(W.model.venues.getObjectById(id), venueSDKForRender);
const placeLat = coords.lat;
const placeLon = coords.lon;
if (placeLat === null || placeLon === null)
{
alert("No se pudieron obtener las coordenadas del lugar.");
return;
}
const allCities = Object.values(W.model.cities.objects)
.filter(city =>
city &&
city.attributes &&
typeof city.attributes.name === 'string' &&
city.attributes.name.trim() !== ''
);
const citiesWithDistance = allCities.map(city =>
{
if (!city.attributes.geoJSONGeometry ||
!Array.isArray(city.attributes.geoJSONGeometry.coordinates) ||
city.attributes.geoJSONGeometry.coordinates.length < 2)
return null;
const cityLon = city.attributes.geoJSONGeometry.coordinates[0];
const cityLat = city.attributes.geoJSONGeometry.coordinates[1];
const distanceInMeters = calculateDistance(placeLat, placeLon, cityLat, cityLon);
const distanceInKm = distanceInMeters / 1000;
return {
name: city.attributes.name,
distance: distanceInKm,
cityId: city.getID()
};
}).filter(Boolean);
const closestCities = citiesWithDistance.sort((a, b) => a.distance - b.distance).slice(0, 5);
const modal = document.createElement("div");
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.background = "#fff";
modal.style.border = "1px solid #aad";
modal.style.padding = "28px 32px 20px 32px";
modal.style.zIndex = "20000";
modal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
modal.style.fontFamily = "sans-serif";
modal.style.borderRadius = "10px";
modal.style.textAlign = "center";
modal.style.minWidth = "340px";
const iconElement = document.createElement("div");
iconElement.innerHTML = "🏙️";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
modal.appendChild(iconElement);
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>Asignar ciudad al lugar</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
modal.appendChild(messageTitle);
const listDiv = document.createElement("div");
listDiv.style.textAlign = "left";
listDiv.style.marginTop = "10px";
if (closestCities.length === 0)
{
const noCityLine = document.createElement("div");
noCityLine.textContent = "No se encontraron ciudades cercanas para mostrar.";
noCityLine.style.color = "#888";
listDiv.appendChild(noCityLine);
}
else
{
closestCities.forEach((city, idx) =>
{
const cityLine = document.createElement("div");
cityLine.style.marginBottom = "8px";
cityLine.style.display = "flex";
cityLine.style.alignItems = "center";
const radioInput = document.createElement("input");
radioInput.type = "radio";
radioInput.name = `city-selection-${id}`;
radioInput.value = city.cityId;
radioInput.id = `city-radio-${city.cityId}`;
radioInput.style.marginRight = "10px";
radioInput.style.marginTop = "0";
if (idx === 0) radioInput.checked = true;
const radioLabel = document.createElement("label");
radioLabel.htmlFor = `city-radio-${city.cityId}`;
radioLabel.style.cursor = "pointer";
radioLabel.innerHTML = `<b>${city.name}</b> <span style="color: #666; font-size: 11px;">(ID: ${city.cityId})</span> <span style="color: #007bff;">${city.distance.toFixed(1)} km</span>`;
cityLine.appendChild(radioInput);
cityLine.appendChild(radioLabel);
listDiv.appendChild(cityLine);
});
}
modal.appendChild(listDiv);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "flex-end";
buttonWrapper.style.gap = "12px";
buttonWrapper.style.marginTop = "20px";
const applyBtn = document.createElement("button");
applyBtn.textContent = "Aplicar Ciudad";
applyBtn.style.padding = "8px 16px";
applyBtn.style.background = "#28a745";
applyBtn.style.color = "#fff";
applyBtn.style.border = "none";
applyBtn.style.borderRadius = "4px";
applyBtn.style.cursor = "pointer";
applyBtn.style.fontWeight = "bold";
applyBtn.addEventListener('click', () => {
const selectedRadio = modal.querySelector(`input[name="city-selection-${id}"]:checked`);
if (!selectedRadio) {
alert("Por favor, selecciona una ciudad de la lista.");
return;
}
const selectedCityId = parseInt(selectedRadio.value, 10);
const selectedCityName = selectedRadio.parentElement.querySelector('label b').textContent;
const venueToUpdate = W.model.venues.getObjectById(id);
if (!venueToUpdate) {
alert("Error: No se pudo encontrar el lugar para actualizar. Puede que ya no esté visible.");
modal.remove();
return;
}
try {
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(venueToUpdate, { cityID: selectedCityId });
W.model.actionManager.add(action);
const row = document.querySelector(`tr[data-place-id="${id}"]`);
if (row) {
row.dataset.addressChanged = 'true';
const iconToUpdate = row.querySelector('.city-status-icon');
if (iconToUpdate) {
iconToUpdate.innerHTML = '✅';
iconToUpdate.style.color = 'green';
iconToUpdate.title = `Ciudad asignada: ${selectedCityName}`;
iconToUpdate.style.pointerEvents = 'none';
}
updateApplyButtonState(row, original);
}
modal.remove();
showTemporaryMessage("Ciudad asignada correctamente. No olvides Guardar los cambios.", 4000, 'success');
} catch (e) {
console.error("[WME PLN] Error al crear o ejecutar la acción de actualizar ciudad:", e);
alert("Ocurrió un error al intentar asignar la ciudad: " + e.message);
}
});
const closeBtn = document.createElement("button");
closeBtn.textContent = "Cerrar";
closeBtn.style.padding = "8px 16px";
closeBtn.style.background = "#888";
closeBtn.style.color = "#fff";
closeBtn.style.border = "none";
closeBtn.style.borderRadius = "4px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontWeight = "bold";
closeBtn.addEventListener("click", () => modal.remove());
buttonWrapper.appendChild(applyBtn);
buttonWrapper.appendChild(closeBtn);
modal.appendChild(buttonWrapper);
closeBtn.style.padding = "8px 16px";
closeBtn.style.background = "#888";
closeBtn.style.color = "#fff";
closeBtn.style.border = "none";
closeBtn.style.borderRadius = "4px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontWeight = "bold";
closeBtn.addEventListener("click", () => modal.remove());
buttonWrapper.appendChild(applyBtn);
buttonWrapper.appendChild(closeBtn);
modal.appendChild(buttonWrapper);
document.body.appendChild(modal);
});
}
cellContentWrapper.appendChild(cityStatusIconSpan);
typeCityCell.appendChild(cellContentWrapper);
row.appendChild(typeCityCell);
const lockCell = document.createElement("td");
lockCell.textContent = lockRankEmoji;
lockCell.style.textAlign = "center";
lockCell.style.padding = "4px";
lockCell.style.width = "40px";
lockCell.style.fontSize = "18px";
row.appendChild(lockCell);
// Editor
const editorCell = document.createElement("td");
editorCell.textContent = editor || "Desconocido";
editorCell.title = "Último editor";
editorCell.style.padding = "4px";
editorCell.style.width = "140px";
editorCell.style.textAlign = "center";
row.appendChild(editorCell);
// Nombre Actual
const originalCell = document.createElement("td");
const inputOriginal = document.createElement("textarea");
inputOriginal.rows = 3; inputOriginal.readOnly = true;
inputOriginal.style.whiteSpace = "pre-wrap";
const venueLive = W.model.venues.getObjectById(id);
const currentLiveName = venueLive?.attributes?.name?.value || venueLive?.attributes?.name || "";
inputOriginal.value = currentLiveName || original;
if (currentLiveName.trim().toLowerCase() !== normalized.trim().toLowerCase())
{
inputOriginal.style.border = "1px solid red";
inputOriginal.title = "Este nombre es distinto del original mostrado en el panel";
}
inputOriginal.disabled = true;
inputOriginal.style.width = "270px";
inputOriginal.style.backgroundColor = "#eee";
originalCell.style.padding = "4px";
originalCell.style.width = "270px";
originalCell.style.display = "flex";
originalCell.style.alignItems = "flex-start";
originalCell.style.verticalAlign = "middle";
inputOriginal.style.flex = "1";
inputOriginal.style.height = "100%";
inputOriginal.style.boxSizing = "border-box";
originalCell.appendChild(inputOriginal);
row.appendChild(originalCell);
const alertCell = document.createElement("td");
alertCell.style.width = "30px";
alertCell.style.textAlign = "center";
alertCell.style.verticalAlign = "middle";
alertCell.style.padding = "4px";
if (isDuplicate)
{
const warningIcon = document.createElement("span");
warningIcon.textContent = " ⚠️";
warningIcon.style.fontSize = "16px";
let tooltipText = `Nombre de lugar duplicado cercano.`;
if (duplicatePartners && duplicatePartners.length > 0)
{
const partnerDetails = duplicatePartners.map(p => `Línea ${p.line}: "${p.originalName}"`).join(", ");
tooltipText += ` Duplicado(s) con: ${partnerDetails}.`;
}
else
{
tooltipText += ` No se encontraron otros duplicados cercanos específicos.`;
}
warningIcon.title = tooltipText;
alertCell.appendChild(warningIcon);
}
row.appendChild(alertCell);
const suggestionCell = document.createElement("td");
suggestionCell.style.display = "flex";
suggestionCell.style.alignItems = "flex-start";
suggestionCell.style.justifyContent = "flex-start";
suggestionCell.style.padding = "4px";
suggestionCell.style.width = "270px";
const inputReplacement = document.createElement("textarea");
inputReplacement.className = 'replacement-input';
try
{
inputReplacement.value = normalized;
}
catch (_)
{
inputReplacement.value = normalized;
}
inputReplacement.style.width = "100%";
inputReplacement.style.height = "100%";
inputReplacement.style.boxSizing = "border-box";
inputReplacement.style.whiteSpace = "pre-wrap";
inputReplacement.rows = 3;
suggestionCell.appendChild(inputReplacement);
function debounce(func, delay)
{
let timeout;
return function (...args)
{
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
const checkAndUpdateApplyButton = () =>
{
const nameIsDifferent = inputReplacement.value.trim() !== original.trim();
const categoryWasChanged = row.dataset.categoryChanged === 'true';
if (nameIsDifferent || categoryWasChanged)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
const successIcon = applyButtonWrapper.querySelector('span');
if (successIcon) successIcon.remove();
}
else
{
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
}
};
inputReplacement.addEventListener('input', debounce(checkAndUpdateApplyButton, 300));
let autoApplied = false;
if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded' && s.similarity === 1))
{
autoApplied = true;
}
if (autoApplied)
{
inputReplacement.style.backgroundColor = "#c8e6c9";
inputReplacement.title = "Reemplazo automático aplicado (palabra especial con 100% similitud)";
}
else if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded'))
{
inputReplacement.style.backgroundColor = "#fff3cd";
inputReplacement.title = "Contiene palabra especial reemplazada";
}
function debounce(func, delay)
{
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
inputReplacement.addEventListener('input', debounce(() =>
{
if (inputReplacement.value.trim() !== original)
{
applyButton.disabled = false;
applyButton.style.color = "";
}
else
{
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
}, 300));
inputOriginal.addEventListener('input', debounce(() =>
{
}, 300));
const suggestionListCell = document.createElement("td");
suggestionListCell.style.padding = "4px";
suggestionListCell.style.width = "180px";
const suggestionContainer = document.createElement('div');
const palabrasYaProcesadas = new Set();
const currentPlaceSuggestions = allSuggestions[id];
if (currentPlaceSuggestions)
{
Object.entries(currentPlaceSuggestions).forEach(([originalWordForThisPlace, suggestionsArray]) =>
{
if (Array.isArray(suggestionsArray))
{
suggestionsArray.forEach(s =>
{
let icono = '';
let textoSugerencia = '';
let colorFondo = '#f9f9f9';
let esSugerenciaValida = false;
let palabraAReemplazar = originalWordForThisPlace;
let palabraAInsertar = s.word;
switch (s.fuente)
{
case 'original_preserved':
esSugerenciaValida = true;
icono = '⚙️';
textoSugerencia = `¿"${originalWordForThisPlace}" x "${s.word}"?`;
colorFondo = '#f0f0f0';
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = s.word;
break;
case 'excluded':
if (s.similarity < 1 || (s.similarity === 1 && originalWordForThisPlace.toLowerCase() !== s.word.toLowerCase()))
{
esSugerenciaValida = true;
icono = '🏷️';
textoSugerencia = `¿"${originalWordForThisPlace}" x "${s.word}"? (sim. ${(s.similarity * 100).toFixed(0)}%)`;
colorFondo = '#f3f9ff';
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = s.word;
palabrasYaProcesadas.add(originalWordForThisPlace.toLowerCase());
}
break;
case 'dictionary':
esSugerenciaValida = true;
icono = '📘';
colorFondo = '#e6ffe6';
const normalizedSuggestedWordForDisplay = normalizeWordInternal(s.word, true, false);
textoSugerencia = `¿"${originalWordForThisPlace}" x "${normalizedSuggestedWordForDisplay}"? (sim. ${(s.similarity * 100).toFixed(0)}%)`;
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = normalizedSuggestedWordForDisplay;
break;
case 'dictionary_tilde':
esSugerenciaValida = true;
icono = '✍️';
colorFondo = '#ffe6e6';
textoSugerencia = `¿"${originalWordForThisPlace}" x "${s.word}"? (Corregir Tilde)`;
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = s.word;
break;
}
if (esSugerenciaValida)
{
const suggestionDiv = document.createElement("div");
suggestionDiv.innerHTML = `${icono} ${textoSugerencia}`;
suggestionDiv.style.cursor = "pointer";
suggestionDiv.style.padding = "2px 4px";
suggestionDiv.style.margin = "2px 0";
suggestionDiv.style.border = "1px solid #ddd";
suggestionDiv.style.borderRadius = "3px";
suggestionDiv.style.backgroundColor = colorFondo;
suggestionDiv.addEventListener("click", () =>
{
const currentSuggestedValue = inputReplacement.value;
const searchRegex = new RegExp("\\b" + escapeRegExp(palabraAReemplazar) + "\\b", "gi");
const newSuggestedValue = currentSuggestedValue.replace(searchRegex, palabraAInsertar);
if (inputReplacement.value !== newSuggestedValue)
{
inputReplacement.value = newSuggestedValue;
}
checkAndUpdateApplyButton();
});
suggestionContainer.appendChild(suggestionDiv);
}
});
}
else
{
console.warn(`[WME_PLN][DEBUG] suggestionsArray para "${originalWordForThisPlace}" no es un array o es undefined:`, suggestionsArray);
}
});
}
suggestionListCell.appendChild(suggestionContainer);
row.appendChild(suggestionCell);
row.appendChild(suggestionListCell);
const categoryCell = document.createElement("td");
categoryCell.style.padding = "4px";
categoryCell.style.width = "130px";
categoryCell.style.textAlign = "center";
const currentCategoryDiv = document.createElement("div");
currentCategoryDiv.style.display = "flex";
currentCategoryDiv.style.flexDirection = "column";
currentCategoryDiv.style.alignItems = "center";
currentCategoryDiv.style.gap = "2px";
const currentCategoryText = document.createElement("span");
currentCategoryText.textContent = currentCategoryTitle;
currentCategoryText.title = `Categoría Actual: ${currentCategoryTitle}`;
currentCategoryDiv.appendChild(currentCategoryText);
const currentCategoryIconDisplay = document.createElement("span");
currentCategoryIconDisplay.textContent = currentCategoryIcon;
currentCategoryIconDisplay.style.fontSize = "20px";
currentCategoryDiv.appendChild(currentCategoryIconDisplay);
categoryCell.appendChild(currentCategoryDiv);
row.appendChild(categoryCell);
const recommendedCategoryCell = document.createElement("td");
recommendedCategoryCell.style.padding = "4px";
recommendedCategoryCell.style.width = "180px";
recommendedCategoryCell.style.textAlign = "left";
const categoryDropdown = createRecommendedCategoryDropdown(
id,
currentCategoryKey,
dynamicCategorySuggestions
);
recommendedCategoryCell.appendChild(categoryDropdown);
row.appendChild(recommendedCategoryCell);
const actionCell = document.createElement("td");
actionCell.style.padding = "4px";
actionCell.style.width = "120px";
const buttonGroup = document.createElement("div");
buttonGroup.style.display = "flex";
buttonGroup.style.flexDirection = "column";
buttonGroup.style.gap = "4px";
buttonGroup.style.alignItems = "flex-start";
const commonButtonStyle = {
width: "40px",
height: "30px",
minWidth: "40px",
minHeight: "30px",
padding: "4px",
border: "1px solid #ccc",
borderRadius: "4px",
backgroundColor: "#f0f0f0",
color: "#555",
cursor: "pointer",
fontSize: "18px",
display: "flex",
justifyContent: "center",
alignItems: "center",
boxSizing: "border-box"
};
const applyButton = document.createElement("button");
Object.assign(applyButton.style, commonButtonStyle);
applyButton.textContent = "✔";
applyButton.title = "Aplicar sugerencia";
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
const applyButtonWrapper = document.createElement("div");
applyButtonWrapper.style.display = "flex";
applyButtonWrapper.style.alignItems = "center";
applyButtonWrapper.style.gap = "5px";
applyButtonWrapper.appendChild(applyButton);
buttonGroup.appendChild(applyButtonWrapper);
let deleteButton = document.createElement("button");
Object.assign(deleteButton.style, commonButtonStyle);
deleteButton.textContent = "🗑️";
deleteButton.title = "Eliminar lugar";
const deleteButtonWrapper = document.createElement("div");
Object.assign(deleteButtonWrapper.style, {
display: "flex",
alignItems: "center",
gap: "5px"
});
deleteButtonWrapper.appendChild(deleteButton);
buttonGroup.appendChild(deleteButtonWrapper);
const addToExclusionBtn = document.createElement("button");
Object.assign(addToExclusionBtn.style, commonButtonStyle);
addToExclusionBtn.textContent = "🏷️";
addToExclusionBtn.title = "Marcar palabra como especial (no se modifica)";
buttonGroup.appendChild(addToExclusionBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
const excludePlaceBtn = document.createElement("button");
Object.assign(excludePlaceBtn.style, commonButtonStyle);
excludePlaceBtn.textContent = "📵";
excludePlaceBtn.title = "Excluir este lugar (no aparecerá en futuras búsquedas)";
buttonGroup.appendChild(excludePlaceBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
applyButton.addEventListener("click", async () =>
{
const venueObj = W.model.venues.getObjectById(id);
if (!venueObj)
{
console.error("[WME_PLN] Error: El lugar no está disponible o ya fue eliminado.");
return;
}
const newName = inputReplacement.value.trim();
const currentLiveNameInWaze = venueObj?.attributes?.name?.value || venueObj?.attributes?.name || "";
const row = applyButton.closest('tr');
const nameWasChanged = (newName !== currentLiveNameInWaze);
const categoryWasChanged = row.dataset.categoryChanged === 'true';
try
{
if (nameWasChanged)
{
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(venueObj, { name: newName });
W.model.actionManager.add(action);
showTemporaryMessage("Lugar normalizado. Presione 'Guardar' para finalizar.", 4000, 'success');
recordNormalizationEvent();
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.fontSize = "20px";
applyButtonWrapper.appendChild(successIcon);
row.dataset.categoryChanged = 'false';
}
else if (categoryWasChanged)
{
showTemporaryMessage("Cambios de categoría aplicados correctamente.", 3000, 'success');
recordNormalizationEvent();
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.fontSize = "20px";
applyButtonWrapper.appendChild(successIcon);
row.dataset.categoryChanged = 'false';
}
else
{
showTemporaryMessage("No hay cambios para aplicar.", 3000, 'warning');
}
if (nameWasChanged || categoryWasChanged)
{
markRowAsProcessed(row, 'applied');
updateInconsistenciesCount(-1);
}
}
catch (e)
{
alert("Error al actualizar: " + e.message);
console.error("[WME_PLN] Error al actualizar lugar:", e);
}
});
deleteButton.addEventListener("click", () =>
{
const confirmModal = document.createElement("div");
confirmModal.style.position = "fixed";
confirmModal.style.top = "50%";
confirmModal.style.left = "50%";
confirmModal.style.transform = "translate(-50%, -50%)";
confirmModal.style.background = "#fff";
confirmModal.style.border = "1px solid #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000";
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
confirmModal.appendChild(iconElement);
const message = document.createElement("div");
const venue = W.model.venues.getObjectById(id);
const placeName = venue?.attributes?.name?.value || venue?.attributes?.name || "este lugar";
message.innerHTML = `<b>¿Eliminar "${placeName}"?</b>`;
message.style.fontSize = "20px";
message.style.marginBottom = "8px";
confirmModal.appendChild(message);
const nameDiv = document.createElement("div");
nameDiv.textContent = `"${placeName}"`;
nameDiv.style.fontSize = "15px";
nameDiv.style.color = "#007bff";
nameDiv.style.marginBottom = "18px";
confirmModal.appendChild(nameDiv);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "7px 18px";
cancelBtn.style.background = "#eee";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => confirmModal.remove());
const confirmBtn = document.createElement("button");
confirmBtn.textContent = "Eliminar";
confirmBtn.style.padding = "7px 18px";
confirmBtn.style.background = "#d9534f";
confirmBtn.style.color = "#fff";
confirmBtn.style.border = "none";
confirmBtn.style.borderRadius = "4px";
confirmBtn.style.cursor = "pointer";
confirmBtn.style.fontWeight = "bold";
confirmBtn.addEventListener("click", () =>
{
const venue = W.model.venues.getObjectById(id);
if (!venue)
{
console.error("[WME_PLN]El lugar no está disponible o ya fue eliminado.");
confirmModal.remove();
return;
}
try
{
const DeleteObject = require("Waze/Action/DeleteObject");
const action = new DeleteObject(venue);
W.model.actionManager.add(action);
recordNormalizationEvent();
const row = deleteButton.closest('tr');
markRowAsProcessed(row, 'deleted');
updateInconsistenciesCount(-1);
deleteButton.disabled = true;
deleteButton.style.color = "#bbb";
deleteButton.style.opacity = "0.5";
applyButton.disabled = true;
applyButton.style.color = "#bbb";
applyButton.style.opacity = "0.5";
const successIcon = document.createElement("span");
successIcon.textContent = " 🗑️";
successIcon.style.marginLeft = "0";
successIcon.style.fontSize = "20px";
deleteButtonWrapper.appendChild(successIcon);
}
catch (e)
{
console.error("[WME_PLN] Error al eliminar lugar: " + e.message, e);
}
confirmModal.remove();
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal);
});
addToExclusionBtn.addEventListener("click", () =>
{
const words = original.split(/\s+/);
const modal = document.createElement("div");
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.background = "#fff";
modal.style.border = "1px solid #aad";
modal.style.padding = "28px 32px 20px 32px";
modal.style.zIndex = "20000";
modal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
modal.style.fontFamily = "sans-serif";
modal.style.borderRadius = "10px";
modal.style.textAlign = "center";
modal.style.minWidth = "340px";
const title = document.createElement("h4");
title.textContent = "Agregar palabra a especiales";
modal.appendChild(title);
const instructions = document.createElement("p");
const list = document.createElement("ul");
list.style.listStyle = "none";
list.style.padding = "0";
words.forEach(w =>
{
if (w.trim() === '') return;
const lowerW = w.trim().toLowerCase();
if (!/[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9]/.test(lowerW) || /^[^a-zA-Z0-9]+$/.test(lowerW)) return;
const alreadyExists = Array.from(excludedWords).some(existing => existing.toLowerCase() === lowerW);
if (commonWords.includes(lowerW) || alreadyExists) return;
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = w;
checkbox.id = `cb-exc-${w.replace(/[^a-zA-Z0-9]/g, "")}`;
li.appendChild(checkbox);
const label = document.createElement("label");
label.htmlFor = checkbox.id;
label.appendChild(document.createTextNode(" " + w));
li.appendChild(label);
list.appendChild(li);
});
modal.appendChild(list);
const confirmBtn = document.createElement("button");
confirmBtn.textContent = "Añadir Seleccionadas";
confirmBtn.addEventListener("click", () =>
{
const checked = modal.querySelectorAll("input[type=checkbox]:checked");
let wordsActuallyAdded = false;
checked.forEach(c =>
{
if (!excludedWords.has(c.value))
{
excludedWords.add(c.value);
wordsActuallyAdded = true;
}
});
if (wordsActuallyAdded)
{
if (typeof renderExcludedWordsList === 'function')
{
const excludedListElement = document.getElementById("excludedWordsList");
if (excludedListElement)
{
renderExcludedWordsList(excludedListElement);
}
else
{
renderExcludedWordsList();
}
}
}
modal.remove();
if (wordsActuallyAdded)
{
saveExcludedWordsToLocalStorage();
showTemporaryMessage("Palabra(s) añadida(s) a especiales y guardada(s).", 3000, 'success');
}
else
{
showTemporaryMessage("No se seleccionaron palabras o ya estaban en la lista.", 3000, 'info');
}
});
modal.appendChild(confirmBtn);
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.marginLeft = "8px";
cancelBtn.addEventListener("click", () => modal.remove());
modal.appendChild(cancelBtn);
document.body.appendChild(modal);
});
buttonGroup.appendChild(addToExclusionBtn);
// Exclude Place Button
excludePlaceBtn.addEventListener("click", () =>
{
const placeName = original || `ID: ${id}`;
const confirmModal = document.createElement("div");
confirmModal.style.position = "fixed";
confirmModal.style.top = "50%";
confirmModal.style.left = "50%";
confirmModal.style.transform = "translate(-50%, -50%)";
confirmModal.style.background = "#fff";
confirmModal.style.border = "1px solid #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000";
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
const iconElement = document.createElement("div");
iconElement.innerHTML = "🚫";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
confirmModal.appendChild(iconElement);
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>¿Excluir "${placeName}"?</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
confirmModal.appendChild(messageTitle);
const explanationDiv = document.createElement("div");
explanationDiv.textContent = `Este lugar no aparecerá en futuras búsquedas del normalizador.`;
explanationDiv.style.fontSize = "15px";
explanationDiv.style.color = "#555";
explanationDiv.style.marginBottom = "18px";
confirmModal.appendChild(explanationDiv);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "7px 18px";
cancelBtn.style.background = "#eee";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => confirmModal.remove());
const confirmExcludeBtn = document.createElement("button");
confirmExcludeBtn.textContent = "Excluir";
confirmExcludeBtn.style.padding = "7px 18px";
confirmExcludeBtn.style.background = "#d9534f";
confirmExcludeBtn.style.color = "#fff";
confirmExcludeBtn.style.border = "none";
confirmExcludeBtn.style.borderRadius = "4px";
confirmExcludeBtn.style.cursor = "pointer";
confirmExcludeBtn.style.fontWeight = "bold";
// Acción al confirmar exclusión
confirmExcludeBtn.addEventListener("click", () =>
{
excludedPlaces.set(id, placeName);
saveExcludedPlacesToLocalStorage();
showTemporaryMessage("Lugar excluido de futuras búsquedas.", 3000, 'success');
// Marcar la fila como procesada y actualizar el contador
const row = excludePlaceBtn.closest('tr');
if (row) {
markRowAsProcessed(row, 'excluded');
updateInconsistenciesCount(-1);
}
confirmModal.remove();
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmExcludeBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal);
});
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
row.style.borderBottom = "1px solid #ddd";
row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff";
row.querySelectorAll("td").forEach(td =>
{
td.style.verticalAlign = "top";
});
tbody.appendChild(row);
checkAndUpdateApplyButton();
setTimeout(() =>
{
const progress = Math.floor(((index + 1) / inconsistents.length) * 100);
const progressElem = document.getElementById("scanProgressText");
if (progressElem)
{
progressElem.textContent = `Analizando lugares: ${progress}% (${index + 1}/${inconsistents.length})`;
}
}, 0);
});
table.appendChild(tbody);
if (window.plnPruneObserver) {
// Desconectar de cualquier observación anterior para estar seguros
window.plnPruneObserver.disconnect();
// Conectar el observador para que vigile ÚNICAMENTE la tabla de resultados
window.plnPruneObserver.observe(tbody, {
childList: true, // Observar si se añaden/quitan filas
subtree: true, // Observar cambios dentro de las filas
attributes: true // Observar cambios en atributos (como 'disabled')
});
}
output.appendChild(table);
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
{
existingOverlay.remove();
}
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent = `Progreso: 100% (${inconsistents.length}/${placesArr.length})`;
}
function reactivateAllActionButtons()
{
document.querySelectorAll("#wme-place-inspector-output button")
.forEach(btn =>
{
btn.disabled = false;
btn.style.color = "";
btn.style.opacity = "";
});
}
W.model.actionManager.events.register("afterundoaction", null, () =>
{
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons, 250);
});
}
else
{
//console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
});
W.model.actionManager.events.register("afterredoaction", null, () =>
{
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons, 250);
});
}
else
{
// console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
});
}
}// renderPlacesInFloatingPanel
// Normaliza una palabra eliminando diacríticos (tildes) y caracteres especiales
function getLevenshteinDistance(a, b)
{
const matrix = Array.from(
{ length : b.length + 1 },
(_, i) => Array.from({ length : a.length + 1 },(_, j) => (i === 0 ? j : (j === 0 ? i : 0))));
for (let i = 1; i <= b.length; i++)
{
for (let j = 1; j <= a.length; j++)
{
if (b.charAt(i - 1) === a.charAt(j - 1))
{
matrix[i][j] = matrix[i - 1][j - 1];
}
else
{
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + 1 // substitution
);
}
}
}
return matrix[b.length][a.length];
}// getLevenshteinDistance
// Normaliza una palabra eliminando caracteres especiales y convirtiéndola a minúsculas
function calculateSimilarity(word1, word2)
{
const w1_lower = word1.toLowerCase();
const w2_lower = word2.toLowerCase();
// Si las palabras son diferentes, pero al quitarles las tildes son idénticas,
// dales una similitud muy alta (99%) para priorizarlas siempre.
if (w1_lower !== w2_lower && removeDiacritics(w1_lower) === removeDiacritics(w2_lower))
{
return 0.99; // Prioridad máxima para corrección de tildes
}
// Si no es un caso de tildes, procede con el cálculo normal de Levenshtein.
const distance = getLevenshteinDistance(w1_lower, w2_lower);
const maxLen = Math.max(w1_lower.length, w2_lower.length);
if (maxLen === 0) return 1;
return 1 - distance / maxLen;
}// calculateSimilarity
// Verifica si una fecha de edición está dentro del rango especificado
function isDateWithinRange(editDate, filterRange)
{
if (!(editDate instanceof Date) || isNaN(editDate))
{
console.warn("[WME PLN] Se proporcionó una fecha de edición inválida a isDateWithinRange.");
return false; // No se puede comparar una fecha inválida.
}
const now = new Date();
let cutoffDate = new Date();
switch (filterRange)
{
case "all": // Si es "Elegir una opción", siempre se cumple la condición
return true;
case "6_months":
cutoffDate.setMonth(now.getMonth() - 6);
break;
case "3_months":
cutoffDate.setMonth(now.getMonth() - 3);
break;
case "1_month":
cutoffDate.setMonth(now.getMonth() - 1);
break;
case "1_week":
cutoffDate.setDate(now.getDate() - 7);
break;
case "1_day":
cutoffDate.setDate(now.getDate() - 1);
break;
default:
return true; // Si el filtro es desconocido, por seguridad no se filtra.
}
return editDate >= cutoffDate;
}//isDateWithinRange
// Encuentra palabras similares a una palabra dada en una lista o array indexado
function findSimilarWords(word, indexedListOrArray, threshold)
{
const lowerWord = word.toLowerCase();
const firstChar = lowerWord.charAt(0);
let candidates = [];
// Si el segundo argumento es un objeto literal (como window.dictionaryIndex)
if (indexedListOrArray && typeof indexedListOrArray === 'object' && !Array.isArray(indexedListOrArray) && indexedListOrArray[firstChar])
{
candidates = Array.from(indexedListOrArray[firstChar] || []);
}
// Si es un Set o Array (menos óptimo, pero fallback)
else if (indexedListOrArray instanceof Set || Array.isArray(indexedListOrArray))
{
candidates = Array.from(indexedListOrArray).filter(candidate => {
// CORREGIDO: Extraer la palabra si es un objeto
const candidateWord = typeof candidate === 'object' ? candidate.word : candidate;
// CORREGIDO: Asegurar que es una string antes de llamar a charAt
return typeof candidateWord === 'string' && candidateWord.charAt(0).toLowerCase() === firstChar;
});
}
else
{
return [];
}
return candidates
.map(candidate =>
{
// CORREGIDO: Extraer la palabra si es un objeto
const candidateWord = typeof candidate === 'object' ? candidate.word : candidate;
// CORREGIDO: Asegurar que es una string antes de llamar a toLowerCase
const candidateLower = typeof candidateWord === 'string' ? candidateWord.toLowerCase() : '';
const similarity = calculateSimilarity(lowerWord, candidateLower);
return { word: candidateWord, similarity };
})
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity);
}// findSimilarWords
// Sugiere palabras excluidas basadas en el nombre actual y las palabras excluidas
function suggestExcludedReplacements(currentName, excludedWords)
{
const words = currentName.split(/\s+/);
const suggestions = {};
const threshold = parseFloat(document.getElementById("similarityThreshold")?.value || "85") / 100;
words.forEach(word =>
{
const similar = findSimilarWords(word, Array.from(excludedWords), threshold);
if (similar.length > 0)
{
suggestions[word] = similar;
}
});
return suggestions;
}// suggestExcludedReplacements
// Reset del inspector: progreso y texto de tab
function resetInspectorState()
{
const inner = document.getElementById("progressBarInnerTab");
const text = document.getElementById("progressBarTextTab");
const outputTab = document.getElementById("wme-normalization-tab-output");
if (inner)
inner.style.width = "0%";
if (text)
text.textContent = `Progreso: 0% (0/0)`;
if (outputTab)
outputTab.textContent = "Presiona 'Start Scan...' para analizar los lugares visibles.";
}// resetInspectorState
// Función auxiliar para marcar una fila de la tabla como procesada/eliminada
// Función auxiliar para marcar una fila de la tabla como procesada/eliminada
function markRowAsProcessed(rowElement, actionType) {
if (!rowElement) return;
// Estilos para atenuar y tachar la fila
rowElement.style.opacity = '0.4';
rowElement.style.textDecoration = 'line-through';
rowElement.style.transition = 'opacity 0.5s ease'; // Transición suave
// NUEVA LÓGICA: Verificar estado del botón toggle y ocultar si es necesario
const toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (toggleBtn && toggleBtn.dataset.state === 'hidden') {
// Si el botón está en estado "hidden" (lo que significa que los elementos procesados deben ocultarse)
rowElement.classList.add('pln-hidden-normalized');
}
// Deshabilitar todos los botones de acción en esta fila
const buttons = rowElement.querySelectorAll('button');
buttons.forEach(btn => {
btn.disabled = true;
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.3';
});
// Opcional: Mostrar un pequeño icono de confirmación en la fila
const numberCell = rowElement.querySelector('td:first-child');
if (numberCell) {
let icon = '';
let tooltip = '';
if (actionType === 'applied') {
icon = '✓';
tooltip = 'Cambios aplicados';
} else if (actionType === 'deleted') {
icon = '🗑️';
tooltip = 'Lugar eliminado';
} else if (actionType === 'excluded') {
icon = '🚫';
tooltip = 'Lugar excluido';
}
if (icon) {
const iconSpan = document.createElement('span');
iconSpan.textContent = ' ' + icon;
iconSpan.title = tooltip;
iconSpan.style.marginLeft = '3px';
iconSpan.style.fontSize = '12px';
numberCell.appendChild(iconSpan);
}
}
}// markRowAsProcessed
// Muestra un mensaje temporal en la parte superior de la pantalla
function showTemporaryMessage(message, duration = 3000, type = 'info')
{
// Crear el elemento del popup
const popup = document.createElement('div');
popup.textContent = message;
// Estilos base para el popup
popup.style.position = 'fixed';
popup.style.top = '70px';
popup.style.left = '50%';
popup.style.transform = 'translateX(-50%)';
popup.style.padding = '12px 25px';
popup.style.borderRadius = '6px';
popup.style.color = 'white';
popup.style.fontWeight = 'bold';
popup.style.zIndex = '25000'; // Un z-index muy alto para que esté por encima de todo
popup.style.boxShadow = '0 4px 10px rgba(0,0,0,0.15)';
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.4s ease, top 0.4s ease'; // Animación suave
// Estilos según el tipo de mensaje
switch (type)
{
case 'success':
popup.style.backgroundColor = '#28a745'; // Verde
break;
case 'warning':
popup.style.backgroundColor = '#ffc107'; // Amarillo/Naranja
popup.style.color = '#212529'; // Texto oscuro para mejor contraste sobre amarillo
break;
case 'error':
popup.style.backgroundColor = '#dc3545'; // Rojo
break;
default: // 'info'
popup.style.backgroundColor = '#17a2b8'; // Azul
break;
}
// Añadir el popup al body y animar su entrada
document.body.appendChild(popup);
setTimeout(() =>
{
popup.style.opacity = '1';
popup.style.top = '90px'; // Se desliza hacia abajo
}, 50);
// Configurar la animación de salida y eliminación del DOM
setTimeout(() =>
{
popup.style.opacity = '0';
popup.style.top = '70px'; // Se desliza hacia arriba
// Eliminar el elemento del DOM después de que termine la transición
popup.addEventListener('transitionend', () => popup.remove());
// Agregamos un fallback por si el evento transitionend no se dispara
setTimeout(() =>
{
if (popup.parentElement)
{
popup.remove();
}
}, 500);
}, duration);
}// showTemporaryMessage
//Permite crear un panel flotante para mostrar los resultados del escaneo
function createFloatingPanel(status = "processing", numInconsistents = 0)
{
if (!floatingPanelElement)
{
floatingPanelElement = document.createElement("div");
floatingPanelElement.id = "wme-place-inspector-panel";
floatingPanelElement.style.position = "fixed";
floatingPanelElement.style.zIndex = "10005"; // Z-INDEX DEL PANEL DE RESULTADOS
floatingPanelElement.style.background = "#fff";
floatingPanelElement.style.border = "1px solid #ccc";
floatingPanelElement.style.borderRadius = "8px";
floatingPanelElement.style.boxShadow = "0 5px 15px rgba(0,0,0,0.2)";
floatingPanelElement.style.padding = "10px";
floatingPanelElement.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
floatingPanelElement.style.display = 'none';
floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s"; // Agregado left y top a la transición
floatingPanelElement.style.overflow = "hidden";
// Variables para almacenar el estado del panel
floatingPanelElement._isMaximized = false;
floatingPanelElement._isMinimized = false;
floatingPanelElement._originalState = {};
floatingPanelElement._isDragging = false;
floatingPanelElement._currentStatus = status;
// Crear barra de título con controles
const titleBar = document.createElement("div");
titleBar.style.display = "flex";
titleBar.style.justifyContent = "space-between";
titleBar.style.alignItems = "center";
titleBar.style.marginBottom = "10px";
titleBar.style.userSelect = "none";
titleBar.style.cursor = "move";
titleBar.style.padding = "5px 0";
// Título del panel
const titleElement = document.createElement("h4");
titleElement.id = "wme-pln-panel-title";
titleElement.style.margin = "0";
titleElement.style.fontSize = "20px";
titleElement.style.color = "#333";
titleElement.style.fontWeight = "bold";
titleElement.style.flex = "1";
titleElement.style.textAlign = "center";
// Contenedor de controles estilo macOS
const controlsContainer = document.createElement("div");
controlsContainer.style.display = "flex";
controlsContainer.style.gap = "8px";
controlsContainer.style.alignItems = "center";
controlsContainer.style.position = "absolute";
controlsContainer.style.left = "15px";
controlsContainer.style.top = "15px";
// Función para crear botones estilo macOS
function createMacButton(color, action, tooltip) {
const btn = document.createElement("div");
btn.style.width = "12px";
btn.style.height = "12px";
btn.style.borderRadius = "50%";
btn.style.backgroundColor = color;
btn.style.cursor = "pointer";
btn.style.border = "1px solid rgba(0,0,0,0.1)";
btn.style.display = "flex";
btn.style.alignItems = "center";
btn.style.justifyContent = "center";
btn.style.fontSize = "8px";
btn.style.color = "rgba(0,0,0,0.6)";
btn.style.transition = "all 0.2s";
btn.title = tooltip;
// Efectos hover
btn.addEventListener("mouseenter", () => {
btn.style.transform = "scale(1.1)";
if (color === "#ff5f57") btn.textContent = "×";
else if (color === "#ffbd2e") btn.textContent = "−";
else if (color === "#28ca42") btn.textContent = action === "maximize" ? "⬜" : "🗗";
});
btn.addEventListener("mouseleave", () => {
btn.style.transform = "scale(1)";
btn.textContent = "";
});
btn.addEventListener("click", action);
return btn;
}
// Botón cerrar (rojo)
const closeBtn = createMacButton("#ff5f57", () => {
if (floatingPanelElement._currentStatus === "processing") {
// Confirmar cancelación de búsqueda
const confirmCancel = confirm("¿Estás seguro de que quieres detener la búsqueda en progreso?");
if (!confirmCancel) return;
// Aquí puedes agregar lógica para cancelar la búsqueda actual
// Por ejemplo, detener cualquier proceso en curso
resetInspectorState();
}
if (floatingPanelElement) floatingPanelElement.style.display = 'none';
resetInspectorState();
}, "Cerrar panel");
// Botón minimizar (amarillo)
const minimizeBtn = createMacButton("#ffbd2e", () => {
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
if (!floatingPanelElement._isMinimized) {
// Guardar estado actual antes de minimizar
floatingPanelElement._originalState = {
width: floatingPanelElement.style.width,
height: floatingPanelElement.style.height,
top: floatingPanelElement.style.top,
left: floatingPanelElement.style.left,
transform: floatingPanelElement.style.transform,
outputHeight: outputDiv ? outputDiv.style.height : 'auto'
};
// Minimizar - mover a la parte superior
floatingPanelElement.style.top = "20px";
floatingPanelElement.style.left = "50%";
floatingPanelElement.style.transform = "translateX(-50%)";
floatingPanelElement.style.height = "50px";
floatingPanelElement.style.width = "300px";
if (outputDiv) outputDiv.style.display = "none";
floatingPanelElement._isMinimized = true;
updateButtonVisibility();
} else {
// Restaurar desde minimizado
const originalState = floatingPanelElement._originalState;
floatingPanelElement.style.width = originalState.width;
floatingPanelElement.style.height = originalState.height;
floatingPanelElement.style.top = originalState.top;
floatingPanelElement.style.left = originalState.left;
floatingPanelElement.style.transform = originalState.transform;
if (outputDiv) {
outputDiv.style.display = "block";
outputDiv.style.height = originalState.outputHeight;
}
floatingPanelElement._isMinimized = false;
updateButtonVisibility();
}
}, "Minimizar panel");
// Botón maximizar (verde)
// const maximizeBtn = createMacButton("#28ca42", () => {
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
// Función para actualizar visibilidad de botones
// Replace the updateButtonVisibility function in createFloatingPanel
function updateButtonVisibility()
{
const isProcessing = floatingPanelElement._currentStatus === "processing";
// Limpiar contenedor
controlsContainer.innerHTML = "";
if (isProcessing) {
// Solo botón cerrar durante la búsqueda
controlsContainer.appendChild(closeBtn);
} else if (floatingPanelElement._isMinimized) {
// Minimizado: cerrar y restaurar
controlsContainer.appendChild(closeBtn);
// Crear botón de restaurar si estamos minimizados
const restoreBtn = createMacButton("#28ca42", () => {
// Restaurar desde minimizado
const originalState = floatingPanelElement._originalState;
floatingPanelElement.style.width = originalState.width;
floatingPanelElement.style.height = originalState.height;
floatingPanelElement.style.top = originalState.top;
floatingPanelElement.style.left = originalState.left;
floatingPanelElement.style.transform = originalState.transform;
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
if (outputDiv) {
outputDiv.style.display = "block";
outputDiv.style.height = originalState.outputHeight;
}
floatingPanelElement._isMinimized = false;
updateButtonVisibility();
}, "Restaurar panel");
restoreBtn.textContent = "🗗";
controlsContainer.appendChild(restoreBtn);
} else {
// Normal: cerrar y minimizar
controlsContainer.appendChild(closeBtn);
controlsContainer.appendChild(minimizeBtn);
}
}// updateButtonVisibility
// Funcionalidad de arrastrar
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
titleBar.addEventListener("mousedown", (e) => {
if (e.target === titleBar || e.target === titleElement) {
isDragging = true;
const rect = floatingPanelElement.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
floatingPanelElement.style.transition = "none";
e.preventDefault();
}
});
document.addEventListener("mousemove", (e) => {
if (isDragging && !floatingPanelElement._isMaximized) {
const newLeft = e.clientX - dragOffset.x;
const newTop = e.clientY - dragOffset.y;
floatingPanelElement.style.left = `${newLeft}px`;
floatingPanelElement.style.top = `${newTop}px`;
floatingPanelElement.style.transform = "none";
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s";
}
});
// Agregar controles y título a la barra
titleBar.appendChild(controlsContainer);
titleBar.appendChild(titleElement);
// Agregar barra de título al panel
floatingPanelElement.appendChild(titleBar);
// Contenido del panel
const outputDivLocal = document.createElement("div");
outputDivLocal.id = "wme-place-inspector-output";
outputDivLocal.style.fontSize = "18px";
outputDivLocal.style.backgroundColor = "#fdfdfd";
outputDivLocal.style.overflowY = "auto";
outputDivLocal.style.flex = "1";
floatingPanelElement.appendChild(outputDivLocal);
// Función para actualizar botones (hacer accesible)
floatingPanelElement._updateButtonVisibility = updateButtonVisibility;
document.body.appendChild(floatingPanelElement);
}
// Actualizar estado actual
floatingPanelElement._currentStatus = status;
// Referencias a elementos existentes
const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title");
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
// Limpiar contenido
if(outputDiv) outputDiv.innerHTML = "";
// Actualizar visibilidad de botones
if (floatingPanelElement._updateButtonVisibility) {
floatingPanelElement._updateButtonVisibility();
}
// Configurar según el estado
if (status === "processing")
{
// Solo actualizar si no está maximizado o minimizado
if (!floatingPanelElement._isMaximized && !floatingPanelElement._isMinimized) {
floatingPanelElement.style.width = processingPanelDimensions.width;
floatingPanelElement.style.height = processingPanelDimensions.height;
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "50%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
if(outputDiv && !floatingPanelElement._isMinimized) {
outputDiv.style.height = floatingPanelElement._isMaximized ? "calc(100vh - 100px)" : "150px";
outputDiv.style.display = "block";
}
if(titleElement) titleElement.textContent = "Buscando...";
if (outputDiv && !floatingPanelElement._isMinimized)
{
outputDiv.innerHTML = "<div style='display:flex; align-items:center; justify-content:center; height:100%;'><span class='loader-spinner' style='width:32px; height:32px; border:4px solid #ccc; border-top:4px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span></div>";
}
}
else
{ // status === "results"
// Solo actualizar si no está maximizado o minimizado
if (!floatingPanelElement._isMaximized && !floatingPanelElement._isMinimized) {
floatingPanelElement.style.width = resultsPanelDimensions.width;
floatingPanelElement.style.height = resultsPanelDimensions.height;
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "60%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
if(outputDiv && !floatingPanelElement._isMinimized) {
outputDiv.style.height = floatingPanelElement._isMaximized ? "calc(100vh - 100px)" : "660px";
outputDiv.style.display = "block";
}
if(titleElement) titleElement.textContent = "NrmliZer: Resultados";
// --- BOTÓN MOSTRAR/OCULTAR NORMALIZADOS ---
let showHidden = false;
let toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (!toggleBtn) {
toggleBtn = document.createElement('button');
toggleBtn.id = 'pln-toggle-hidden-btn';
toggleBtn.textContent = 'Mostrar normalizados';
toggleBtn.style.marginLeft = '12px';
toggleBtn.style.padding = '4px 10px';
toggleBtn.style.fontSize = '12px';
toggleBtn.style.border = '1px solid #bbb';
toggleBtn.style.borderRadius = '5px';
toggleBtn.style.background = '#f4f4f4';
toggleBtn.style.cursor = 'pointer';
toggleBtn.addEventListener('click', () => {
showHidden = !showHidden;
if (showHidden) {
// Mostrar lo oculto
const st = document.getElementById('pln-hide-style'); if (st) st.remove();
document.querySelectorAll('tr.pln-hidden-normalized')
.forEach(tr => tr.classList.remove('pln-hidden-normalized'));
toggleBtn.textContent = 'Ocultar normalizados';
} else {
// Volver a ocultar normalizados
if (!document.getElementById('pln-hide-style')) {
const st = document.createElement('style');
st.id = 'pln-hide-style';
st.textContent = `tr.pln-hidden-normalized{display:none !important;}`;
document.head.appendChild(st);
}
document.querySelectorAll('tr').forEach(tr => {
// Reaplicar la lógica de ocultar si corresponde
if (tr.dataset && tr.dataset.placeId) {
// Si la fila ya estaba normalizada, volver a ocultarla
// (esto depende de lógica de marcado, aquí solo se vuelve a aplicar la clase si no tiene cambios)
// Si quieres forzar el ocultamiento, puedes volver a llamar a processAll() si la tienes global
if (typeof window.__plnHideNormalizedRows === 'function') {
window.__plnHideNormalizedRows();
}
}
});
toggleBtn.textContent = 'Mostrar normalizados';
}
});
// Insertar el botón en la barra de título del panel
const titleBar = floatingPanelElement.querySelector('div');
if (titleBar) titleBar.appendChild(toggleBtn);
}
}
floatingPanelElement.style.display = 'flex';
floatingPanelElement.style.flexDirection = 'column';
}
// Escuchar el botón Guardar de WME para resetear el inspector
const wmeSaveBtn = document.querySelector(
"button.action.save, button[title='Guardar'], button[aria-label='Guardar']");
if (wmeSaveBtn)
{
wmeSaveBtn.addEventListener("click", () => {
// Ocultar el panel flotante de resultados si está visible
if (floatingPanelElement) {
floatingPanelElement.style.display = 'none';
}
// Resetear el estado del inspector en la pestaña lateral
resetInspectorState();
});
}
// Función para crear la pestaña lateral del script
function createSidebarTab()
{
try
{
// 1. Verificar si WME y la función para registrar pestañas están listos
if (!W || !W.userscripts || typeof W.userscripts.registerSidebarTab !== 'function')
{
console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral.");
return;
}
// 2. Registrar la pestaña principal del script en WME y obtener tabPane
let registration;
try
{
registration = W.userscripts.registerSidebarTab("NrmliZer"); // Nombre del Tab que aparece en WME
}
catch (e)
{
if (e.message.includes("already been registered"))
{
console.warn("[WME PLN] Tab 'NrmliZer' ya registrado. El script puede no funcionar como se espera si hay múltiples instancias.");
// Podrías intentar obtener el tabPane existente o simplemente
// retornar. Para evitar mayor complejidad, si ya está
// registrado, no continuaremos con la creación de la UI de la
// pestaña.
return;
}
throw e; // Relanzar otros errores para que se vean en consola
}
const { tabLabel, tabPane } = registration;
if (!tabLabel || !tabPane)
{
return;
}
// Configurar el ícono y nombre de la pestaña principal del script
// Corrección aquí: usar directamente MAIN_TAB_ICON_BASE64 en el src
tabLabel.innerHTML = `
<img src="${MAIN_TAB_ICON_BASE64}" style="height: 16px; vertical-align: middle; margin-right: 5px;">
NrmliZer
`;
// 3. Inicializar las pestañas internas (General, Especiales,
// Diccionario, Reemplazos)
const tabsContainer = document.createElement("div");
tabsContainer.style.display = "flex";
tabsContainer.style.marginBottom = "8px";
tabsContainer.style.gap = "8px";
const tabButtons = {};
const tabContents = {}; // Objeto para guardar los divs de contenido
// Crear botones para cada pestaña
tabNames.forEach(({ label, icon }) =>
{
const btn = document.createElement("button");
btn.innerHTML = icon
? `<span style="display: inline-flex; align-items: center; font-size: 11px;">
<span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label}
</span>`
: `<span style="font-size: 11px;">${label}</span>`;
btn.style.fontSize = "11px";
btn.style.padding = "4px 8px";
btn.style.marginRight = "4px";
btn.style.minHeight = "28px";
btn.style.border = "1px solid #ccc";
btn.style.borderRadius = "4px 4px 0 0";
btn.style.cursor = "pointer";
btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada
btn.className = "custom-tab-style";
// Agrega el tooltip personalizado para cada tab
if (label === "Gene") btn.title = "Configuración general";
else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)";
else if (label === "Dicc") btn.title = "Diccionario de palabras válidas";
else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos";
// Estilo inicial: la primera pestaña es la activa
if (label === tabNames[0].label)
{
btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco)
btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa
btn.style.fontWeight = "bold";
}
else
{
btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro)
btn.style.fontWeight = "normal";
}
btn.addEventListener("click", () =>
{
tabNames.forEach(({ label: tabLabel_inner }) =>
{
const isActive = (tabLabel_inner === label);
const currentButton = tabButtons[tabLabel_inner];
if (tabContents[tabLabel_inner])
{
tabContents[tabLabel_inner].style.display = isActive ? "block" : "none";
}
if (currentButton)
{
// Aplicar/Quitar estilos de pestaña activa directamente
if (isActive)
{
currentButton.style.backgroundColor = "#ffffff"; // Activo
currentButton.style.borderBottom = "2px solid #007bff";
currentButton.style.fontWeight = "bold";
}
else
{
currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo
currentButton.style.borderBottom = "none";
currentButton.style.fontWeight = "normal";
}
}
// Llamar a la función de renderizado correspondiente
if (isActive)
{
if (tabLabel_inner === "Espe")
{
const ul = document.getElementById("excludedWordsList");
if (ul && typeof renderExcludedWordsList === 'function') renderExcludedWordsList(ul);
}
else if (tabLabel_inner === "Dicc")
{
const ulDict = document.getElementById("dictionaryWordsList");
if (ulDict && typeof renderDictionaryList === 'function') renderDictionaryList(ulDict);
}
else if (tabLabel_inner === "Reemp")
{
const ulReemplazos = document.getElementById("replacementsListElementID");
if (ulReemplazos && typeof renderReplacementsList === 'function') renderReplacementsList(ulReemplazos);
}
}
});
});
tabButtons[label] = btn;
tabsContainer.appendChild(btn);
});
tabPane.appendChild(tabsContainer);
// Crear los divs contenedores para el contenido de cada pestaña
tabNames.forEach(({ label }) =>
{
const contentDiv = document.createElement("div");
contentDiv.style.display = label === tabNames[0].label ? "block" : "none"; // Mostrar solo la primera
contentDiv.style.padding = "10px";
tabContents[label] = contentDiv; // Guardar referencia
tabPane.appendChild(contentDiv);
});
// --- POBLAR EL CONTENIDO DE CADA PESTAÑA ---
// 4. Poblar el contenido de la pestaña "General"
const containerGeneral = tabContents["Gene"];
if (containerGeneral)
{
// Crear el contenedor principal
const mainTitle = document.createElement("h3");
mainTitle.textContent = "NormliZer";
mainTitle.style.textAlign = "center";
mainTitle.style.fontSize = "20px";
mainTitle.style.marginBottom = "2px";
containerGeneral.appendChild(mainTitle);
// Crear el subtítulo (información de la versión)
const versionInfo = document.createElement("div");
versionInfo.textContent = "V. " + VERSION; // VERSION global
versionInfo.style.textAlign = "right";
versionInfo.style.fontSize = "10px";
versionInfo.style.color = "#777";
versionInfo.style.marginBottom = "15px";
containerGeneral.appendChild(versionInfo);
//Crear un div para mostrar el ID del usuario
const userIdInfo = document.createElement("div"); //
userIdInfo.id = "wme-pln-user-id"; //
userIdInfo.textContent = "Cargando usuario..."; //
userIdInfo.style.textAlign = "right"; //
userIdInfo.style.fontSize = "10px"; //
userIdInfo.style.color = "#777"; //
userIdInfo.style.marginBottom = "15px"; //
containerGeneral.appendChild(userIdInfo); //
// Esta función reemplaza la necesidad de las funciones getCurrentEditorViaSdk, etc.
const pollAndDisplayUserInfo = () =>
{
let pollingAttempts = 0;
const maxPollingAttempts = 60;
const pollInterval = setInterval(async () =>
{
let currentUserInfoLocal = null; //: Usar una variable local temporal
// Primero intentar con wmeSDK.State.getUserInfo() ***
if (wmeSDK && wmeSDK.State && typeof wmeSDK.State.getUserInfo === 'function')
{
try
{
const sdkUserInfo = await wmeSDK.State.getUserInfo();
if (sdkUserInfo && sdkUserInfo.userName)
{
currentUserInfoLocal = {
// Si sdkUserInfo.id NO existe, usar sdkUserInfo.userName DIRECTAMENTE (sin Number())
id: sdkUserInfo.id !== undefined ? sdkUserInfo.id : sdkUserInfo.userName, //
name: sdkUserInfo.userName,
privilege: sdkUserInfo.privilege || 'N/A'
};
// Asegurarse de que el ID es válido para el log
const displayId = typeof currentUserInfoLocal.id === 'number' ? currentUserInfoLocal.id : `"${currentUserInfoLocal.id}"`; //
}
else
{
}
}
catch (e)
{
}
}
else
{
//console.warn(`[WME_PLN][DEBUG] SDK.State.getUserInfo no disponible. wmeSDK:`, wmeSDK);
}
// Fallback a W.loginManager (si SDK.State no funcionó)
if (!currentUserInfoLocal && typeof W !== 'undefined' && W.loginManager && W.loginManager.userName && W.loginManager.userId) { //: Usar currentUserInfoLocal
currentUserInfoLocal = {
id: Number(W.loginManager.userId), // Convertir a número
name: W.loginManager.userName,
privilege: W.loginManager.userPrivilege || 'N/A'
};
//console.log(`[WME PLN][DEBUG] W.loginManager SUCCESS: Usuario obtenido: ${currentUserInfoLocal.name} (ID: ${currentUserInfoLocal.id})`);
}
else if (!currentUserInfoLocal)
{ //: Solo logear si aún no se encontró en ningún método
// console.warn(`[WME_PLN][DEBUG] W.loginManager devolvió datos incompletos o null:`, W?.loginManager);
}
if (currentUserInfoLocal && currentUserInfoLocal.id && currentUserInfoLocal.name)
{
clearInterval(pollInterval);
currentGlobalUserInfo = currentUserInfoLocal;
userIdInfo.textContent = `Editor Actual: ${currentGlobalUserInfo.name}`;
userIdInfo.title = `Privilegio: ${currentGlobalUserInfo.privilege}`;
updateStatsDisplay();//: Actualizar estadísticas con el nuevo usuario
// console.log('[WME_PLN][DEBUG] USUARIO CARGADO EXITOSAMENTE mediante polling.');
const labelToUpdate = document.querySelector('label[for="chk-avoid-my-edits"]');
if (labelToUpdate)
{
labelToUpdate.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #007bff; font-weight: normal;">${currentGlobalUserInfo.name}</span>`;
}
const avoidMyEditsCheckbox = document.getElementById("chk-avoid-my-edits");
if (avoidMyEditsCheckbox)
{
avoidMyEditsCheckbox.disabled = false;
avoidMyEditsCheckbox.style.opacity = "1";
avoidMyEditsCheckbox.style.cursor = "pointer";
}
}
else if (pollingAttempts >= maxPollingAttempts - 1)
{
clearInterval(pollInterval);
userIdInfo.textContent = "Usuario no detectado (agotados intentos)";
//console.log('[WME PLN][DEBUG] Polling agotado. Usuario no detectado después de varios intentos.');
// Asignar el estado de fallo a currentGlobalUserInfo
currentGlobalUserInfo = { id: 0, name: 'No detectado', privilege: 'N/A' }; // Usar 0 o null como number
// Actualizar el texto del checkbox para evitar ediciones del usuario
const avoidTextSpanToUpdate = document.querySelector("#chk-avoid-my-edits + label span");
//: Actualizar el texto del checkbox para evitar ediciones del usuario
if (avoidTextSpanToUpdate)
{
//: Usa innerHTML y estilo atenuado para el nombre "No detectado"
avoidTextSpanToUpdate.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #777; opacity: 0.5;">No detectado</span>`; //
avoidTextSpanToUpdate.style.opacity = "1"; //: Asegurar opacidad base para el span principal
// avoidTextSpanToUpdate.style.color = "#777"; //: Puedes quitar esta línea si el color del span es suficiente
}
const avoidMyEditsCheckbox = document.getElementById("chk-avoid-my-edits");
//: Deshabilitar el checkbox si no se detecta el usuario
if (avoidMyEditsCheckbox)
{
avoidMyEditsCheckbox.disabled = true;
avoidMyEditsCheckbox.style.opacity = "0.5";
avoidMyEditsCheckbox.style.cursor = "not-allowed";
}
}
pollingAttempts++;
}, 200);
};
// Iniciar el polling para la información del usuario
pollAndDisplayUserInfo(); //Llamada directa a la nueva función de polling
// Título de la sección de normalización
const normSectionTitle = document.createElement("h4");
normSectionTitle.textContent = "Análisis de Nombres de Places";
normSectionTitle.style.fontSize = "16px";
normSectionTitle.style.marginTop = "10px";
normSectionTitle.style.marginBottom = "5px";
normSectionTitle.style.borderBottom = "1px solid #eee";
normSectionTitle.style.paddingBottom = "3px";
containerGeneral.appendChild(normSectionTitle);
// Descripción de la sección
const scanButton = document.createElement("button");
scanButton.id = "pln-start-scan-btn";
scanButton.textContent = "Start Scan...";
scanButton.setAttribute("type", "button");
scanButton.style.marginBottom = "10px";
scanButton.style.fontSize = "14px";
scanButton.style.width = "100%";
scanButton.style.padding = "8px";
scanButton.style.border = "none";
scanButton.style.borderRadius = "4px";
scanButton.style.backgroundColor = "#007bff";
scanButton.style.color = "#fff";
scanButton.style.cursor = "pointer";
scanButton.addEventListener("click", () =>
{
disableScanControls(); // Deshabilitar controles durante el escaneo
scanButton.textContent = "Escaneando..."; // Cambia el texto del botón
const places = getVisiblePlaces();
const outputDiv = document.getElementById("wme-normalization-tab-output");
if (!outputDiv)
{ // Mover esta verificación antes
return;
}
if (places.length === 0)
{
outputDiv.textContent = "No se encontraron lugares visibles para analizar.";
setTimeout(() => {renderPlacesInFloatingPanel([], { totalVisibleCount: 0, excludedCount: 0, skipExcludedFiltering: true });}, 10);
return;
}
const { filtered: placesWithoutExcluded, excludedCount } = filterOutExcludedPlaces(places);
const totalVisibleCount = places.length;
const maxPlacesInput = document.getElementById("maxPlacesInput");
const maxPlacesToScan = parseInt(maxPlacesInput?.value || "100", 10);
const scannedCount = Math.min(placesWithoutExcluded.length, maxPlacesToScan);
if (placesWithoutExcluded.length === 0)
{
outputDiv.textContent = excludedCount > 0
? `Todos los ${totalVisibleCount} lugares visibles están en la lista de excluidos.`
: "No se encontraron lugares visibles para analizar.";
}
else
{
outputDiv.textContent = excludedCount > 0
? `Escaneando ${scannedCount} lugares... (${excludedCount} excluidos que serán omitidos)`
: `Escaneando ${scannedCount} lugares...`;
}
setTimeout(() => {renderPlacesInFloatingPanel(placesWithoutExcluded, {
totalVisibleCount,
excludedCount,
skipExcludedFiltering: true
});}, 10);
});
containerGeneral.appendChild(scanButton);
// Crear el contenedor para el checkbox de usuario
const maxWrapper = document.createElement("div");
maxWrapper.style.display = "flex";
maxWrapper.style.alignItems = "center";
maxWrapper.style.gap = "8px";
maxWrapper.style.marginBottom = "8px";
const maxLabel = document.createElement("label");
maxLabel.textContent = "Máximo de places a revisar:";
maxLabel.style.fontSize = "13px";
maxWrapper.appendChild(maxLabel);
const maxInput = document.createElement("input");
maxInput.type = "number";
maxInput.id = "maxPlacesInput";
maxInput.min = "1";
maxInput.value = "100";
maxInput.style.width = "80px";
maxWrapper.appendChild(maxInput);
containerGeneral.appendChild(maxWrapper);
const presets = [ 25, 50, 100, 250, 500 ];
const presetContainer = document.createElement("div");
presetContainer.style.textAlign = "center";
presetContainer.style.marginBottom = "8px";
presets.forEach(preset =>
{
const btn = document.createElement("button");
btn.className = "pln-preset-btn"; // Clase para aplicar estilos comunes
btn.textContent = preset.toString();
btn.style.margin = "2px";
btn.style.padding = "4px 6px";
btn.addEventListener("click", () =>
{
if (maxInput)
maxInput.value = preset.toString();
});
presetContainer.appendChild(btn);
});
containerGeneral.appendChild(presetContainer);
// Checkbox para recomendar categorías
const recommendCategoriesWrapper = document.createElement("div");
recommendCategoriesWrapper.style.marginTop = "10px";
recommendCategoriesWrapper.style.marginBottom = "5px";
recommendCategoriesWrapper.style.display = "flex";
recommendCategoriesWrapper.style.flexDirection = "column"; //Cambiar a columna para apilar checkboxes
recommendCategoriesWrapper.style.alignItems = "flex-start"; //Alinear ítems al inicio
recommendCategoriesWrapper.style.padding = "6px 8px"; // Añadir padding
recommendCategoriesWrapper.style.backgroundColor = "#e0f7fa"; // Fondo claro para destacar
recommendCategoriesWrapper.style.border = "1px solid #00bcd4"; // Borde azul
recommendCategoriesWrapper.style.borderRadius = "4px"; // Bordes redondeados
containerGeneral.appendChild(recommendCategoriesWrapper); //Añadir el wrapper aquí, antes de sus contenidos
// Contenedor para el checkbox "Recomendar categorías"
const recommendCategoryCheckboxRow = document.createElement("div"); //
recommendCategoryCheckboxRow.style.display = "flex"; //Fila para checkbox y etiqueta
recommendCategoryCheckboxRow.style.alignItems = "center"; //
recommendCategoryCheckboxRow.style.marginBottom = "5px"; //Margen inferior
// Crear el checkbox y la etiqueta
const recommendCategoriesCheckbox = document.createElement("input");
recommendCategoriesCheckbox.type = "checkbox";
recommendCategoriesCheckbox.id = "chk-recommend-categories";
recommendCategoriesCheckbox.style.marginRight = "8px";
const savedCategoryRecommendationState = localStorage.getItem("wme_pln_recommend_categories");
recommendCategoriesCheckbox.checked = (savedCategoryRecommendationState === "true");
const recommendCategoriesLabel = document.createElement("label");
recommendCategoriesLabel.htmlFor = "chk-recommend-categories";
recommendCategoriesLabel.style.fontSize = "14px";
recommendCategoriesLabel.style.cursor = "pointer";
recommendCategoriesLabel.style.fontWeight = "bold";
recommendCategoriesLabel.style.color = "#00796b";
recommendCategoriesLabel.style.display = "flex";
recommendCategoriesLabel.style.alignItems = "center";
const iconSpan = document.createElement("span");
iconSpan.innerHTML = "✨ ";
iconSpan.style.marginRight = "4px";
iconSpan.style.fontSize = "16px";
iconSpan.appendChild(document.createTextNode("Recomendar categorías"));
recommendCategoriesLabel.appendChild(iconSpan);
recommendCategoryCheckboxRow.appendChild(recommendCategoriesCheckbox); //
recommendCategoryCheckboxRow.appendChild(recommendCategoriesLabel); //
recommendCategoriesWrapper.appendChild(recommendCategoryCheckboxRow); //Añadir la fila al wrapper
recommendCategoriesCheckbox.addEventListener("change", () =>
{
localStorage.setItem("wme_pln_recommend_categories", recommendCategoriesCheckbox.checked ? "true" : "false");
});
// --- Contenedor para AGRUPAR las opciones de exclusión ---
const excludeContainer = document.createElement('div');
excludeContainer.style.marginTop = '8px'; // Espacio que lo separa de la opción de arriba
// --- Fila para el checkbox "Excluir lugares..." ---
const avoidMyEditsCheckboxRow = document.createElement("div");
avoidMyEditsCheckboxRow.style.display = "flex";
avoidMyEditsCheckboxRow.style.alignItems = "center";
//: Añadir un margen inferior para separar del checkbox de categorías
const avoidMyEditsCheckbox = document.createElement("input");
avoidMyEditsCheckbox.type = "checkbox";
avoidMyEditsCheckbox.id = "chk-avoid-my-edits";
avoidMyEditsCheckbox.style.marginRight = "8px";
const savedAvoidMyEditsState = localStorage.getItem("wme_pln_avoid_my_edits");
avoidMyEditsCheckbox.checked = (savedAvoidMyEditsState === "true");
avoidMyEditsCheckboxRow.appendChild(avoidMyEditsCheckbox);
//: Añadir un label con el texto de la opción
const avoidMyEditsLabel = document.createElement("label");
avoidMyEditsLabel.htmlFor = "chk-avoid-my-edits";
avoidMyEditsLabel.style.fontSize = "16px"; // Tamaño de fuente consistente
avoidMyEditsLabel.style.cursor = "pointer";
avoidMyEditsLabel.style.fontWeight = "bold";
avoidMyEditsLabel.style.color = "#00796b";
avoidMyEditsLabel.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #007bff; font-weight: normal;">Cargando...</span>`;
avoidMyEditsCheckboxRow.appendChild(avoidMyEditsLabel);
// --- Fila para el dropdown de fecha (sub-menú) ---
const dateFilterRow = document.createElement("div");
dateFilterRow.style.display = "flex";
dateFilterRow.style.alignItems = "center";
dateFilterRow.style.marginTop = "8px"; // Espacio entre el checkbox y esta fila
dateFilterRow.style.paddingLeft = "25px"; // Indentación para que parezca una sub-opción
dateFilterRow.style.gap = "8px";
//: Añadir un label para el dropdown
const dateFilterLabel = document.createElement("label");
dateFilterLabel.htmlFor = "dateFilterSelect";
dateFilterLabel.textContent = "Excluir solo ediciones de:";
dateFilterLabel.style.fontSize = "13px";
dateFilterLabel.style.fontWeight = "500";
dateFilterLabel.style.color = "#334";
dateFilterRow.appendChild(dateFilterLabel);
//: Crear el dropdown para seleccionar el filtro de fecha
const dateFilterSelect = document.createElement("select");
dateFilterSelect.id = "dateFilterSelect";
dateFilterSelect.style.padding = "5px 8px";
dateFilterSelect.style.border = "1px solid #b0c4de";
dateFilterSelect.style.borderRadius = "4px";
dateFilterSelect.style.backgroundColor = "#fff";
dateFilterSelect.style.flexGrow = "1";
dateFilterSelect.style.fontSize = "13px";
dateFilterSelect.style.cursor = "pointer";
// Añadir opciones al dropdown
const dateOptions = {
"all": "Elegir una opción",
"6_months": "Últimos 6 meses",
"3_months": "Últimos 3 meses",
"1_month": "Último mes",
"1_week": "Última Semana",
"1_day": "Último día"
};
// Añadir las opciones al dropdown
for (const [value, text] of Object.entries(dateOptions))
{
const option = document.createElement("option");
option.value = value;
option.textContent = text;
dateFilterSelect.appendChild(option);
}
// Cargar el valor guardado del localStorage
const savedDateFilter = localStorage.getItem("wme_pln_date_filter");
if (savedDateFilter)
{
dateFilterSelect.value = savedDateFilter;
}
dateFilterSelect.addEventListener("change", () =>
{
localStorage.setItem("wme_pln_date_filter", dateFilterSelect.value);
});
dateFilterRow.appendChild(dateFilterSelect);
// --- Añadir AMBAS filas al contenedor de exclusión ---
excludeContainer.appendChild(avoidMyEditsCheckboxRow);
excludeContainer.appendChild(dateFilterRow);
// --- Añadir el contenedor AGRUPADO al wrapper principal (el cuadro azul) ---
recommendCategoriesWrapper.appendChild(excludeContainer);
// --- Lógica para habilitar/deshabilitar el dropdown ---
const toggleDateFilterState = () =>
{
const isChecked = avoidMyEditsCheckbox.checked;
dateFilterSelect.disabled = !isChecked;
dateFilterRow.style.opacity = isChecked ? "1" : "0.5";
dateFilterRow.style.pointerEvents = isChecked ? "auto" : "none";
};
// --- Listener unificado para el checkbox ---
avoidMyEditsCheckbox.addEventListener("change", () =>
{
toggleDateFilterState(); // Actualiza la UI del dropdown
localStorage.setItem("wme_pln_avoid_my_edits", avoidMyEditsCheckbox.checked ? "true" : "false"); // Guarda el estado
});
// Llamada inicial para establecer el estado correcto al cargar
toggleDateFilterState();
// --- Contenedor para el checkbox de estadísticas ---
const statsContainer = document.createElement('div');
statsContainer.style.marginTop = '8px';
// Añadir un borde y fondo para destacar
const statsCheckboxRow = document.createElement("div");
statsCheckboxRow.style.display = "flex";
statsCheckboxRow.style.alignItems = "center";
// Añadir un margen inferior para separar del checkbox de exclusión
const statsCheckbox = document.createElement("input");
statsCheckbox.type = "checkbox";
statsCheckbox.id = "chk-enable-stats";
statsCheckbox.style.marginRight = "8px";
statsCheckbox.checked = localStorage.getItem(STATS_ENABLED_KEY) === 'true';
statsCheckboxRow.appendChild(statsCheckbox);
// Crear la etiqueta para el checkbox de estadísticas
const statsLabel = document.createElement("label");
statsLabel.htmlFor = "chk-enable-stats";
statsLabel.style.fontSize = "16px"; // Tamaño consistente
statsLabel.style.cursor = "pointer";
statsLabel.style.fontWeight = "bold";
statsLabel.style.color = "#00796b";
statsLabel.innerHTML = `📊 Habilitar panel de estadísticas`;
statsCheckboxRow.appendChild(statsLabel);
// Añadir un tooltip al checkbox de estadísticas
statsContainer.appendChild(statsCheckboxRow);
// Añadir el contenedor de estadísticas al wrapper principal (el cuadro azul)
recommendCategoriesWrapper.appendChild(statsContainer);
// Listener para el checkbox de estadísticas
statsCheckbox.addEventListener("change", () =>
{
localStorage.setItem(STATS_ENABLED_KEY, statsCheckbox.checked ? "true" : "false");
toggleStatsPanelVisibility();
});
//===========================Finaliza bloque de estadísticas
// Listener para guardar el estado del nuevo checkbox
avoidMyEditsCheckbox.addEventListener("change", () =>
{ //
localStorage.setItem("wme_pln_avoid_my_edits", avoidMyEditsCheckbox.checked ? "true" : "false"); //
});
// Barra de progreso y texto
const tabProgressWrapper = document.createElement("div");
tabProgressWrapper.style.margin = "10px 0";
tabProgressWrapper.style.height = "18px";
tabProgressWrapper.style.backgroundColor = "transparent";
const tabProgressBar = document.createElement("div");
tabProgressBar.style.height = "100%";
tabProgressBar.style.width = "0%";
tabProgressBar.style.backgroundColor = "#007bff";
tabProgressBar.style.transition = "width 0.2s";
tabProgressBar.id = "progressBarInnerTab";
tabProgressWrapper.appendChild(tabProgressBar);
containerGeneral.appendChild(tabProgressWrapper);
// Texto de progreso
const tabProgressText = document.createElement("div");
tabProgressText.style.fontSize = "13px";
tabProgressText.style.marginTop = "5px";
tabProgressText.id = "progressBarTextTab";
tabProgressText.textContent = "Progreso: 0% (0/0)";
containerGeneral.appendChild(tabProgressText);
// Div para mostrar el resultado del análisis
const outputNormalizationInTab = document.createElement("div");
outputNormalizationInTab.id = "wme-normalization-tab-output";
outputNormalizationInTab.style.fontSize = "12px";
outputNormalizationInTab.style.minHeight = "20px";
outputNormalizationInTab.style.padding = "5px";
outputNormalizationInTab.style.marginBottom = "15px";
outputNormalizationInTab.textContent = "Presiona 'Start Scan...' para analizar los places visibles.";
containerGeneral.appendChild(outputNormalizationInTab);
}
else
{
console.error("[WME PLN] No se pudo poblar la pestaña 'General' porque su contenedor no existe.");
}
// 5. Poblar las otras pestañas
if (tabContents["Espe"])
createSpecialItemsManager(tabContents["Espe"]);
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'.");
}
// --- Llamada A La Función Para Poblar La Nueva Pestaña "Diccionario"
if (tabContents["Dicc"])
{
createDictionaryManager(tabContents["Dicc"]);
}
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Diccionario'.");
}
// --- Llamada A La Función Para Poblar La Nueva Pestaña "Reemplazos"
if (tabContents["Reemp"])
{
createReplacementsManager(tabContents["Reemp"]); // Esta es la llamada clave
}
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'.");
}
}
catch (error)
{
console.error("[WME PLN] Error creando la pestaña lateral:", error, error.stack);
}
} // Fin de createSidebarTab
// 2. Esperar a que Waze API esté disponible
function waitForSidebarAPI()
{
// Comprobar si Waze API está disponible
if (W && W.userscripts && W.userscripts.registerSidebarTab)
{
const savedExcluded = localStorage.getItem("excludedWordsList");
if (savedExcluded)
{
try
{
const parsed = JSON.parse(savedExcluded);
excludedWords = new Set(parsed);
// Reconstruir el mapa optimizado
excludedWordsMap.clear();
excludedWords.forEach(word => {
const firstChar = word.charAt(0).toLowerCase();
if (!excludedWordsMap.has(firstChar)) {
excludedWordsMap.set(firstChar, new Set());
}
excludedWordsMap.get(firstChar).add(word);
});
}
catch (e)
{
console.error("[WME PLN] Error al cargar palabras excluidas:", e);
excludedWords = new Set();
excludedWordsMap.clear();
}
}
else
{
excludedWords = new Set();
excludedWordsMap.clear();
}
// --- Cargar Lugares Excluidos desde localStorage ---
loadExcludedPlacesFromStorage(); // Llamar a la función directamente
// --- Cargar diccionario desde localStorage ---
const savedDictionary = localStorage.getItem("dictionaryWordsList");
if (savedDictionary)
{
try
{
const parsed = JSON.parse(savedDictionary);
window.dictionaryWords = new Set(parsed);
// Reconstruir el índice del diccionario
window.dictionaryIndex = {};
window.dictionaryWords.forEach(word => {
const firstChar = word.charAt(0).toLowerCase();
if (!window.dictionaryIndex[firstChar]) {
window.dictionaryIndex[firstChar] = [];
}
window.dictionaryIndex[firstChar].push(word);
});
}
catch (e)
{
console.error("[WME PLN] Error al cargar diccionario:", e);
window.dictionaryWords = new Set();
window.dictionaryIndex = {};
}
}
else
{
window.dictionaryWords = new Set();
window.dictionaryIndex = {};
}
updateDictionaryWordsCountLabel();
// Esto añadirá nuevas palabras del Excel a window.dictionaryWords y se encarga de guardar en localStorage después.
// Se hace de forma asíncrona pero no bloquea la UI.
loadDictionaryWordsFromSheet().then(() =>
{
console.log("[WME PLN] Diccionario de Google Sheets cargado y combinado.");
}).catch(err =>
{
console.error("[WME PLN] No se pudo cargar el diccionario de Google Sheets:", err);
});
// Cargar estadísticas del editor
loadEditorStats();
// --- Cargar palabras de reemplazo desde localStorage ---
loadReplacementWordsFromStorage();
// Cargar reemplazos desde Google Sheets y fusionar (bloqueando los de hoja)
loadReplacementsFromSheet().then(() => {
const ul = document.getElementById("replacementsListElementID");
if (ul && typeof renderReplacementsList === 'function') renderReplacementsList(ul);
}).catch(err => console.warn('[WME PLN] No se pudo cargar Replacements desde Sheets:', err));
// Cargar Swap desde Google Sheets (bloqueado) y fusionar con los del usuario
loadSwapWordsFromSheet().then(() => {
try { if (typeof saveSwapWordsToStorage === 'function') saveSwapWordsToStorage(); } catch(_){ }
}).catch(err => console.warn('[WME PLN] No se pudo cargar Swap desde Sheets:', err));
// La llamada a waitForWazeAPI ya se encarga de la lógica de dynamicCategoriesLoaded.
waitForWazeAPI(() =>
{
if (!dynamicCategoriesLoaded)
{
loadDynamicCategoriesFromSheet().then(() =>
{
dynamicCategoriesLoaded = true;
console.log("[WME PLN] Categorías dinámicas cargadas.");
}).catch(err =>
{
console.error("[WME PLN] Error al cargar categorías dinámicas:", err);
});
}
createSidebarTab();
createStatsPanel();
tryInitializeSDK(() => {
console.log("[WME PLN] SDK inicializado, obteniendo usuario...");
getCurrentEditorViaSdk().then(userInfo => {
if (userInfo) {
currentGlobalUserInfo = userInfo;
console.log(`[WME PLN] Usuario SDK detectado: ${userInfo.name} (ID: ${userInfo.id})`);
} else {
console.warn("[WME PLN] No se pudo obtener usuario del SDK, intentando otros métodos.");
const wrapUser = getCurrentEditorViaWazeWrap();
if (wrapUser) {
currentGlobalUserInfo = wrapUser;
console.log(`[WME PLN] Usuario WazeWrap detectado: ${wrapUser.name}`);
} else {
const internalUser = getCurrentEditorViaWmeInternal();
if (internalUser) {
currentGlobalUserInfo = internalUser;
console.log(`[WME PLN] Usuario WME interno detectado: ${internalUser.name}`);
} else {
console.error("[WME PLN] No se pudo detectar el usuario.");
currentGlobalUserInfo.name = "No detectado";
}
}
}
updateStatsDisplay();
});
});
});
}
else
{
// console.log("[WME PLN] Esperando W.userscripts API...");
setTimeout(waitForSidebarAPI, 1000);
}
window.PLN_READY = { ts: Date.now(), version: VERSION };
window.dispatchEvent(new CustomEvent('PLN_READY', { detail: { version: VERSION } }));
console.log('[WME PLN] Señal PLN_READY emitida');
}// Fin de waitForSidebarAPI
// 1. normalizePlaceName
// REEMPLAZA ESTA FUNCIÓN EN TU ARCHIVO wme_pln_8.2.0.js
function normalizePlaceName(originalName) {
// --- Helpers de capitalización (integrados para que la función sea autocontenida) ---
function plnCapitalizeStart(str) {
try { return String(str || '').replace(/^\s*([a-záéíóúñ])/iu, (m, c) => m.replace(c, c.toUpperCase())); } catch { return str; }
}
function plnCapitalizeAfterHyphen(str) {
try { return String(str || '').replace(/(\s-\s*)([a-záéíóúñ])/giu, (m, sep, ch) => sep + ch.toUpperCase()); } catch (_) { return String(str || ''); }
}
function plnTitleCaseEs(str) {
try {
const STOP = new Set(['de', 'del', 'la', 'las', 'el', 'los', 'y', 'e', 'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en', 'con', 'tras', 'por', 'al', 'lo']);
const isAllCaps = w => w.length > 1 && w === w.toUpperCase();
const cap = w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
let i = 0;
return String(str || '').replace(/([\p{L}\p{M}][\p{L}\p{M}\.'’]*)/gu, (m) => {
const w = m, lw = w.toLowerCase(), atStart = (i === 0); i += w.length;
const excl = (typeof isExcludedWord === 'function') ? isExcludedWord(w) : null;
if (excl) return excl;
if (isAllCaps(w)) return w;
if (STOP.has(lw) && !atStart) return lw;
return cap(w);
});
} catch { return str; }
}
function plnPostSwapCap(str) {
let out = String(str || '');
out = plnTitleCaseEs(out);
out = plnCapitalizeStart(out);
out = plnCapitalizeAfterHyphen(out);
return out.trim();
}
// --- Inicia el proceso de normalización ---
const DBG = !!(window.__PLN_DECISION_DEBUG_ON || localStorage.getItem('wme_pln_debug_decision') === '1');
if (DBG) console.group('[PLN Normalize] Proceso para:', originalName);
// 1. Procesa el nombre base (capitalización inicial, reglas de palabras, etc.)
let processedName = processPlaceName(originalName);
if (DBG) console.log('1. Después de processPlaceName:', processedName);
// 2. Aplica las reglas de SWAP para mover palabras
let swappedName = applySwapRules(processedName);
if (DBG) console.log('2. Después de applySwapRules:', swappedName);
// 3. ✨ **CORRECCIÓN CLAVE**: Vuelve a aplicar la capitalización después del swap
let finalName = plnPostSwapCap(swappedName);
if (DBG) console.log('3. Después de plnPostSwapCap (capitalización final):', finalName);
// 4. Limpieza final y restauración de palabras especiales
finalName = plnApplyExclusions(finalName);
if (DBG) {
console.log('4. Resultado final (tras exclusiones):', finalName);
console.groupEnd();
}
return finalName.trim();
}// Fin de normalizePlaceName
/* function normalizePlaceName(word)
{
//console.log("[WME_PLN][DEBUG] Analizando nombre:", word);
if (!word || typeof word !== "string")
return "";
// Manejar palabras con "/" recursivamente
if (word.includes("/"))
{
if (word === "/") return "/";
return word.split("/").map(part => normalizePlaceName(part.trim())).join("/");
}
// Regla 1: Si la palabra es SOLO números, mantenerla tal cual. (Prioridad alta)
if (/^[0-9]+$/.test(word))
return word;
// Regla 2: Números seguidos de letras (sin espacio)
word = word.replace(/(\d)([a-zA-Z])/g, (_, num, letter) => `${num}${letter.toUpperCase()}`);
// Regla 3: Números romanos
const romanRegexStrict = /^(C{0,3}(XC|XL|L?X{0,3})?(IX|IV|V?I{0,3})?)$/i;
if (romanRegexStrict.test(word))
return word.toUpperCase();
// Regla 4: Acrónimos/Palabras con puntos/letras mayúsculas que deben mantenerse. Esto es para "St." o "U.S.A." o "EPM", "SURA"
// NOTA: originalNameFull ya no tiene emoticones gracias a `processNextPlace`
if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && (word.includes('.') || /^[A-ZÁÉÍÓÚÑ]+$/.test(word)))
{
// Asegurarse de que no sea "DI", "SI" si están en mayúsculas accidentales
if (word.toUpperCase() === "DI" || word.toUpperCase() === "SI")
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
return word; // Mantener como está
}
// Regla 5: Capitalización estándar para el resto de las palabras.
// Esta será la regla para la mayoría de las palabras que no caen en las anteriores.
let normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
return normalizedWord;
}*/// Fin de normalizePlaceName
// Función para escapar caracteres especiales en una cadena para usar en regex
// Función para aplicar el movimiento de palabras al inicio del nombre.
function applyWordsToStartMovement(name, wordsArray = null) {
let newName = name;
const wordsToProcess = wordsArray || (window.swapWords || []);
if (wordsToProcess.length === 0) {
return newName;
}
const sortedWords = [...wordsToProcess].sort((a, b) => {
const aWord = typeof a === 'object' ? a.word : a;
const bWord = typeof b === 'object' ? b.word : b;
return bWord.length - aWord.length;
});
for (const item of sortedWords) {
const word = typeof item === 'object' ? item.word : item;
const regex = new RegExp(`\\s*(${escapeRegExp(String(word))})\\s*$`, 'i');
if (regex.test(newName)) {
const match = newName.match(regex);
const matchedWord = match[1];
const remainingName = newName.replace(regex, '').trim();
const capitalizedWord = capitalizeEachWord(matchedWord);
// ✅ CORRECCIÓN: No se modifica la capitalización del resto del nombre.
// Se asume que "La Calleja" ya viene correctamente capitalizado.
newName = `${capitalizedWord} ${remainingName}`.trim();
break;
}
}
return newName;
}//applyWordsToStartMovement
// Esta función aplica el movimiento de palabras al final del nombre.
function applyWordsToEndMovement(name, wordsArray = null)
{
let newName = name;
const wordsToProcess = wordsArray || (window.swapWords || []);
if (wordsToProcess.length === 0)
{
return newName;
}
const sortedWords = [...wordsToProcess].sort((a, b) => {
const aWord = typeof a === 'object' ? a.word : a;
const bWord = typeof b === 'object' ? b.word : b;
return bWord.length - aWord.length;
});
for (const item of sortedWords)
{
const word = typeof item === 'object' ? item.word : item;
const regex = new RegExp(`^\\s*(${escapeRegExp(String(word))})\\s+`, 'i');
if (regex.test(newName))
{
const match = newName.match(regex);
const matchedWord = match[1];
const remainingName = newName.replace(regex, '').trim();
const capitalizedWord = capitalizeEachWord(matchedWord);
// ✅ CORRECCIÓN: Se mantiene la capitalización del resto del nombre.
newName = `${remainingName} ${capitalizedWord}`.trim();
break;
}
}
return newName;
}//applyWordsToEndMovement
// Función para aplicar el movimiento de palabras (unificada)
function applySwapMovement(name)
{
let newName = name;
if (!window.swapWords || window.swapWords.length === 0) {
return newName;
}
// Separar palabras por dirección
const startWords = window.swapWords
.filter(item => item.direction === "start")
.map(item => item.word);
const endWords = window.swapWords
.filter(item => item.direction === "end")
.map(item => item.word);
// Aplicar movimiento al inicio (mover del final al inicio)
if (startWords.length > 0) {
newName = applyWordsToStartMovement(newName, startWords);
}
// Aplicar movimiento al final (mover del inicio al final)
if (endWords.length > 0) {
newName = applyWordsToEndMovement(newName, endWords);
}
return newName;
}//applySwapMovement
//---------------------------------------------------------------------
// Función para capitalizar la primera letra de cada palabra en una cadena
function implementarCargaBajoDemanda(contenedor, datos, tamañoPagina = 50)
{
let indiceActual = 0;
const contenedorElementos = document.createElement('div');
contenedorElementos.className = 'diccionario-contenido';
contenedor.appendChild(contenedorElementos);
// Cargar batch inicial
cargarSiguienteLote();
// Agregar detector de scroll
contenedor.addEventListener('scroll', manejarScroll);
// Función para manejar el evento de scroll
function manejarScroll()
{
if (contenedor.scrollTop + contenedor.clientHeight >= contenedorElementos.offsetHeight - 200)
{
cargarSiguienteLote();
}
}//manejarScroll
// Función para cargar el siguiente lote de elementos
function cargarSiguienteLote()
{
const fragmento = document.createDocumentFragment();
const limite = Math.min(indiceActual + tamañoPagina, datos.length);
for (let i = indiceActual; i < limite; i++)
{
const elemento = crearElementoDiccionario(datos[i]);
fragmento.appendChild(elemento);
}
contenedorElementos.appendChild(fragmento);
indiceActual = limite;
// Eliminar detector de scroll si ya se cargaron todos los elementos
if (indiceActual >= datos.length)
{
contenedor.removeEventListener('scroll', manejarScroll);
}
}//cargarSiguienteLote
// Función para crear un elemento del diccionario
function crearElementoDiccionario(datoElemento)
{
const elemento = document.createElement('div');
elemento.className = 'diccionario-item';
elemento.innerHTML = `
<h3>${datoElemento.termino}</h3>
<p>${datoElemento.definicion}</p>
`;
return elemento;
}//crearElementoDiccionario
}
// Función para agregar un indicador de carga al contenedor
function agregarIndicadorCarga(contenedor)
{
const indicador = document.createElement('div');
indicador.className = 'indicador-carga';
indicador.innerHTML = '<span>Cargando más términos...</span>';
indicador.style.display = 'none';
contenedor.appendChild(indicador);
return {
mostrar: () => { indicador.style.display = 'block'; },
ocultar: () => { indicador.style.display = 'none'; }
};
}//agregarIndicadorCarga
function agregarEstilosDiccionario() {
// Creamos los elementos del diccionario dinámico con estilos aplicados directamente
// Estilos para el contenedor de diccionario
const dictionaryContainerStyle = {
maxHeight: "250px",
overflowY: "auto",
border: "1px solid #ddd",
padding: "5px",
margin: "0",
background: "#fff",
borderRadius: "4px"
};
// Estilos para cada elemento de diccionario
const dictionaryItemStyle = {
padding: "8px 10px",
borderBottom: "1px solid #eee",
transition: "background-color 0.2s",
cursor: "default",
marginBottom: "4px",
borderRadius: "3px"
};
// Estilos para el estado hover de los elementos
const dictionaryItemHoverStyle = {
backgroundColor: "#f5f5f5"
};
// Estilos para el título de cada elemento
const dictionaryItemTitleStyle = {
margin: "0 0 3px 0",
fontSize: "14px",
color: "#333",
fontWeight: "500"
};
// Estilos para la descripción de cada elemento (si existe)
const dictionaryItemDescriptionStyle = {
margin: "0",
fontSize: "12px",
color: "#666",
fontStyle: "italic"
};
// Estilos para el indicador de carga
const indicadorCargaStyle = {
textAlign: "center",
padding: "10px",
color: "#777",
fontStyle: "italic",
borderTop: "1px dashed #ddd",
margin: "5px 0 0 0",
fontSize: "12px"
};
// Aplicar estilos al contenedor principal cuando se cree
const applyContainerStyles = (container) => {
if (!container) return;
Object.entries(dictionaryContainerStyle).forEach(([property, value]) => {
container.style[property] = value;
});
};
// Función para aplicar estilos a cada elemento del diccionario
window.plnApplyDictionaryItemStyles = (element) => {
if (!element) return;
// Aplicar estilos base
Object.entries(dictionaryItemStyle).forEach(([property, value]) => {
element.style[property] = value;
});
// Añadir eventos para el hover
element.addEventListener('mouseenter', () => {
Object.entries(dictionaryItemHoverStyle).forEach(([property, value]) => {
element.style[property] = value;
});
});
element.addEventListener('mouseleave', () => {
element.style.backgroundColor = dictionaryItemStyle.backgroundColor || '';
});
// Aplicar estilos al título si existe
const title = element.querySelector('h3');
if (title) {
Object.entries(dictionaryItemTitleStyle).forEach(([property, value]) => {
title.style[property] = value;
});
}
// Aplicar estilos a la descripción si existe
const description = element.querySelector('p');
if (description) {
Object.entries(dictionaryItemDescriptionStyle).forEach(([property, value]) => {
description.style[property] = value;
});
}
};
// Función para crear y aplicar estilos al indicador de carga
window.plnCreateLoadingIndicator = (container) => {
if (!container) return null;
const indicador = document.createElement('div');
indicador.className = 'indicador-carga';
// Aplicar estilos al indicador
Object.entries(indicadorCargaStyle).forEach(([property, value]) => {
indicador.style[property] = value;
});
// Crear el contenido del indicador
const spinner = document.createElement('div');
spinner.style.width = "16px";
spinner.style.height = "16px";
spinner.style.border = "2px solid #ccc";
spinner.style.borderTop = "2px solid #007bff";
spinner.style.borderRadius = "50%";
spinner.style.animation = "spin 0.8s linear infinite";
spinner.style.display = "inline-block";
spinner.style.marginRight = "8px";
spinner.style.verticalAlign = "middle";
const texto = document.createElement('span');
texto.textContent = 'Cargando más términos...';
texto.style.verticalAlign = "middle";
indicador.appendChild(spinner);
indicador.appendChild(texto);
indicador.style.display = 'none';
container.appendChild(indicador);
return {
mostrar: () => { indicador.style.display = 'block'; },
ocultar: () => { indicador.style.display = 'none'; }
};
};
// Inicializar aplicación de estilos al contenedor de diccionario
const dictionaryContainer = document.getElementById('dictionaryContainer');
if (dictionaryContainer) {
applyContainerStyles(dictionaryContainer);
} else {
// Si el contenedor aún no existe, preparar un observador para detectarlo cuando se cree
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const dictionaryContainer = document.getElementById('dictionaryContainer');
if (dictionaryContainer) {
applyContainerStyles(dictionaryContainer);
observer.disconnect();
break;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// Asegurarse de que existe la animación de spin en la página
if (!document.getElementById('wme-pln-animations')) {
const styleElement = document.createElement('style');
styleElement.id = 'wme-pln-animations';
styleElement.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(styleElement);
}
}
// Modificar la función inicializarDiccionarioDinamico para usar estos estilos
function inicializarDiccionarioDinamico(contenedorId, datos)
{
const contenedor = document.getElementById(contenedorId);
if (!contenedor) return;
// Aplicar estilos al contenedor (por si se llamó antes de que existiera el observador)
if (window.plnApplyDictionaryContainerStyles) {
window.plnApplyDictionaryContainerStyles(contenedor);
}
// Usar el nuevo creador de indicador
const indicadorCarga = window.plnCreateLoadingIndicator ?
window.plnCreateLoadingIndicator(contenedor) :
agregarIndicadorCarga(contenedor);
let indiceActual = 0;
const tamañoPagina = 50;
const contenedorElementos = document.createElement('div');
contenedorElementos.className = 'diccionario-contenido';
contenedor.appendChild(contenedorElementos);
// Cargar batch inicial
cargarSiguienteLote();
// Agregar detector de scroll
contenedor.addEventListener('scroll', manejarScroll);
// Función para manejar el evento de scroll
function manejarScroll() {
if (contenedor.scrollTop + contenedor.clientHeight >= contenedorElementos.offsetHeight - 200) {
cargarSiguienteLote();
}
}
// Función para cargar el siguiente lote de elementos
function cargarSiguienteLote() {
if (indiceActual >= datos.length) return;
indicadorCarga.mostrar();
// Simulamos un pequeño retraso para ver el indicador de carga
setTimeout(() => {
const fragmento = document.createDocumentFragment();
const limite = Math.min(indiceActual + tamañoPagina, datos.length);
for (let i = indiceActual; i < limite; i++) {
const elemento = crearElementoDiccionario(datos[i]);
fragmento.appendChild(elemento);
}
contenedorElementos.appendChild(fragmento);
indiceActual = limite;
indicadorCarga.ocultar();
// Eliminar detector de scroll si ya se cargaron todos los elementos
if (indiceActual >= datos.length) {
contenedor.removeEventListener('scroll', manejarScroll);
}
}, 300);
}
// Función para crear un elemento del diccionario
function crearElementoDiccionario(datoElemento) {
const elemento = document.createElement('div');
elemento.className = 'diccionario-item';
// Crear estructura interna
const titulo = document.createElement('h3');
titulo.textContent = datoElemento.termino;
const descripcion = document.createElement('p');
descripcion.textContent = datoElemento.definicion || '';
elemento.appendChild(titulo);
elemento.appendChild(descripcion);
// Aplicar estilos si existe la función
if (window.plnApplyDictionaryItemStyles) {
window.plnApplyDictionaryItemStyles(elemento);
}
return elemento;
}
}
// Llamar a la función para añadir estilos al inicio
agregarEstilosDiccionario();
//---------------------------------------------------------------------
// Esta función normaliza una palabra individual, considerando palabras excluidas, tildes y capitalización
function normalizeWordInternal(word, isFirstWordInSequence = false, isInsideQuotesOrParentheses = false)
{
//console.log(`[WME PLN - NWI] Inicia procesamiento de palabra: "${word}"`); // LOG INICIO
if (!word || typeof word !== 'string')
{
return "";
}
// PRioridad 1: Palabras Especiales (Excluidas)
if (excludedWords && excludedWordsMap)
{
//console.log(`[WME PLN - NWI] Intentando Prioridad 1 (Excluidas) para: "${word}"`); // LOG INICIO EXCLUIDAS
// La limpieza para comparación ahora SÓLO quita tildes y convierte a minúsculas.
// Ya no elimina símbolos como '&' o '.', haciendo la comparación más estricta.
const cleanedInputWord = removeDiacritics(word.toLowerCase());
const firstChar = word.charAt(0).toLowerCase();
const excludedCandidates = excludedWordsMap.get(firstChar);
//console.log(`[WME PLN - NWI] cleanedInputWord: "${cleanedInputWord}", firstChar: "${firstChar}"`); // LOG CLEANED
//console.log(`[WME PLN - NWI] excludedCandidates para '${firstChar}':`, excludedCandidates ? Array.from(excludedCandidates) : 'Ninguno'); // LOG CANDIDATOS
// Verifica si hay candidatos excluidos para la primera letra de la palabra.
if (excludedCandidates)
{
for (const excludedWord of excludedCandidates)
{
// Limpia la palabra de la lista de la misma manera estricta.
const cleanedExcludedWord = removeDiacritics(excludedWord.toLowerCase());
if (cleanedExcludedWord === cleanedInputWord)
{
return excludedWord; // Si es una palabra excluida, devuelve su forma exacta y termina.
}
}
//console.log(`[WME PLN - NWI] 🚫 No se encontró coincidencia exacta para excluida: "${word}"`); // LOG NO COINCIDENCIA
}
}
// FIN PRIORIDAD 1
// Prioridad 2: Manejo De Guiones Dentro De Palabras (solo si no fue excluida completa)
// La condición /\p{L}-\p{L}/u.test(word) es crucial: asegura que el guion esté entre letras
if (word.includes('-') && /\p{L}-\p{L}/u.test(word))
{
//console.log(`[WME PLN - NWI] Aplicando Prioridad 2: Manejo de guiones para: "${word}"`);
const parts = word.split('-');
const normalizedParts = parts.map((part, partIndex) => {
let normalizedPart = part;
const isAcronymLikePart = /^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(part);
if (isAcronymLikePart && part.length > 1) {
normalizedPart = part; // Mantener como acrónimo si lo es.
} else {
normalizedPart = part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
}
return normalizedPart;
});
return normalizedParts.join('-');
}
// Prioridad 3: Palabras Con Apóstrofe
if (word.includes("'"))
{
//console.log(`[WME PLN - NWI] Aplicando Prioridad 3 (Apóstrofe): "${word}"`);
return handleApostropheWord(word);
}
// Prioridad 4: Acrónimos Y Palabras Con Mayúsculas/Puntos/& (después de guiones y apóstrofes)
const isAcronymLike = /^[A-ZÁÉÍÓÚÑ0-9.&]+$/.test(word);
if (isAcronymLike && word.length > 1)
{
//console.log(`[WME PLN - NWI] Aplicando Prioridad 4 (Acrónimo): "${word}"`);
return word; // Mantener como está
}
// Prioridad 5: Números Romanos
const romanRegexInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
if (romanRegexInsensitive.test(word))
{
//console.log(`[WME PLN - NWI] Aplicando Prioridad 5 (Números Romanos): "${word}"`);
return word.toUpperCase();
}
// Prioridad 6: Palabras Comunes
const lowerWord = word.toLowerCase().replace('.', '');
// `commonWords` es una constante global.
if (commonWords.includes(lowerWord)) // Si es una palabra común
{
//console.log(`[WME PLN - NWI] Aplicando Prioridad 6 (Palabra Común): "${word}"`);
// Artículos y preposiciones que siempre queremos capitalizar si están en commonWords
const alwaysCapitalizeCommonWords = ["el", "la", "los", "las", "de", "del", "y", "e", "o", "u", "al", "en", "con", "por"];
// Solo capitalizar "y" si es la primera palabra o después de guion o punto
if (lowerWord === "y")
{
if (isFirstWordInSequence)
{
return "Y";
}
else
{
return "y";
}
}
// Solo capitalizar "e" si es la primera palabra o después de guion o punto
if (lowerWord === "e")
{
if (isFirstWordInSequence)
{
return "E";
}
else
{
return "e";
}
}
if (alwaysCapitalizeCommonWords.includes(lowerWord)) {
// Si es un artículo/preposición de la lista, SIEMPRE capitalizar su primera letra.
// Esto forzará "el" -> "El", "de" -> "De", incluso si no es la primera palabra.
return lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1);
} else if (isFirstWordInSequence && !isInsideQuotesOrParentheses) {
// Para otras palabras comunes (ej. "un", "una"), solo capitalizar si es la primera palabra
return lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1);
} else {
// Si es una palabra común que NO es un artículo/preposición de la lista,
// y NO es la primera palabra, la minúsculas (comportamiento actual).
return lowerWord;
}
}
// Prioridad 7: Capitalización Estándar (Regla Por Defecto)
let wordWithoutPunctuation = word.endsWith('.') ? word.slice(0, -1) : word;
let result = wordWithoutPunctuation.charAt(0).toUpperCase() + wordWithoutPunctuation.slice(1).toLowerCase();
//console.log(`[WME PLN - NWI] Aplicando Prioridad 7 (Capitalización Estándar). Resultado: "${result}"`); // LOG Capitalización estándar
return result;
}//normalizeWordInternal
window.normalizeWordInternal = normalizeWordInternal;
// Maneja la capitalización de palabras que contienen un apóstrofe.
function handleApostropheWord(word)
{
const parts = word.split("'");
// Solo aplica si hay exactamente un apóstrofe.
if (parts.length === 2)
{
const before = parts[0];
const after = parts[1];
if (after.toLowerCase() === 's')
{
// Caso posesivo como McDonald's, la 's' va en minúscula.
return before + "'s";
}
else
{
// Caso como Q'Menú, se capitaliza la parte después del apóstrofe.
const capitalizedAfter = after.charAt(0).toUpperCase() + after.slice(1).toLowerCase();
return before + "'" + capitalizedAfter;
}
}
// Si no es un caso manejable, devuelve la palabra original para que la procesen otras reglas.
return word;
}// handleApostropheWord
// Función para crear un dropdown de categorías
function createCategoryDropdown(currentCategoryKey, rowIndex, venue)
{
const select = document.createElement("select");
select.style.padding = "4px";
select.style.borderRadius = "4px";
select.style.fontSize = "12px";
select.title = "Selecciona una categoría";
select.id = `categoryDropdown-${rowIndex}`;
Object.entries(categoryIcons).forEach(([key, value]) =>
{
const option = document.createElement("option");
option.value = key;
option.textContent = `${value.icon} ${value.en}`;
if (key === currentCategoryKey)
option.selected = true;
select.appendChild(option);
});
// Evento: al cambiar la categoría
select.addEventListener("change", (e) =>
{
const selectedCategory = e.target.value;
if (!venue || !venue.model || !venue.model.attributes)
{
//console.error("[WME_PLN] Venue inválido al intentar actualizar la categoría");
return;
}
// Actualizar la categoría en el modelo
venue.model.attributes.categories = [selectedCategory];
venue.model.save();
// Mensaje opcional de confirmación
WazeWrap.Alerts.success("Categoría actualizada", `Nueva categoría: ${categoryIcons[selectedCategory].en}`);
});
return select;
}
// 3. La función postProcessQuotesAndParentheses (CORREGIDA de la respuesta anterior)
function postProcessQuotesAndParentheses(text)
{
if (typeof text !== 'string') return text;
// Función auxiliar para capitalizar la primera letra de una cadena
function capitalizeFirstLetter(string) {
if (!string) return string;
return string.charAt(0).toUpperCase() + string.slice(1);
}
// Normalizar contenido dentro de comillas dobles
text = text.replace(/"([^"]*)"/g, (match, content) =>
{
const trimmedContent = content.trim();
if (trimmedContent === "") return '""';
// Capitaliza la primera letra de todo el contenido interno
const capitalizedContent = capitalizeFirstLetter(trimmedContent);
return `"${capitalizedContent}"`; // Sin espacios extra
});
// Normalizar contenido dentro de paréntesis
text = text.replace(/\(([^)]*)\)/g, (match, content) =>
{
const trimmedContent = content.trim();
if (trimmedContent === "") return '()';
// Capitaliza la primera letra de todo el contenido interno
const capitalizedContent = capitalizeFirstLetter(trimmedContent);
return `(${capitalizedContent})`; // Sin espacios extra
});
return text.replace(/\s+/g, ' ').trim(); // Limpieza final general
}// postProcessQuotesAndParentheses// postProcessQuotesAndParentheses
// === Palabras especiales ===
let excludedWords = new Set(); // Mantenemos el Set para facilitar el renderizado original
let excludedWordsMap = new Map(); // Para la búsqueda optimizada
let excludedPlaces = new Map(); // Nuevo Map para IDs de lugares excluidos
let dictionaryWords = new Set(); // O window.dictionaryWords = un Set global
let dictionaryWordsCountLabelElement = null;
// === Palabras especiales ===
// --- ADICIÓN PARA DEPURACIÓN EN CONSOLA ---
window.excludedWords = excludedWords;
window.excludedWordsMap = excludedWordsMap;
window.excludedPlaces = excludedPlaces;
// Función para crear el gestor de palabras excluidas y lugares excluidos
function createSpecialItemsManager(parentContainer)
{
const mainSection = document.createElement("div"); // <--- Nueva sección principal para la pestaña "Espe"
mainSection.id = "specialItemsManagerSection";
mainSection.style.marginTop = "20px";
mainSection.style.borderTop = "1px solid #ccc";
mainSection.style.paddingTop = "10px";
// --- Dropdown para seleccionar el tipo de gestión ---
const typeSelectorWrapper = document.createElement("div");
typeSelectorWrapper.style.marginBottom = "15px";
typeSelectorWrapper.style.textAlign = "center";
const typeSelectorLabel = document.createElement("label");
typeSelectorLabel.textContent = "Gestionar:";
typeSelectorLabel.style.marginRight = "10px";
typeSelectorLabel.style.fontWeight = "bold";
typeSelectorWrapper.appendChild(typeSelectorLabel);
const typeSelector = document.createElement("select");
typeSelector.id = "specialTypeSelector";
typeSelector.style.padding = "5px";
typeSelector.style.borderRadius = "4px";
typeSelector.style.fontSize = "13px";
const optionWords = document.createElement("option");
optionWords.value = "words";
optionWords.textContent = "Palabras Especiales";
typeSelector.appendChild(optionWords);
const optionPlaces = document.createElement("option");
optionPlaces.value = "places";
optionPlaces.textContent = "Lugares Excluidos";
typeSelector.appendChild(optionPlaces);
typeSelectorWrapper.appendChild(typeSelector);
mainSection.appendChild(typeSelectorWrapper); // Añadir a mainSection
// --- Contenedores para las dos vistas ---
const wordsView = document.createElement("div");
wordsView.id = "specialWordsView";
wordsView.style.display = "block"; // Visible por defecto
const placesView = document.createElement("div");
placesView.id = "excludedPlacesView";
placesView.style.display = "none"; // Oculto por defecto
mainSection.appendChild(wordsView); // Añadir a mainSection
mainSection.appendChild(placesView); // Añadir a mainSection
// ***********************************************************************************
// INICIO DEL CONTENIDO DE LA VISTA DE PALABRAS ESPECIALES (Antigua createExcludedWordsManager)
// ***********************************************************************************
// Título de la sección
const wordsTitle = document.createElement("h4");
wordsTitle.textContent = "Gestión de Palabras Especiales";
wordsTitle.style.fontSize = "15px";
wordsTitle.style.marginBottom = "10px";
wordsView.appendChild(wordsTitle); // AÑADIDO A wordsView
// Contenedor para los controles de añadir palabra
const addWordsControlsContainer = document.createElement("div"); // Renombrado para claridad
addWordsControlsContainer.style.display = "flex";
addWordsControlsContainer.style.gap = "8px";
addWordsControlsContainer.style.marginBottom = "8px";
addWordsControlsContainer.style.alignItems = "center"; // Alinear verticalmente
// Input para añadir nueva palabra o frase
const wordsInput = document.createElement("input"); // Renombrado para claridad
wordsInput.type = "text";
wordsInput.placeholder = "Nueva palabra o frase";
wordsInput.style.flexGrow = "1";
wordsInput.style.padding = "6px";
wordsInput.style.border = "1px solid #ccc";
wordsInput.style.borderRadius = "3px";
addWordsControlsContainer.appendChild(wordsInput); // AÑADIDO A addWordsControlsContainer
// Botón para añadir la palabra
const addWordBtn = document.createElement("button"); // Renombrado para claridad
addWordBtn.textContent = "Añadir";
addWordBtn.style.padding = "6px 10px";
addWordBtn.style.cursor = "pointer";
// Añadir tooltip al botón
addWordBtn.addEventListener("click", function ()
{
const newWord = wordsInput.value.trim(); // Usa wordsInput
const validation = isValidExcludedWord(newWord);
if (!validation.valid)
{
alert(validation.msg);
return;
}
excludedWords.add(newWord);
const firstCharNew = newWord.charAt(0).toLowerCase();
if (!excludedWordsMap.has(firstCharNew))
{
excludedWordsMap.set(firstCharNew, new Set());
}
excludedWordsMap.get(firstCharNew).add(newWord);
wordsInput.value = ""; // Limpia wordsInput
renderExcludedWordsList(document.getElementById("excludedWordsList"));
saveExcludedWordsToLocalStorage();
});
addWordsControlsContainer.appendChild(addWordBtn); // AÑADIDO A addWordsControlsContainer
wordsView.appendChild(addWordsControlsContainer); // AÑADIDO A wordsView
// Contenedor para los botones de acción (Exportar/Limpiar para Palabras)
const wordsActionButtonsContainer = document.createElement("div"); // Renombrado
wordsActionButtonsContainer.style.display = "flex";
wordsActionButtonsContainer.style.gap = "8px";
wordsActionButtonsContainer.style.marginBottom = "10px";
const exportWordsBtn = document.createElement("button"); // Renombrado
exportWordsBtn.textContent = "Exportar";
exportWordsBtn.title = "Exportar Lista a XML";
exportWordsBtn.style.padding = "6px 10px";
exportWordsBtn.style.cursor = "pointer";
exportWordsBtn.addEventListener("click", () => exportSharedDataToXml("words")); // Pasa el tipo
wordsActionButtonsContainer.appendChild(exportWordsBtn); // AÑADIDO A wordsActionButtonsContainer
const clearWordsBtn = document.createElement("button"); // Renombrado
clearWordsBtn.textContent = "Limpiar";
clearWordsBtn.title = "Limpiar toda la lista";
clearWordsBtn.style.padding = "6px 10px";
clearWordsBtn.style.cursor = "pointer";
clearWordsBtn.addEventListener("click", function ()
{
if (confirm("¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?"))
{
excludedWords.clear();
excludedWordsMap.clear();
renderExcludedWordsList(document.getElementById("excludedWordsList"));
saveExcludedWordsToLocalStorage();
}
});
wordsActionButtonsContainer.appendChild(clearWordsBtn); // AÑADIDO A wordsActionButtonsContainer
wordsView.appendChild(wordsActionButtonsContainer); // AÑADIDO A wordsView
// Contenedor para la lista de palabras excluidas (buscador y UL)
const wordsSearchInput = document.createElement("input"); // Renombrado
wordsSearchInput.type = "text";
wordsSearchInput.placeholder = "Buscar en especiales...";
wordsSearchInput.style.display = "block";
wordsSearchInput.style.width = "calc(100% - 14px)";
wordsSearchInput.style.padding = "6px";
wordsSearchInput.style.border = "1px solid #ccc";
wordsSearchInput.style.borderRadius = "3px";
wordsSearchInput.style.marginBottom = "5px";
wordsSearchInput.addEventListener("input", () =>
{
renderExcludedWordsList(document.getElementById("excludedWordsList"), wordsSearchInput.value.trim()); // Usa wordsSearchInput
});
wordsView.appendChild(wordsSearchInput); // AÑADIDO A wordsView
// UL para palabras excluidas
const wordsListUL = document.createElement("ul"); // Renombrado
wordsListUL.id = "excludedWordsList"; // Mantiene el ID original para compatibilidad con renderExcludedWordsList
wordsListUL.style.maxHeight = "150px";
wordsListUL.style.overflowY = "auto";
wordsListUL.style.border = "1px solid #ddd";
wordsListUL.style.padding = "5px";
wordsListUL.style.margin = "0";
wordsListUL.style.background = "#fff";
wordsListUL.style.listStyle = "none";
wordsView.appendChild(wordsListUL); // AÑADIDO A wordsView
// Drop Area para XML de palabras
const wordsDropArea = document.createElement("div"); // Renombrado
wordsDropArea.textContent = "Arrastra aquí el archivo XML de palabras especiales";
wordsDropArea.style.border = "2px dashed #ccc";
wordsDropArea.style.borderRadius = "4px";
wordsDropArea.style.padding = "15px";
wordsDropArea.style.marginTop = "10px";
wordsDropArea.style.textAlign = "center";
wordsDropArea.style.background = "#f9f9f9";
wordsDropArea.style.color = "#555";
wordsDropArea.addEventListener("dragover", (e) =>
{
e.preventDefault();
wordsDropArea.style.background = "#e9e9e9";
wordsDropArea.style.borderColor = "#aaa";
});
wordsDropArea.addEventListener("dragleave", () =>
{
wordsDropArea.style.background = "#f9f9f9";
wordsDropArea.style.borderColor = "#ccc";
});
wordsDropArea.addEventListener("drop", (e) =>
{
e.preventDefault();
wordsDropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0], "words"); // Pasar el tipo de importación
});
wordsView.appendChild(wordsDropArea); // AÑADIDO A wordsView
// ***********************************************************************************
// FIN DEL CONTENIDO DE LA VISTA DE PALABRAS ESPECIALES
// ***********************************************************************************
// ***********************************************************************************
// INICIO DEL CONTENIDO DE LA VISTA DE LUGARES EXCLUIDOS (Nueva lógica)
// ***********************************************************************************
// Título de la sección
const placesTitle = document.createElement("h4");
placesTitle.textContent = "Gestión de Lugares Excluidos";
placesTitle.style.fontSize = "15px";
placesTitle.style.marginBottom = "10px";
placesView.appendChild(placesTitle);
// Controles de búsqueda y lista de lugares
const placesSearchInput = document.createElement("input");
placesSearchInput.type = "text";
placesSearchInput.placeholder = "Buscar lugar excluido...";
placesSearchInput.style.display = "block";
placesSearchInput.style.width = "calc(100% - 14px)";
placesSearchInput.style.padding = "6px";
placesSearchInput.style.border = "1px solid #ccc";
placesSearchInput.style.borderRadius = "3px";
placesSearchInput.style.marginBottom = "5px";
placesSearchInput.addEventListener("input", () =>
{
renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"), placesSearchInput.value.trim());
});
placesView.appendChild(placesSearchInput);
const placesListUL = document.createElement("ul");
placesListUL.id = "excludedPlacesListUL"; // Nuevo ID para la lista de Places
placesListUL.style.maxHeight = "200px"; // Un poco más grande
placesListUL.style.overflowY = "auto";
placesListUL.style.border = "1px solid #ddd";
placesListUL.style.padding = "5px";
placesListUL.style.margin = "0";
placesListUL.style.background = "#fff";
placesListUL.style.listStyle = "none";
placesView.appendChild(placesListUL);
// Botones de acción para Lugares Excluidos
const placesActionButtonsContainer = document.createElement("div");
placesActionButtonsContainer.style.display = "flex";
placesActionButtonsContainer.style.gap = "8px";
placesActionButtonsContainer.style.marginTop = "10px";
const exportPlacesBtn = document.createElement("button");
exportPlacesBtn.textContent = "Exportar";
exportPlacesBtn.title = "Exportar Lugares Excluidos a XML";
exportPlacesBtn.style.padding = "6px 10px";
exportPlacesBtn.style.cursor = "pointer";
exportPlacesBtn.addEventListener("click", () => exportSharedDataToXml("places")); // Pasa el tipo
placesActionButtonsContainer.appendChild(exportPlacesBtn);
const clearPlacesBtn = document.createElement("button");
clearPlacesBtn.textContent = "Limpiar";
clearPlacesBtn.title = "Limpiar lista de lugares excluidos";
clearPlacesBtn.style.padding = "6px 10px";
clearPlacesBtn.style.cursor = "pointer";
clearPlacesBtn.addEventListener("click", () => {
if (confirm("¿Estás seguro de que deseas eliminar TODOS los lugares de la lista?")) {
excludedPlaces.clear();
renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"));
saveExcludedPlacesToLocalStorage();
}
});
placesActionButtonsContainer.appendChild(clearPlacesBtn);
placesView.appendChild(placesActionButtonsContainer);
// Drop Area para XML de Lugares Excluidos
const placesDropArea = document.createElement("div");
placesDropArea.textContent = "Arrastra aquí el archivo XML de lugares excluidos";
placesDropArea.style.border = "2px dashed #ccc";
placesDropArea.style.borderRadius = "4px";
placesDropArea.style.padding = "15px";
placesDropArea.style.marginTop = "10px";
placesDropArea.style.textAlign = "center";
placesDropArea.style.background = "#f9f9f9";
placesDropArea.style.color = "#555";
placesDropArea.addEventListener("dragover", (e) =>
{
e.preventDefault();
placesDropArea.style.background = "#e9e9e9";
placesDropArea.style.borderColor = "#aaa";
});
placesDropArea.addEventListener("dragleave", () =>
{
placesDropArea.style.background = "#f9f9f9";
placesDropArea.style.borderColor = "#ccc";
});
placesDropArea.addEventListener("drop", (e) =>
{
e.preventDefault();
placesDropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0], "places"); // Pasa el tipo de importación
});
placesView.appendChild(placesDropArea);
// ***********************************************************************************
// FIN DEL CONTENIDO DE LA VISTA DE LUGARES EXCLUIDOS
// ***********************************************************************************
// --- Lógica de alternancia del selector ---
typeSelector.addEventListener("change", () => {
if (typeSelector.value === "words") {
wordsView.style.display = "block";
placesView.style.display = "none";
renderExcludedWordsList(document.getElementById("excludedWordsList"), wordsSearchInput.value.trim()); // Renderiza lista de palabras
} else {
wordsView.style.display = "none";
placesView.style.display = "block";
renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"), placesSearchInput.value.trim()); // Renderiza lista de lugares
}
});
// --- Renderizado inicial de las listas al cargar ---
renderExcludedWordsList(wordsListUL, ""); // Usa la referencia directa a wordsListUL
renderExcludedPlacesList(placesListUL, ""); // Usa la referencia directa a placesListUL
parentContainer.appendChild(mainSection); // <--- AÑADE SOLO ESTA SECCIÓN PRINCIPAL AL PARENT CONTAINER
}
// Función para validar una palabra o frase antes de añadirla a las palabras excluidas
function prepararDatosDiccionario()
{
// Convertir el Set de palabras a un array de objetos con el formato requerido
const datos = Array.from(window.dictionaryWords || []).map(palabra =>
{
return {
termino: palabra,
definicion: "" // Si no tiene definición, dejar vacío
};
});
// Ordenar alfabéticamente para mejor navegación
return datos.sort((a, b) => a.termino.localeCompare(b.termino));
}//prepararDatosDiccionario
// Actualiza la etiqueta que muestra el conteo de palabras en el diccionario
function updateDictionaryWordsCountLabel()
{
if (!dictionaryWordsCountLabelElement)
{
dictionaryWordsCountLabelElement = document.getElementById("dictionaryWordsCountLabel");
}
if (!dictionaryWordsCountLabelElement)
{
return;
}
const count = window.dictionaryWords ? window.dictionaryWords.size : 0;
dictionaryWordsCountLabelElement.textContent = `Palabras Cargadas: ${count}`;
}//updateDictionaryWordsCountLabel
// === Diccionario ===
function createDictionaryManager(parentContainer)
{
const section = document.createElement("div");
section.id = "dictionaryManagerSection";
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
// Título de la sección
const title = document.createElement("h4");
title.textContent = "Gestión del Diccionario";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
section.appendChild(title);
// Contenedor para los controles de añadir palabra
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems = "center";
// Input para añadir nueva palabra
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra";
input.style.flexGrow = "1";
input.style.padding = "6px";
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
// Botón para añadir la palabra
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px";
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function() {
const newWord = input.value.trim();
const validation = isValidExcludedWord(newWord);
if (!validation.valid) {
alert(validation.msg);
return;
}
if (newWord) {
const lowerNewWord = newWord.toLowerCase();
const alreadyExists = Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord);
if (commonWords.includes(lowerNewWord)) {
alert("La palabra es muy común y no debe agregarse a la lista.");
return;
}
if (alreadyExists) {
alert("La palabra ya está en la lista.");
return;
}
window.dictionaryWords.add(lowerNewWord);
input.value = "";
// Reinicializar el diccionario dinámico con los datos actualizados
const listContainer = document.getElementById("dictionaryContainer");
if (listContainer) {
listContainer.innerHTML = "";
inicializarDiccionarioDinamico("dictionaryContainer", prepararDatosDiccionario());
}
updateDictionaryWordsCountLabel();
}
});
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
// Contenedor para los botones de acción
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.alignItems = "center";
actionButtonsContainer.style.marginBottom = "10px";
/*
// Botón para limpiar
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar";
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function() {
if (confirm("¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?")) {
window.dictionaryWords.clear();
// Reinicializar el diccionario dinámico con los datos actualizados
const listContainer = document.getElementById("dictionaryContainer");
if (listContainer) {
listContainer.innerHTML = "";
inicializarDiccionarioDinamico("dictionaryContainer", prepararDatosDiccionario());
}
updateDictionaryWordsCountLabel();
}
});
actionButtonsContainer.appendChild(clearBtn);*/
const dictionaryCountLabel = document.createElement("span");
dictionaryCountLabel.id = "dictionaryWordsCountLabel";
dictionaryCountLabel.style.marginLeft = "auto";
dictionaryCountLabel.style.fontSize = "12px";
dictionaryCountLabel.style.color = "#333";
dictionaryCountLabel.style.whiteSpace = "nowrap";
dictionaryWordsCountLabelElement = dictionaryCountLabel;
updateDictionaryWordsCountLabel();
actionButtonsContainer.appendChild(dictionaryCountLabel);
section.appendChild(actionButtonsContainer);
// Campo de búsqueda
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en diccionario...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)";
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginTop = "5px";
search.style.marginBottom = "10px";
// Implementar búsqueda en tiempo real
search.addEventListener("input", () => {
const searchTerm = search.value.trim().toLowerCase();
const filteredData = prepararDatosDiccionario().filter(item =>
item.termino.toLowerCase().includes(searchTerm)
);
// Actualizar la visualización con datos filtrados
const listContainer = document.getElementById("dictionaryContainer");
if (listContainer) {
listContainer.innerHTML = "";
inicializarDiccionarioDinamico("dictionaryContainer", filteredData);
}
});
section.appendChild(search);
// Contenedor para el diccionario con carga bajo demanda
const dictionaryContainer = document.createElement("div");
dictionaryContainer.id = "dictionaryContainer";
dictionaryContainer.style.maxHeight = "250px";
dictionaryContainer.style.overflowY = "auto";
dictionaryContainer.style.border = "1px solid #ddd";
dictionaryContainer.style.padding = "5px";
dictionaryContainer.style.margin = "0";
dictionaryContainer.style.background = "#fff";
section.appendChild(dictionaryContainer);
// Área para soltar archivos XML
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML del diccionario";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
// Eventos de arrastrar y soltar
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
dropArea.addEventListener("dragleave", () => {
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
const file = e.dataTransfer.files[0];
if (file && (file.type === "text/xml" || file.name.endsWith(".xml"))) {
const reader = new FileReader();
reader.onload = function(evt) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError) {
console.error("[WME PLN] Error parseando XML:", parserError.textContent);
alert("Error al parsear el archivo XML del diccionario.");
return;
}
const xmlWords = xmlDoc.querySelectorAll("word");
let newWordsAddedCount = 0;
for (let i = 0; i < xmlWords.length; i++) {
const val = xmlWords[i].textContent.trim();
if (val && !window.dictionaryWords.has(val)) {
window.dictionaryWords.add(val);
newWordsAddedCount++;
}
}
// Actualizar la visualización después de importar
const listContainer = document.getElementById("dictionaryContainer");
if (listContainer) {
listContainer.innerHTML = "";
inicializarDiccionarioDinamico("dictionaryContainer", prepararDatosDiccionario());
}
updateDictionaryWordsCountLabel();
if (newWordsAddedCount > 0) {
alert(`${newWordsAddedCount} nuevas palabras añadidas desde XML.`);
} else {
alert("No se encontraron palabras nuevas para añadir.");
}
} catch (err) {
alert("Error procesando el diccionario XML.");
console.error("[WME PLN] Error procesando XML:", err);
}
};
reader.readAsText(file);
} else {
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
// Añadir todo al contenedor principal
parentContainer.appendChild(section);
// Inicializar el diccionario con carga bajo demanda
setTimeout(() => {
inicializarDiccionarioDinamico("dictionaryContainer", prepararDatosDiccionario());
updateDictionaryWordsCountLabel();
}, 100);
}//createDictionaryManager
/* function createDictionaryManager(parentContainer)
{
const section = document.createElement("div");
section.id = "dictionaryManagerSection";
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
// Título de la sección
const title = document.createElement("h4");
title.textContent = "Gestión del Diccionario";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
section.appendChild(title);
// Contenedor para los controles de añadir palabra
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems = "center"; // Alinear verticalmente
// Input para añadir nueva palabra
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra";
input.style.flexGrow = "1";
input.style.padding = "6px"; // Mejor padding
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
// Botón para añadir la palabra
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px"; // Mejor padding
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function ()
{
const newWord = input.value.trim();
const validation = isValidExcludedWord(newWord);
if (!validation.valid) {
alert(validation.msg);
return;
}
if (newWord)
{
const lowerNewWord = newWord.toLowerCase();
const alreadyExists =
Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord);
if (commonWords.includes(lowerNewWord))
{
alert("La palabra es muy común y no debe agregarse a la lista.");
return;
}
if (alreadyExists)
{
alert("La palabra ya está en la lista.");
return;
}
window.dictionaryWords.add(lowerNewWord);
input.value = "";
renderDictionaryList(document.getElementById("dictionaryWordsList"));
}
});
// Añadir tooltip al botón
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
// Contenedor para los botones de acción
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px"; // Más espacio
// Botón para importar desde XML
const exportBtn = document.createElement("button");
exportBtn.textContent = "Exportar"; // Más corto
exportBtn.title = "Exportar Diccionario a XML";
exportBtn.style.padding = "6px 10px";
exportBtn.style.cursor = "pointer";
exportBtn.addEventListener("click", exportDictionaryWordsList);
actionButtonsContainer.appendChild(exportBtn);
// Botón para importar desde XML
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar"; // Más corto
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function ()
{
if (confirm("¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?"))
{
window.dictionaryWords.clear();
renderDictionaryList(document.getElementById("dictionaryWordsList")); // Pasar el elemento UL
}
});
actionButtonsContainer.appendChild(clearBtn);
section.appendChild(actionButtonsContainer);
// Diccionario: búsqueda
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en diccionario...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)";
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginTop = "5px";
// On search input, render filtered list
search.addEventListener("input", () =>
{
renderDictionaryList(document.getElementById("dictionaryWordsList"),search.value.trim());
});
section.appendChild(search);
// Lista UL para mostrar palabras del diccionario
const listContainerElement = document.createElement("ul");
listContainerElement.id = "dictionaryWordsList";
listContainerElement.style.maxHeight = "150px";
listContainerElement.style.overflowY = "auto";
listContainerElement.style.border = "1px solid #ddd";
listContainerElement.style.padding = "5px";
listContainerElement.style.margin = "0";
listContainerElement.style.background = "#fff";
listContainerElement.style.listStyle = "none";
section.appendChild(listContainerElement);
// Renderizar la lista de palabras del diccionario
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML del diccionario";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
// Añadir eventos de arrastrar y soltar
dropArea.addEventListener("dragover", (e) =>
{
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
// Evento para cuando el ratón sale del área de arrastre
dropArea.addEventListener("dragleave", () =>
{
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
// Evento para cuando se suelta el archivo
dropArea.addEventListener("drop", (e) =>
{
e.preventDefault();
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
const file = e.dataTransfer.files[0];
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function (evt)
{
try
{
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(evt.target.result,
"application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
console.error("[WME PLN] Error parseando XML:", parserError.textContent);
alert("Error al parsear el archivo XML del diccionario.");
return;
}
const xmlWords = xmlDoc.querySelectorAll("word");
let newWordsAddedCount = 0;
for (let i = 0; i < xmlWords.length; i++)
{
const val = xmlWords[i].textContent.trim();
if (val && !window.dictionaryWords.has(val))
{
window.dictionaryWords.add(val);
newWordsAddedCount++;
}
}
if (newWordsAddedCount > 0)
//console.log(`[WME PLN] ${newWordsAddedCount} nuevas palabras añadidas desde XML.`);
// Renderizar la lista en el panel
renderDictionaryList(listContainerElement);
}
catch (err)
{
alert("Error procesando el diccionario XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
parentContainer.appendChild(section);
renderDictionaryList(listContainerElement);
}*/// createDictionaryManager
// Helper de normalización para comparación de reemplazos (insensible a mayúsculas, tildes, puntos y espacios)
function plnNormalizeReplacementKey(s){
try{
return String(s||'')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g,'') // quitar diacríticos
.replace(/[\.\s]+/g,'') // quitar puntos y espacios
.toLowerCase()
.trim();
}catch(_){ return String(s||'').toLowerCase().trim(); }
}
// Carga las palabras excluidas desde localStorage
function loadReplacementWordsFromStorage()
{
const savedReplacements = localStorage.getItem("replacementWordsList");
if (savedReplacements)
{
try
{
replacementWords = JSON.parse(savedReplacements);
if (typeof replacementWords !== 'object' || replacementWords === null)
{ // Asegurar que sea un objeto
replacementWords = {};
}
// Cargar mapa de fuentes (usuario/hoja) y asegurar valores por defecto
try {
const savedSources = localStorage.getItem("replacementSources");
window.replacementSources = savedSources ? JSON.parse(savedSources) : {};
} catch (e) {
window.replacementSources = {};
}
// Para cualquier reemplazo existente sin fuente, asumir que es del usuario
Object.keys(replacementWords).forEach(k => {
if (!window.replacementSources || typeof window.replacementSources !== 'object') {
window.replacementSources = {};
}
if (!window.replacementSources[k]) {
window.replacementSources[k] = 'user';
}
});
// --- NUEVO: Normalización y eliminación de conflictos con reglas de hoja ---
try {
// Construir índice normalizado de claves para evitar duplicados por mayúsculas/puntos/tildes
const seen = new Map(); // norm(from) -> canonical from
Object.keys(replacementWords).forEach(from => {
const lc = plnNormalizeReplacementKey(from);
if (seen.has(lc)) {
// Preferir la regla de hoja si hay duplicado, si no mantener la primera
const keepKey = (window.replacementSources[seen.get(lc)] === 'sheet') ? seen.get(lc)
: (window.replacementSources[from] === 'sheet' ? from : seen.get(lc));
const dropKey = keepKey === from ? seen.get(lc) : from;
if (dropKey && dropKey !== keepKey) {
delete replacementWords[dropKey];
if (window.replacementSources) delete window.replacementSources[dropKey];
}
seen.set(lc, keepKey);
} else {
seen.set(lc, from);
}
});
} catch(_) {}
// Guardar por si hubo saneo
saveReplacementWordsToStorage();
}
catch (e)
{
console.error("[WME PLN] Error cargando lista de reemplazos desde localStorage:", e);
replacementWords = {};
window.replacementSources = {};
}
}
else
{
replacementWords = {}; // Inicializar si no hay nada guardado
window.replacementSources = {};
}
//console.log("[WME PLN] Reemplazos cargados:", Object.keys(replacementWords).length, "reglas.");
}// loadReplacementWordsFromStorage
// Carga las palabras excluidas desde localStorage
// Función para guardar las palabras swap en localStorage (formato nuevo)
function saveSwapWordsToStorage()
{
try
{
localStorage.setItem("swapWords", JSON.stringify(window.swapWords || []));
//console.log("[WME PLN] SwapWords guardadas en localStorage:", window.swapWords ? window.swapWords.length : 0, "palabras");
}
catch (e)
{
console.error("[WME PLN] Error guardando swapWords en localStorage:", e);
}
}// saveSwapWordsToStorage
// Carga las palabras reemplazo
function saveReplacementWordsToStorage()
{
try
{
localStorage.setItem("replacementWordsList",
JSON.stringify(replacementWords));
// Guardar también las fuentes de reemplazo
try {
localStorage.setItem("replacementSources", JSON.stringify(window.replacementSources || {}));
} catch (e) {
console.warn('[WME PLN] Error guardando replacementSources en localStorage:', e);
}
// console.log("[WME PLN] Lista de reemplazos guardada en localStorage.");
}
catch (e)
{
console.error("[WME PLN] Error guardando lista de reemplazos en localStorage:", e);
}
}// saveReplacementWordsToStorage
// Carga reemplazos desde Google Sheets y fusiona con los del usuario, bloqueando los de hoja
async function loadReplacementsFromSheet(forceReload = false)
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const SHEET_CANDIDATES = [
"Replace!A2:B", // Solicitud del usuario: la hoja se llama "Replace"
]; // A=from, B=to
// Asegurar estructuras
if (typeof replacementWords !== 'object' || !replacementWords) replacementWords = {};
if (typeof window.replacementSources !== 'object' || !window.replacementSources) window.replacementSources = {};
// Evitar si no se configuró
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") {
console.warn('[WME PLN] SPREADSHEET_ID o API_KEY no configurados para reemplazos.');
return;
}
// Intentar usar caché (24h)
const cacheKey = 'wme_pln_replacements_cache';
const cached = localStorage.getItem(cacheKey);
if (!forceReload && cached) {
try {
const { data, timestamp } = JSON.parse(cached);
if (data && timestamp && (Date.now() - timestamp < 24 * 60 * 60 * 1000)) {
// Exponer Set con orígenes fijos de hoja (exactos) para validaciones UI
try {
window.fixedReplacementFroms = new Set(Array.isArray(data) ? data.map(d => d.from) : []);
// También mantener sets normalizados para comparaciones flexibles (tildes, puntos, espacios, mayúsculas)
const rows = Array.isArray(data) ? data : [];
window.fixedReplacementFromsNorm = new Set(rows.map(d => plnNormalizeReplacementKey(d.from)));
// NUEVO: proteger también los destinos (columna B) del Sheet
window.fixedReplacementTargets = new Set(rows.map(d => d.to));
window.fixedReplacementTargetsNorm = new Set(rows.map(d => plnNormalizeReplacementKey(d.to)));
// NUEVO: índice normalizado de hoja y reverso para detectar contradicciones de forma global
try {
window.fixedReplacementIndex = new Map(rows.map(d => [plnNormalizeReplacementKey(d.from), plnNormalizeReplacementKey(d.to)]));
window.fixedReplacementReverseIndex = new Map(rows.map(d => [plnNormalizeReplacementKey(d.to), plnNormalizeReplacementKey(d.from)]));
window._fixedReplacementPairsRaw = rows.slice();
} catch(_){ window.fixedReplacementIndex = null; window.fixedReplacementReverseIndex = null; }
} catch(_) {
window.fixedReplacementFroms = new Set();
window.fixedReplacementFromsNorm = new Set();
window.fixedReplacementTargets = new Set();
window.fixedReplacementTargetsNorm = new Set();
}
// Fusionar datos en memoria
mergeSheetReplacementsIntoLocal(data);
// NUEVO: poda adicional de reglas de usuario en contradicción
try { plnPruneContradictoryUserReplacements?.(); } catch(_) {}
// Re-render si existe la lista
{
const _el = document.getElementById("replacementsListUL") || document.querySelector("#replacementsContainer ul");
const _sel = document.getElementById("replacementModeSelector");
if (_el)
{
if (_sel && _sel.value === "swapStart" && typeof renderSwapList === "function")
{
renderSwapList(_el);
}
else
{
renderReplacementsList(_el);
}
}
}
return;
}
}
catch (e)
{
console.warn('[WME PLN] Error leyendo caché de reemplazos:', e);
}
}
// Intentar con múltiples posibles rangos por si el nombre de la pestaña difiere
return new Promise((resolve) => {
const tryNext = (idx) => {
if (idx >= SHEET_CANDIDATES.length)
{
console.warn('[WME PLN] No se encontraron reemplazos en ninguna pestaña candidata de Google Sheets.');
{
try
{
if (typeof plnCanonicalizeReplacementsBySheet === 'function') plnCanonicalizeReplacementsBySheet();
}
catch (_)
{
}
requestAnimationFrame(() => {
const _el = document.getElementById("replacementsListUL") || document.querySelector("#replacementsContainer ul");
const _sel = document.getElementById("replacementModeSelector");
if (_el)
{
if (_sel && _sel.value === "swapStart" && typeof renderSwapList === "function")
{
renderSwapList(_el);
}
else
{
renderReplacementsList(_el);
}
}
});
}
resolve();
return;
}
const RANGE = SHEET_CANDIDATES[idx];
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
// console.log('[WME PLN] Cargando reemplazos desde:', RANGE);
makeRequest({
method: 'GET',
url,
timeout: 10000,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const payload = JSON.parse(response.responseText);
const rows = Array.isArray(payload.values) ? payload.values : [];
const sheetData = rows
.map(r => ({ from: (r[0] ?? '').toString().trim(), to: (r[1] ?? '').toString() }))
.filter(item => item.from.length > 0);
if (sheetData.length === 0) {
// Intentar con la siguiente candidata
tryNext(idx + 1);
return;
}
// Guardar caché y fusionar
try { localStorage.setItem(cacheKey, JSON.stringify({ data: sheetData, timestamp: Date.now() })); } catch {}
// Exponer Set con orígenes fijos de hoja (exactos) para validaciones UI
try {
window.fixedReplacementFroms = new Set(sheetData.map(d => d.from));
// Sets adicionales con claves normalizadas para detección/lock por equivalencia
window.fixedReplacementFromsNorm = new Set(sheetData.map(d => plnNormalizeReplacementKey(d.from)));
window.fixedReplacementTargets = new Set(sheetData.map(d => d.to));
window.fixedReplacementTargetsNorm = new Set(sheetData.map(d => plnNormalizeReplacementKey(d.to)));
// Índices globales normalizados para detección de contradicciones
try {
window.fixedReplacementIndex = new Map(sheetData.map(d => [plnNormalizeReplacementKey(d.from), plnNormalizeReplacementKey(d.to)]));
window.fixedReplacementReverseIndex = new Map(sheetData.map(d => [plnNormalizeReplacementKey(d.to), plnNormalizeReplacementKey(d.from)]));
window._fixedReplacementPairsRaw = sheetData.slice();
} catch(_){ window.fixedReplacementIndex = null; window.fixedReplacementReverseIndex = null; }
} catch(_) {
window.fixedReplacementFroms = new Set();
window.fixedReplacementFromsNorm = new Set();
window.fixedReplacementTargets = new Set();
window.fixedReplacementTargetsNorm = new Set();
}
mergeSheetReplacementsIntoLocal(sheetData);
// Poda adicional (defensa en profundidad) de reglas de usuario contradictorias
try { plnPruneContradictoryUserReplacements?.(); } catch(_) {}
// NUEVO: eliminar reglas de usuario que contradigan reglas de hoja (reverse mapping)
try {
const sheetIndex = new Map(); // norm(from) -> norm(to)
sheetData.forEach(({from,to})=>{
sheetIndex.set(plnNormalizeReplacementKey(from), plnNormalizeReplacementKey(to));
});
let changed = false;
Object.keys(replacementWords).forEach(userFrom => {
if (window.replacementSources[userFrom] === 'sheet') return; // solo usuario
const userTo = String(replacementWords[userFrom] || '');
// Si existe una regla de hoja que sea el reverso (normalizada)
const sheetTo = sheetIndex.get(plnNormalizeReplacementKey(userTo));
if (sheetTo && sheetTo === plnNormalizeReplacementKey(userFrom)) {
delete replacementWords[userFrom];
if (window.replacementSources) delete window.replacementSources[userFrom];
changed = true;
}
});
if (changed) saveReplacementWordsToStorage();
}
catch (_)
{
}
}
catch (e)
{
console.error('[WME PLN] Error procesando reemplazos de Google Sheets:', e);
}
}
else
{
// Intentar siguiente pestaña si 404/400 (rango inválido)
// Para otros códigos, solo avisar y continuar
// console.warn(`[WME PLN] Error HTTP ${response.status} al cargar rango ${RANGE}`);
tryNext(idx + 1);
return;
}
{
try
{
if (typeof plnCanonicalizeReplacementsBySheet === 'function') plnCanonicalizeReplacementsBySheet();
}
catch (_)
{
}
requestAnimationFrame(() => {
const _el = document.getElementById("replacementsListUL") || document.querySelector("#replacementsContainer ul");
const _sel = document.getElementById("replacementModeSelector");
if (_el)
{
if (_sel && _sel.value === "swapStart" && typeof renderSwapList === "function")
{
renderSwapList(_el);
}
else
{
renderReplacementsList(_el);
}
}
});
}
resolve();
},
onerror: function () {
tryNext(idx + 1);
},
ontimeout: function () {
tryNext(idx + 1);
}
});
};
tryNext(0);
});
// Fusión de reemplazos de hoja con los del usuario. Previene duplicados; respeta los del usuario.
function mergeSheetReplacementsIntoLocal(sheetPairs)
{
// Construir un mapa temporal para eliminar duplicados (case-insensitive, último gana)
const tempMap = new Map(); // key: norm(from), value: {from, to}
sheetPairs.forEach(({ from, to }) => {
const key = plnNormalizeReplacementKey(from || '');
if (!key) return;
tempMap.set(key, { from, to });
});
// NUEVO: construir set de destinos normalizados de la hoja (columna B)
let sheetTargetsNorm = new Set();
try {
sheetTargetsNorm = new Set(sheetPairs.map(p => plnNormalizeReplacementKey(p.to || '')));
} catch(_) {}
// Limpiar del caché local (usuario) cualquier regla cuyo origen coincida
// con una regla de hoja, usando comparación normalizada para evitar
// variantes de mayúsculas, puntos o tildes.
try {
const sheetNormSet = new Set(Array.from(tempMap.keys())); // claves normalizadas
const keys = Object.keys(replacementWords);
keys.forEach(fromKey => {
const isFromSheet = window.replacementSources && window.replacementSources[fromKey] === 'sheet';
if (isFromSheet) return;
const normLocal = plnNormalizeReplacementKey(fromKey);
if (sheetNormSet.has(normLocal) || sheetTargetsNorm.has(normLocal)) {
delete replacementWords[fromKey];
if (window.replacementSources) delete window.replacementSources[fromKey];
}
});
} catch(_) {}
// Fusionar en replacementWords: la lista fija de hoja SIEMPRE prevalece
for (const { from, to } of tempMap.values()) {
replacementWords[from] = to;
window.replacementSources[from] = 'sheet';
}
// Persistir cambios
saveReplacementWordsToStorage();
}
}// loadReplacementsFromSheet
// Carga las palabras excluidas desde localStorage
function saveExcludedWordsToLocalStorage()
{
try
{
localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords)));
// console.log("[WME PLN] Lista de palabras especiales guardada en localStorage.");
}
catch (e)
{
console.error("[WME PLN] Error guardando palabras especiales en localStorage:", e);
}
}// saveExcludedWordsToLocalStorage
// Función para guardar los IDs de lugares excluidos en localStorage
function saveExcludedPlacesToLocalStorage()
{
try {
// Convertir el Map a un array de arrays antes de stringify
localStorage.setItem("excludedPlacesList", JSON.stringify(Array.from(excludedPlaces.entries())));
console.log('[WME PLN] Lugares excluidos guardados:', excludedPlaces.size);
} catch (e) {
console.error('[WME PLN] Error guardando lugares excluidos en localStorage:', e);
}
}// saveExcludedPlacesToLocalStorage
// Renderiza la lista de reemplazos
function renderReplacementsList(ulElement)
{
//console.log("[WME_PLN][DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo");
if (!ulElement)
{
//console.error("[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList.");
return;
}
// Asegurar depuración de contradicciones antes de pintar
try { plnPruneContradictoryUserReplacements?.(); } catch(_) {}
ulElement.innerHTML = ""; // Limpiar lista actual
const entries = Object.entries(replacementWords);
// Si no hay reemplazos, mostrar mensaje
if (entries.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay reemplazos definidos.";
li.style.textAlign = "center";
li.style.color = "#777";
li.style.padding = "5px";
ulElement.appendChild(li);
return;
}
// Ordenar alfabéticamente por la palabra original (from)
entries.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()));
entries.forEach(([from, to]) =>
{
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
// Añadir un tooltip al elemento li
const textContainer = document.createElement("div");
textContainer.style.flexGrow = "1";
textContainer.style.overflow = "hidden";
textContainer.style.textOverflow = "ellipsis";
textContainer.style.whiteSpace = "nowrap";
// Determinar si esta regla proviene de hoja. Además del mapa de fuentes,
// considerar el set normalizado para bloquear equivalentes (p.ej. "I.E" vs "i.e.").
const isSheetLocked = (
(window.replacementSources && window.replacementSources[from] === 'sheet') ||
(window.fixedReplacementFromsNorm && window.fixedReplacementFromsNorm.has(plnNormalizeReplacementKey(from)))
);
const source = isSheetLocked ? 'sheet' : 'user';
textContainer.title = `Reemplazar "${from}" con "${to}"` + (source === 'sheet' ? ' [bloqueado de hoja]' : '');
// Crear los spans para mostrar el texto
const fromSpan = document.createElement("span");
fromSpan.textContent = from;
fromSpan.style.fontWeight = "bold";
textContainer.appendChild(fromSpan);
// Añadir un espacio entre el "from" y el "to"
const arrowSpan = document.createElement("span");
arrowSpan.textContent = " → ";
arrowSpan.style.margin = "0 5px";
textContainer.appendChild(arrowSpan);
// Span para el texto de reemplazo
const toSpan = document.createElement("span");
toSpan.textContent = to;
toSpan.style.color = "#007bff";
textContainer.appendChild(toSpan);
// Indicador de bloqueo si es de hoja
if (source === 'sheet') {
const lockSpan = document.createElement('span');
lockSpan.textContent = ' 🔒';
lockSpan.title = 'Reemplazo bloqueado por definición de la Wazeopedia Colombia';
textContainer.appendChild(lockSpan);
}
// Añadir el contenedor de texto al li
li.appendChild(textContainer);
// Botón Editar
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar este reemplazo";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px 4px";
editBtn.style.fontSize = "14px";
editBtn.style.marginLeft = "4px";
if (source === 'sheet') {
editBtn.disabled = true;
editBtn.style.opacity = '0.4';
editBtn.style.cursor = 'not-allowed';
editBtn.title = 'Reemplazo bloqueado por definición de la Wazeopedia Colombia';
}
editBtn.addEventListener("click", () =>
{
if ((window.replacementSources && window.replacementSources[from] === 'sheet')) {
alert('Este reemplazo proviene de reglas de la Wazeopedia Colombia y no puede editarse.');
return;
}
const newFrom = prompt("Editar texto original:", from);
if (newFrom === null) return;
const newTo = prompt("Editar texto de reemplazo:", to);
if (newTo === null) return;
if (!newFrom.trim())
{
alert("El campo 'Texto Original' es requerido.");
return;
}
if (newFrom === newTo)
{
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
// Permitir que el destino contenga al origen y viceversa (ej. "Av" → "Av.", "CED." → "CED").
// La prevención de bucles y conflictos se maneja más adelante con reglas
// de hoja (reversa) y con el motor de reemplazo.
// Regla: impedir editar para usar un ORIGEN que está en la lista fija si esta regla no es de hoja
try
{
if (
(window.fixedReplacementFroms && window.fixedReplacementFroms.has(newFrom)) ||
(window.fixedReplacementFromsNorm && window.fixedReplacementFromsNorm.has(plnNormalizeReplacementKey(newFrom))) ||
(window.fixedReplacementTargets && window.fixedReplacementTargets.has(newFrom)) ||
(window.fixedReplacementTargetsNorm && window.fixedReplacementTargetsNorm.has(plnNormalizeReplacementKey(newFrom)))
) {
alert("Este origen está reservado por la lista de Wazeopedia Colombia (como origen o destino) y no puede usarse como 'Texto Original'.");
return;
}
}
catch (_) { }
// Bloqueo por conflicto con regla de hoja (reversa)
try {
const nFromLC = plnNormalizeReplacementKey(newFrom);
const nToLC = plnNormalizeReplacementKey(newTo);
const keys = Object.keys(replacementWords || {});
for (const k of keys) {
if (window.replacementSources && window.replacementSources[k] === 'sheet') {
const sFromLC = plnNormalizeReplacementKey(k);
const sToLC = plnNormalizeReplacementKey(replacementWords[k] || '');
if (sFromLC === nToLC && sToLC === nFromLC) {
alert(`Esta edición contradice una regla bloqueada en Wazeopedia Colombia: "${k}" → "${replacementWords[k]}".`);
return;
}
}
}
} catch(_) {}
// Duplicado case-insensitive de la clave
try {
const nLc = plnNormalizeReplacementKey(newFrom);
const existingCI = Object.keys(replacementWords || {}).find(k => plnNormalizeReplacementKey(k) === nLc && k !== from);
if (existingCI) {
if (window.replacementSources && window.replacementSources[existingCI] === 'sheet') {
alert(`Ya existe una regla en Wazeopedia Colombia para "${existingCI}". No se puede sobrescribir.`);
return;
}
if (!confirm(`Ya existe una regla para "${existingCI}". ¿Deseas sobrescribirla con "${newFrom}" → "${newTo}"?`))
return;
delete replacementWords[existingCI];
if (window.replacementSources) delete window.replacementSources[existingCI];
}
} catch(_) {}
// Si cambia la clave, elimina la anterior
if (newFrom !== from) delete replacementWords[from];
replacementWords[newFrom] = newTo;
if (!window.replacementSources) window.replacementSources = {};
window.replacementSources[newFrom] = 'user';
delete window.replacementSources[from];
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
});
// Botón Eliminar
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = `Eliminar este reemplazo`;
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px 4px";
deleteBtn.style.fontSize = "14px";
deleteBtn.style.marginLeft = "4px";
if (source === 'sheet') {
deleteBtn.disabled = true;
deleteBtn.style.opacity = '0.4';
deleteBtn.style.cursor = 'not-allowed';
deleteBtn.title = 'Reemplazo bloqueado desde Wazeopedia Colombia';
}
deleteBtn.addEventListener("click", () =>
{
if ((window.replacementSources && window.replacementSources[from] === 'sheet')) {
alert('Este reemplazo proviene de Wazeopedia Colombia y no puede eliminarse.');
return;
}
if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`))
{
delete replacementWords[from];
if (window.replacementSources) delete window.replacementSources[from];
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
}
});
// Contenedor para los botones de acción
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "4px";
btnContainer.appendChild(editBtn);
btnContainer.appendChild(deleteBtn);
// Añadir el contenedor de botones al li
li.appendChild(btnContainer);
ulElement.appendChild(li);
});
}
// Exporta las palabras especiales y reemplazos a un archivo XML
function exportSharedDataToXml()
{
let xmlParts = [];
const rootTagName = "WME_PLN_Backup";
const fileName = "wme_pln_backup.xml";
if (excludedWords.size === 0 && excludedPlaces.size === 0 && Object.keys(replacementWords).length === 0 &&
(!window.swapWords || window.swapWords.length === 0) && Object.keys(editorStats).length === 0)
{
alert("No hay datos (palabras especiales, lugares excluidos, reemplazos, swap, estadísticas) para exportar.");
return;
}
// Exportar palabras especiales (excludedWords)
if (excludedWords.size > 0)
{
xmlParts.push(" <words>");
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.forEach(w => xmlParts.push(` <word>${xmlEscape(w)}</word>`));
xmlParts.push(" </words>");
}
// Exportar reemplazos (replacementWords)
if (Object.keys(replacementWords).length > 0)
{
xmlParts.push(" <replacements>");
Object.entries(replacementWords)
.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()))
.forEach(([from, to]) =>
{
xmlParts.push(` <replacement from="${xmlEscape(from)}">${xmlEscape(to)}</replacement>`);
});
xmlParts.push(" </replacements>");
}
// Exportar palabras de intercambio (swapWords)
if (window.swapWords && window.swapWords.length > 0)
{
xmlParts.push(" <swapWords>");
window.swapWords.forEach(item => {
if (typeof item === 'object' && item.word && item.direction) {
xmlParts.push(` <swap word="${xmlEscape(item.word)}" direction="${xmlEscape(item.direction)}"></swap>`);
}
});
xmlParts.push(" </swapWords>");
}
// Exportar estadísticas (editorStats)
if (Object.keys(editorStats).length > 0)
{
xmlParts.push(" <statistics>");
Object.entries(editorStats).forEach(([userId, data]) =>
{
xmlParts.push(` <editor id="${userId}"
name="${xmlEscape(data.userName || '')}"
total_count="${data.total_count || 0}"
monthly_count="${data.monthly_count || 0}"
monthly_period="${data.monthly_period || ''}"
weekly_count="${data.weekly_count || 0}"
weekly_period="${data.weekly_period || ''}"
daily_count="${data.daily_count || 0}"
daily_period="${data.daily_period || ''}"
last_update="${data.last_update || 0}" />`);
});
xmlParts.push(" </statistics>");
}
// Exportar lugares excluidos (excludedPlaces)
if (excludedPlaces.size > 0)
{
xmlParts.push(" <placeIds>");
Array.from(excludedPlaces.entries())
.sort((a, b) => (a[1] || '').toLowerCase().localeCompare(b[1] || ''))
.forEach(([id, name]) => {
xmlParts.push(` <placeId id="${xmlEscape(id)}" name="${xmlEscape(name || '')}"></placeId>`);
});
xmlParts.push(" </placeIds>");
}
// Construir el contenido XML completo
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootTagName}>\n${xmlParts.join("\n")}\n</${rootTagName}>`;
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// [PLN] Canonicalizar por hoja: drop si 'to' ∈ sheetTo; reencaminar si 'to' ∈ sheetFrom
function plnCanonicalizeReplacementsBySheet()
{
try
{
const map = window.replacementWords || {};
const src = window.replacementSources || {};
// Map y conjuntos de hoja (from -> to canónico)
const sheetFrom = [];
const sheetMap = {};
for (const f in map)
{
if (src[f] === 'sheet')
{
sheetFrom.push(f);
sheetMap[f] = String(map[f]||'').trim();
}
}
const sheetFromSet = new Set(sheetFrom);
const sheetToSet = new Set(sheetFrom.map(f => sheetMap[f]));
const newMap = {};
const newSrc = {};
for (const from in map)
{
const to = String(map[from]||'').trim();
const isSheet = src[from] === 'sheet';
// (A) Si el FROM local existe en hoja, imponer el B canónico de hoja
if (!isSheet && sheetFromSet.has(from))
{
const canonicalTo = sheetMap[from];
if (canonicalTo)
{
newMap[from] = canonicalTo;
if (src[from]) newSrc[from] = src[from];
}
continue;
}
// (B) Si el FROM local es de hoja, conservar tal cual (ya es canónico)
if (isSheet)
{
newMap[from] = to;
newSrc[from] = 'sheet';
continue;
}
// (C) Si el FROM local no es de hoja, aplicar reglas:
if (sheetFromSet.has(to))
{
const canonical = sheetMap[to]; // B de la hoja para ese A
if (canonical)
{
newMap[from] = canonical;
if (src[from]) newSrc[from] = src[from];
}
continue;
}
// 2) Si el 'to' local coincide con un 'from' de hoja → reencaminar al B canónico
if (sheetFromSet.has(to))
{
const canonical = sheetMap[to]; // B de la hoja para ese A
if (canonical)
{
newMap[from] = canonical;
if (src[from]) newSrc[from] = src[from];
}
continue;
}
// 3) Caso normal: conservar
newMap[from] = to;
if (src[from]) newSrc[from] = src[from];
}
window.replacementWords = newMap;
window.replacementSources = newSrc;
}
catch (e)
{
try
{
console.error('[WME PLN] plnCanonicalizeReplacementsBySheet error', e);
}
catch (_) { }
}
}//plnCanonicalizeReplacementsBySheet
// Función para manejar el archivo XML arrastrado
function handleXmlFileDrop(file, type = "words")
{
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt)
{
try
{
let newExcludedAdded = 0;
let newReplacementsAdded = 0;
let replacementsOverwritten = 0;
let newPlacesAdded = 0;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
alert("Error al parsear el archivo XML: " + parserError.textContent);
return;
}
const rootTag = xmlDoc.documentElement.tagName.toLowerCase();
if (type === "words")
{
if (rootTag !== "excludedwords")
{
alert("El archivo XML no es válido para Palabras Especiales. Debe tener <ExcludedWords> como raíz.");
return;
}
const words = xmlDoc.getElementsByTagName("word");
for (let i = 0; i < words.length; i++)
{
const val = words[i].textContent.trim();
// --- CORRECCIÓN CLAVE: Omitimos la validación al importar desde XML ---
// La validación es útil para la entrada manual, pero demasiado restrictiva para la importación,
// ya que rechaza palabras que ya existen en el diccionario.
if (val && !excludedWords.has(val))
{
excludedWords.add(val);
const firstChar = val.charAt(0).toLowerCase();
if (!excludedWordsMap.has(firstChar)) excludedWordsMap.set(firstChar, new Set());
excludedWordsMap.get(firstChar).add(val);
newExcludedAdded++;
}
}
const replacementNodes = xmlDoc.getElementsByTagName("replacement");
for (let i = 0; i < replacementNodes.length; i++) {
const from = replacementNodes[i].getAttribute("from")?.trim();
const to = replacementNodes[i].textContent.trim();
if (from && to)
{
// Saltar si el origen pertenece a la lista fija de Sheets
try
{
if ((window.fixedReplacementFroms && window.fixedReplacementFroms.has(from)) ||
(window.fixedReplacementFromsNorm && window.fixedReplacementFromsNorm.has(plnNormalizeReplacementKey(from))) ||
(window.fixedReplacementTargets && window.fixedReplacementTargets.has(from)) ||
(window.fixedReplacementTargetsNorm && window.fixedReplacementTargetsNorm.has(plnNormalizeReplacementKey(from)))) {
continue;
}
}
catch (_)
{ }
// Permitir que el destino contenga al origen y viceversa (ej. "Av" → "Av.", "CED." → "CED").
// Pero evitar introducir pares cuyo DESTINO coincide con un ORIGEN de la hoja (normalizado),
// para no colisionar con reglas canónicas: en ese caso, saltar la importación.
try {
if (window.fixedReplacementFromsNorm && window.fixedReplacementFromsNorm.has(plnNormalizeReplacementKey(to))) {
continue;
}
} catch(_) {}
if (window.replacementSources && window.replacementSources[from] === 'sheet')
{
// No permitir sobreescribir los que vienen de hoja
continue;
}
if (replacementWords.hasOwnProperty(from) && replacementWords[from] !== to)
{
replacementsOverwritten++;
}
else if (!replacementWords.hasOwnProperty(from))
{
newReplacementsAdded++;
}
replacementWords[from] = to;
if (!window.replacementSources) window.replacementSources = {};
window.replacementSources[from] = 'user';
}
}
}
else if (type === "places")
{
if (rootTag !== "placeIds" && rootTag !== "excludedwords")
{
alert("El archivo XML no es válido para Lugares Excluidos. Debe tener <placeIds> o <ExcludedWords> como raíz.");
return;
}
const placesNodes = xmlDoc.getElementsByTagName("placeId");
let placesUpdated = 0;
for (let i = 0; i < placesNodes.length; i++)
{
const placeId = placesNodes[i].getAttribute("id")?.trim();
const placeName = placesNodes[i].getAttribute("name")?.trim() || `ID: ${placeId}`;
if (placeId)
{
if (!excludedPlaces.has(placeId))
{
newPlacesAdded++;
}
else
{
placesUpdated++;
}
// Guardar solo el nombre
excludedPlaces.set(placeId, placeName);
}
}
alert(`Importación completada.\n- Lugares nuevos añadidos: ${newPlacesAdded}\n- Lugares existentes actualizados: ${placesUpdated}`);
saveExcludedPlacesToLocalStorage();
}
else
{
alert("Tipo de importación XML desconocido.");
return;
}
// En handleXmlFileDrop():
const swapWordsNode = xmlDoc.querySelector("swapWords");
if (swapWordsNode)
{
if (!window.swapWords) window.swapWords = [];
swapWordsNode.querySelectorAll("swap").forEach(swapNode => {
const word = swapNode.getAttribute("value");
const direction = swapNode.getAttribute("direction") || "start"; // Default a "start"
// Saltar si está protegido por hoja (Swap)
try {
const norm = (word||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
if (window.fixedSwapNormSet && window.fixedSwapNormSet.has(norm)) return;
} catch(_) {}
if (word && !window.swapWords.some(item => (typeof item === 'object' ? item.word : item) === word))
{
window.swapWords.push({ word: word, direction: direction });
}
});
saveSwapWordsToStorage();
}
const statsNode = xmlDoc.querySelector("statistics");
if (statsNode)
{
const editorNode = statsNode.querySelector("editor");
if (editorNode)
{
if (editorNode.hasAttribute("total_count"))
{
if (!currentGlobalUserInfo.id || currentGlobalUserInfo.name === 'No detectado')
{
showTemporaryMessage("Espera a que tu usuario se cargue para importar estadísticas.", 5000, "error");
}
else if (editorNode.getAttribute("id") === String(currentGlobalUserInfo.id))
{
editorStats[currentGlobalUserInfo.id] = {
userName: editorNode.getAttribute("name") || currentGlobalUserInfo.name,
total_count: parseInt(editorNode.getAttribute("total_count"), 10) || 0,
monthly_count: parseInt(editorNode.getAttribute("monthly_count"), 10) || 0,
monthly_period: editorNode.getAttribute("monthly_period") || '',
weekly_count: parseInt(editorNode.getAttribute("weekly_count"), 10) || 0,
weekly_period: editorNode.getAttribute("weekly_period") || '',
daily_count: parseInt(editorNode.getAttribute("daily_count"), 10) || 0,
daily_period: editorNode.getAttribute("daily_period") || '',
last_update: parseInt(editorNode.getAttribute("last_update"), 10) || 0
};
}
}
else
{
showTemporaryMessage("XML con formato de estadísticas antiguo detectado. Se omitió la importación.", 6000, "warning");
}
}
}
saveExcludedWordsToLocalStorage();
saveReplacementWordsToStorage();
saveEditorStats();
plnCanonicalizeReplacementsBySheet();
renderExcludedWordsList(document.getElementById("excludedWordsList"));
renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"));
// [PLN] Re-render según modo actual (replacements vs swap) tras canonicalizar
{
const _el = document.getElementById("replacementsListUL") || document.querySelector("#replacementsContainer ul");
const _sel = document.getElementById("replacementModeSelector");
if (_el)
{
if (_sel && _sel.value === "swapStart" && typeof renderSwapList === "function")
{
renderSwapList(_el);
}
else
{
renderReplacementsList(_el);
}
}
}
updateStatsDisplay();
alert(`Importación completada. \n - Palabras Especiales nuevas: ${newExcludedAdded} \n - Reemplazos nuevos: ${newReplacementsAdded} \n - Reemplazos sobrescritos: ${replacementsOverwritten} \n - Nuevos lugares: ${newPlacesAdded}`);
}
catch (err)
{
console.error("[WME PLN] Error procesando el archivo XML importado:", err);
alert("Ocurrió un error procesando el archivo XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
}//handleXmlFileDrop
// Bloquea todos los controles de la pestaña principal durante el escaneo
function disableScanControls()
{
const idsToDisable = [
'pln-start-scan-btn', 'maxPlacesInput', 'chk-recommend-categories',
'chk-avoid-my-edits', 'dateFilterSelect', 'chk-enable-stats'
];
// Deshabilitar los controles principales
idsToDisable.forEach(id =>
{
const el = document.getElementById(id);
if (el)
{
el.disabled = true;
el.style.opacity = '0.6';
el.style.cursor = 'not-allowed';
}
});
// Deshabilitar los botones de presets
document.querySelectorAll('.pln-preset-btn').forEach(btn =>
{
btn.disabled = true;
btn.style.opacity = '0.6';
btn.style.cursor = 'not-allowed';
});
}// disableScanControls
// Reactiva todos los controles de la pestaña principal al finalizar el escaneo
function enableScanControls()
{
const idsToEnable = [
'pln-start-scan-btn', 'maxPlacesInput', 'chk-recommend-categories',
'chk-avoid-my-edits', 'dateFilterSelect', 'chk-enable-stats'
];
// Reactivar los controles principales
idsToEnable.forEach(id =>
{
const el = document.getElementById(id);
if (el)
{
el.disabled = false;
el.style.opacity = '1';
el.style.cursor = 'pointer';
}
});
// Reactivar los botones de presets
document.querySelectorAll('.pln-preset-btn').forEach(btn =>
{
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
});
// Restaurar el estado del dropdown de fecha según el checkbox
const avoidMyEditsCheckbox = document.getElementById("chk-avoid-my-edits");
if (avoidMyEditsCheckbox)
{
const dateFilterRow = document.getElementById("dateFilterSelect").parentElement;
dateFilterRow.style.opacity = avoidMyEditsCheckbox.checked ? "1" : "0.5";
dateFilterRow.style.pointerEvents = avoidMyEditsCheckbox.checked ? "auto" : "none";
}
}// enableScanControls
// Carga las palabras swap desde localStorage
// Función para cargar las palabras swap desde localStorage con migración automática
function loadSwapWordsFromStorage()
{
const stored = localStorage.getItem("swapWords");
// Si hay datos en localStorage, intentar parsearlos
if (stored)
{
try
{
const parsed = JSON.parse(stored);
// MIGRACIÓN AUTOMÁTICA: Verificar el formato de los datos
if (Array.isArray(parsed) && parsed.length > 0)
{
// Verificar si es formato antiguo (array de strings)
if (typeof parsed[0] === "string")
{
//console.log("[WME PLN] Detectado formato antiguo de swapWords. Migrando automáticamente...");
// Migrar formato antiguo a nuevo formato
window.swapWords = parsed.map(word => ({
word: word,
direction: "start" // Todas las palabras existentes se configuran como "start" por defecto
}));
// Guardar el nuevo formato inmediatamente
saveSwapWordsToStorage();
//console.log(`[WME PLN] Migración completada: ${window.swapWords.length} palabras migradas a formato 'start'.`);
}
else if (typeof parsed[0] === "object" && parsed[0].hasOwnProperty('word'))
{
// Ya está en formato nuevo
window.swapWords = parsed;
//console.log("[WME PLN] Formato nuevo de swapWords detectado. No se requiere migración.");
}
else
{
// Formato desconocido, inicializar vacío
console.warn("[WME PLN] Formato desconocido en swapWords. Inicializando lista vacía.");
window.swapWords = [];
}
}
else
{
// Array vacío o null
window.swapWords = [];
}
}
catch (e)
{
console.error("[WME PLN] Error al parsear swapWords desde localStorage:", e);
window.swapWords = [];
}
}
else
{
// No hay datos guardados
window.swapWords = [];
}
}// loadSwapWordsFromStorage
// === Cargar Swap desde Google Sheets (hoja "Swap"): protege las entradas de hoja ===
async function loadSwapWordsFromSheet(forceReload = false)
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = "Swap!A2:B"; // A=token, B=direction
const normDir = v => {
const s = String(v||'').trim().toLowerCase();
if (s === 'start') return 'start';
if (s === 'end') return 'end';
return null;
};
const plnNormalizeSwapKey = s => {
try{
return String(s||'')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g,'')
.replace(/\s+/g,' ')
.trim()
.toLowerCase();
}catch(_){ return String(s||'').toLowerCase().trim(); }
};
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") return;
// Cache 24h
const cacheKey = 'wme_pln_swap_cache';
if (!forceReload) {
try{
const cached = JSON.parse(localStorage.getItem(cacheKey)||'null');
if (cached && cached.timestamp && (Date.now() - cached.timestamp < 24*60*60*1000)){
applySwapSheetRows(cached.data||[]);
return;
}
}catch(_){ }
}
return new Promise(resolve=>{
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
makeRequest({
method:'GET', url, timeout:10000,
onload: (res)=>{
try{
const payload = JSON.parse(res.responseText||'{}');
const rows = Array.isArray(payload.values) ? payload.values : [];
const data = rows.map(r=>({ word: (r[0]||'').toString().trim(), direction: normDir(r[1]) }))
.filter(x=>x.word && x.direction);
try{ localStorage.setItem(cacheKey, JSON.stringify({ data, timestamp: Date.now() })); }catch(_){ }
applySwapSheetRows(data);
}catch(e){ /* noop */ }
resolve();
},
onerror: ()=>resolve(),
ontimeout: ()=>resolve()
});
});
function applySwapSheetRows(rows){
try{
// Crear set de palabras protegidas por hoja (normalizadas)
window.fixedSwapNormSet = new Set(rows.map(r=>plnNormalizeSwapKey(r.word)));
if (!Array.isArray(window.swapWords)) window.swapWords = [];
// Quitar de memoria local cualquier palabra de usuario que choque con la hoja
window.swapWords = window.swapWords.filter(item => !window.fixedSwapNormSet.has(plnNormalizeSwapKey(item.word||item)));
// Añadir las de hoja (canónicas)
rows.forEach(r=>{
const direction = r.direction === 'end' ? 'end' : 'start';
window.swapWords.push({ word: r.word, direction });
});
try{ saveSwapWordsToStorage(); }catch(_){ }
}catch(_){ }
}
}
// === Recolector unificado de reglas de swap ===
function plnCollectSwapRules(){
try{
const normDir = v => {
v = String(v||'').toLowerCase();
if (v==='start') return 'before';
if (v==='end') return 'after';
return null;
};
const key = s => String(s||'')
.normalize('NFD').replace(/[\u0300-\u036f]/g,'')
.toLowerCase().trim();
// Precedencia: 1) window.swapWords 2) UI lists 3) localStorage -> luego OVERRIDES
const map = new Map(); // k -> { word, position, _pri }
const setRule = (w, d, pri) => {
w = String(w||'').trim();
d = normDir(d);
if (!w || !d) return;
const k = key(w);
const prev = map.get(k);
if (!prev || (prev._pri||0) <= pri) {
map.set(k, { word: w, position: d, _pri: pri });
}
};
// 1) Base: window.swapWords
(Array.isArray(window.swapWords)?window.swapWords:[]).forEach(x=>{
if (!x) return;
if (typeof x === 'string') { setRule(x,'before',1); return; }
const w = x.word || x.text || x.token || x.value || x.name;
const d = x.position || x.where || x.dir || x.direction;
setRule(w,d,1);
});
// 2) Listas de la UI si existen
const toArr = v => Array.isArray(v)?v:(typeof v==='string'?(()=>{try{return JSON.parse(v);}catch{return[]}})():[]);
const sStart = window.swapWordsStart || window.wordsStart || window.startWords || null;
const sEnd = window.swapWordsEnd || window.wordsEnd || window.endWords || null;
[
{ source: sStart, dir: 'before' },
{ source: sEnd, dir: 'after' }
].forEach(({ source, dir }) => {
if (source) {
toArr(source).forEach(w => setRule(w, dir, 2));
}
});
// 3) Escaneo de localStorage por claves relacionadas
try{
for (let i=0;i<localStorage.length;i++){
const k = localStorage.key(i);
const low = k.toLowerCase();
if (!/(^|:|_)swap(words)?|\bwords\b|palabra/i.test(low)) continue;
let val=null; try{ val = JSON.parse(localStorage.getItem(k)); }catch{}
if (!val) continue;
if (Array.isArray(val)){
const allStr = val.every(x=>typeof x==='string');
const allObj = val.every(x=>x && typeof x==='object');
if (allStr){
if (/inicio|start|before/.test(low)) val.forEach(w=>setRule(w,'before',3));
else if (/final|end|after/.test(low)) val.forEach(w=>setRule(w,'after',3));
} else if (allObj){
val.forEach(x=>{
const w = x.word||x.text||x.token||x.value||x.name;
let d = normDir(x.position||x.where||x.dir||x.direction);
if (!d){
if (/inicio|start|before/.test(low)) d='before';
else if (/final|end|after/.test(low)) d='after';
}
setRule(w,d,3);
});
}
}
}
}catch{}
return Array.from(map.values());
} catch (e) {
try{ console.error('[PLN Swap] plnCollectSwapRules error', e); }catch{}
// Fallback mínimo a window.swapWords normalizado + override
const key = s => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().trim();
const base = [];
(Array.isArray(window.swapWords)?window.swapWords:[]).forEach(x=>{
if (!x) return;
if (typeof x === 'string') base.push({ word:String(x).trim(), position:'before' });
else {
const w = x.word || x.text || x.token || x.value || x.name;
const d = String(x.position || x.where || x.dir || x.direction || 'before').toLowerCase();
base.push({ word:String(w||'').trim(), position:(d==='after'||d==='end'||d==='despues'||d==='después'||d==='post')?'after':'before' });
}
});
base.sort((a,b)=> b.word.length - a.word.length);
return base;
}
}
// Función para calcular el área en metros cuadrados de una geometría de polígono
// NOTA: Turf.js ha sido deshabilitado temporalmente, usando función alternativa
function calculateAreaMeters(shape)
{
if (!shape || !shape.geometry) {
return null; // Return null instead of Infinity
}
try {
// Check if we have a valid polygon geometry
if (shape.geometry.type === 'Polygon') {
// Extract coordinates from the polygon
const coordinates = shape.geometry.coordinates[0]; // First ring of coordinates
if (!coordinates || !Array.isArray(coordinates) || coordinates.length < 3) {
return null; // Not enough points for a valid polygon
}
// Use the Shoelace formula (Gauss's area formula) to calculate polygon area
let area = 0;
for (let i = 0; i < coordinates.length - 1; i++) {
if (!Array.isArray(coordinates[i]) || !Array.isArray(coordinates[i+1]) ||
coordinates[i].length < 2 || coordinates[i+1].length < 2) {
return null; // Invalid coordinate pair
}
area += coordinates[i][0] * coordinates[i+1][1];
area -= coordinates[i][1] * coordinates[i+1][0];
}
area = Math.abs(area) / 2;
// Convert to square meters based on projection
// This assumes the coordinates are in a geographic coordinate system
const metersPerDegree = 111319.9; // Approximate meters per degree at the equator
return area * Math.pow(metersPerDegree, 2);
}
} catch (error) {
console.warn("[WME PLN] Error calculating area:", error);
return null; // Return null on error
}
return null; // Default return for non-polygon shapes
}// calculateAreaMeters
// Crea el gestor de reemplazos
function createReplacementsManager(parentContainer)
{
loadSwapWordsFromStorage();
parentContainer.innerHTML = ''; // Limpiar por si acaso
// ===================================================================
// INICIO: NUEVA FUNCIÓN HELPER PARA EL MODAL DE EDICIÓN
// ===================================================================
function openSwapWordEditor(item, index) {
// Crear el fondo del modal
const modalOverlay = document.createElement("div");
modalOverlay.style.position = "fixed";
modalOverlay.style.top = "0";
modalOverlay.style.left = "0";
modalOverlay.style.width = "100%";
modalOverlay.style.height = "100%";
modalOverlay.style.background = "rgba(0,0,0,0.5)";
modalOverlay.style.zIndex = "20000";
modalOverlay.style.display = "flex";
modalOverlay.style.justifyContent = "center";
modalOverlay.style.alignItems = "center";
// Crear el contenido del modal
const modalContent = document.createElement("div");
modalContent.style.background = "#fff";
modalContent.style.padding = "25px";
modalContent.style.borderRadius = "8px";
modalContent.style.boxShadow = "0 5px 15px rgba(0,0,0,0.3)";
modalContent.style.width = "400px";
modalContent.style.fontFamily = "sans-serif";
// Título del modal
const title = document.createElement("h4");
title.textContent = "Editar Palabra Swap";
title.style.marginTop = "0";
title.style.marginBottom = "20px";
title.style.textAlign = "center";
modalContent.appendChild(title);
// Input para la palabra
const wordLabel = document.createElement("label");
wordLabel.textContent = "Palabra o Frase:";
wordLabel.style.display = "block";
wordLabel.style.marginBottom = "5px";
modalContent.appendChild(wordLabel);
const wordInput = document.createElement("input");
wordInput.type = "text";
wordInput.value = item.word;
wordInput.style.width = "calc(100% - 12px)";
wordInput.style.padding = "8px";
wordInput.style.marginBottom = "15px";
wordInput.setAttribute('spellcheck', 'false');
modalContent.appendChild(wordInput);
// Radio buttons para la dirección
const directionFieldset = document.createElement("fieldset");
directionFieldset.style.border = "1px solid #ccc";
directionFieldset.style.borderRadius = "5px";
directionFieldset.style.padding = "10px";
const legend = document.createElement("legend");
legend.textContent = "Mover a:";
directionFieldset.appendChild(legend);
['start', 'end'].forEach(dir => {
const container = document.createElement("div");
container.style.marginBottom = "5px";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editSwapDirection";
radio.value = dir;
radio.id = `editSwap_${dir}`;
if (item.direction === dir) radio.checked = true;
const label = document.createElement("label");
label.htmlFor = `editSwap_${dir}`;
label.textContent = ` ${dir === 'start' ? 'Al Inicio (Start ←A)' : 'Al Final (A→End)'}`;
label.style.cursor = "pointer";
container.appendChild(radio);
container.appendChild(label);
directionFieldset.appendChild(container);
});
modalContent.appendChild(directionFieldset);
// Contenedor para los botones de acción
const buttonContainer = document.createElement("div");
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
buttonContainer.style.gap = "10px";
buttonContainer.style.marginTop = "20px";
// Botón Cancelar
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "8px 15px";
cancelBtn.addEventListener("click", () => modalOverlay.remove());
buttonContainer.appendChild(cancelBtn);
// Botón Guardar
const saveBtn = document.createElement("button");
saveBtn.textContent = "Guardar Cambios";
saveBtn.style.padding = "8px 15px";
saveBtn.style.background = "#007bff";
saveBtn.style.color = "white";
saveBtn.style.border = "none";
saveBtn.style.borderRadius = "4px";
saveBtn.addEventListener("click", () => {
const newWord = wordInput.value.trim();
const newDirection = document.querySelector('input[name="editSwapDirection"]:checked').value;
if (!newWord) {
alert("La palabra no puede estar vacía.");
return;
}
// Verificar si el nuevo nombre ya existe (excluyendo el item actual)
if (newWord !== item.word && window.swapWords.some((sw, i) => i !== index && sw.word === newWord)) {
alert("Esa palabra ya existe en la lista.");
return;
}
// Actualizar el item en el array global
window.swapWords[index].word = newWord;
window.swapWords[index].direction = newDirection;
saveSwapWordsToStorage();
renderSwapList();
modalOverlay.remove();
});
buttonContainer.appendChild(saveBtn);
modalContent.appendChild(buttonContainer);
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
}
// ===================================================================
// FIN: NUEVA FUNCIÓN HELPER
// ===================================================================
// --- Contenedor principal ---
const title = document.createElement("h4");
title.textContent = "Gestión de Reemplazos";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
parentContainer.appendChild(title);
// --- Dropdown de modo de reemplazo ---
const modeSelector = document.createElement("select");
modeSelector.id = "replacementModeSelector";
modeSelector.style.marginBottom = "10px";
modeSelector.style.marginTop = "5px";
// Añadir opciones al selector
const optionWords = document.createElement("option");
optionWords.value = "words";
optionWords.textContent = "Reemplazos de palabras";
modeSelector.appendChild(optionWords);
// Añadir opción para swap
const optionSwap = document.createElement("option");
optionSwap.value = "swapStart";
optionSwap.textContent = "Palabras al inicio/final (swap)"; // Texto actualizado
modeSelector.appendChild(optionSwap);
parentContainer.appendChild(modeSelector);
//Contenedor para reemplazos y controles
const replacementsContainer = document.createElement("div");
replacementsContainer.id = "replacementsContainer";
// Sección para añadir nuevos reemplazos
const addSection = document.createElement("div");
addSection.style.display = "flex";
addSection.style.gap = "8px";
addSection.style.marginBottom = "12px";
addSection.style.alignItems = "flex-end"; // Alinear inputs y botón
// Contenedores para inputs de texto
const fromInputContainer = document.createElement("div");
fromInputContainer.style.flexGrow = "1";
const fromLabel = document.createElement("label");
fromLabel.textContent = "Texto Original:";
fromLabel.style.display = "block";
fromLabel.style.fontSize = "12px";
fromLabel.style.marginBottom = "2px";
// Input para el texto original
const fromInput = document.createElement("input");
fromInput.type = "text";
fromInput.placeholder = "Ej: Urb.";
fromInput.style.width = "95%"; // Para que quepa bien
fromInput.style.padding = "6px";
fromInput.style.border = "1px solid #ccc";
// Añadir label e input al contenedor
fromInputContainer.appendChild(fromLabel);
fromInputContainer.appendChild(fromInput);
addSection.appendChild(fromInputContainer);
// Contenedor para el texto de reemplazo
const toInputContainer = document.createElement("div");
toInputContainer.style.flexGrow = "1";
const toLabel = document.createElement("label");
toLabel.textContent = "Texto de Reemplazo:";
toLabel.style.display = "block";
toLabel.style.fontSize = "12px";
toLabel.style.marginBottom = "2px";
// Input para el texto de reemplazo
const toInput = document.createElement("input");
toInput.type = "text";
toInput.placeholder = "Ej: Urbanización";
toInput.style.width = "95%";
toInput.style.padding = "6px";
toInput.style.border = "1px solid #ccc";
toInputContainer.appendChild(toLabel);
toInputContainer.appendChild(toInput);
addSection.appendChild(toInputContainer);
// Atributos para evitar corrección ortográfica
fromInput.setAttribute('spellcheck', 'false');
toInput.setAttribute('spellcheck', 'false');
// Botón para añadir el reemplazo
const addReplacementBtn = document.createElement("button");
addReplacementBtn.textContent = "Añadir";
addReplacementBtn.style.padding = "6px 10px";
addReplacementBtn.style.cursor = "pointer";
addReplacementBtn.style.height = "30px"; // Para alinear con los inputs
addSection.appendChild(addReplacementBtn);
// Elemento UL para la lista de reemplazos
const listElement = document.createElement("ul");
listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista
listElement.style.maxHeight = "150px";
listElement.style.overflowY = "auto";
listElement.style.border = "1px solid #ddd";
listElement.style.padding = "8px";
listElement.style.margin = "0 0 10px 0";
listElement.style.background = "#fff";
listElement.style.listStyle = "none";
// Event listener para el botón "Añadir"
addReplacementBtn.addEventListener("click", () =>
{
const fromValue = fromInput.value.trim();
const toValue = toInput.value.trim();
if (!fromValue)
{
alert("El campo 'Texto Original' es requerido.");
return;
}
if (fromValue === toValue)
{
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
// Permitir que el destino contenga al origen y viceversa (ej. "Av" → "Av.", "CED." → "CED").
// La prevención de bucles y conflictos se maneja con las reglas de hoja (reversa)
// y el motor de reemplazo.
// Regla: no permitir crear reglas de usuario con orígenes fijos de Google Sheets
try {
if ((window.fixedReplacementFroms && window.fixedReplacementFroms.has(fromValue)) ||
(window.fixedReplacementFromsNorm && window.fixedReplacementFromsNorm.has(plnNormalizeReplacementKey(fromValue))) ||
(window.fixedReplacementTargets && window.fixedReplacementTargets.has(fromValue)) ||
(window.fixedReplacementTargetsNorm && window.fixedReplacementTargetsNorm.has(plnNormalizeReplacementKey(fromValue)))) {
alert("Este origen está reservado por la lista de Wazeopedia(como origen o destino) y no puede ser agregado por el usuario.");
return;
}
} catch(_) {}
if ((window.replacementSources && window.replacementSources[fromValue] === 'sheet'))
{
alert("No puedes modificar un reemplazo definido por la comunidad.");
return;
}
// Bloqueo: no permitir reglas que contradicen una regla de hoja (reversa)
try {
const userFromLC = plnNormalizeReplacementKey(fromValue);
const userToLC = plnNormalizeReplacementKey(toValue);
const keys = Object.keys(replacementWords || {});
for (const k of keys) {
if (window.replacementSources && window.replacementSources[k] === 'sheet') {
const sheetFromLC = plnNormalizeReplacementKey(k);
const sheetToLC = plnNormalizeReplacementKey(replacementWords[k] || '');
if (sheetFromLC === userToLC && sheetToLC === userFromLC) {
alert(`Esta regla contradice una regla bloqueada por la Comundiad de Editores: "${k}" → "${replacementWords[k]}".\nNo se puede crear "${fromValue}" → "${toValue}".`);
return;
}
}
}
} catch(_) {}
// Tratar duplicados case-insensitive de la clave 'from'
let existingKeySameCI = null;
try {
const lc = plnNormalizeReplacementKey(fromValue);
existingKeySameCI = Object.keys(replacementWords || {}).find(k => plnNormalizeReplacementKey(k) === lc) || null;
if (existingKeySameCI && existingKeySameCI !== fromValue) {
if (window.replacementSources && window.replacementSources[existingKeySameCI] === 'sheet') {
alert(`Ya existe una regla definida por la comunidad para "${existingKeySameCI}". No se puede crear otra con diferente capitalización.`);
return;
}
if (!confirm(`Ya existe una regla para "${existingKeySameCI}". ¿Deseas sobrescribirla con "${fromValue}" → "${toValue}"?`))
return;
}
} catch(_) {}
if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue)
{
if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`))
return;
}
const keyToStore = existingKeySameCI || fromValue;
if (existingKeySameCI && existingKeySameCI !== fromValue) {
if (existingKeySameCI in replacementWords && existingKeySameCI !== keyToStore) {
delete replacementWords[existingKeySameCI];
if (window.replacementSources) delete window.replacementSources[existingKeySameCI];
}
}
replacementWords[keyToStore] = toValue;
if (!window.replacementSources) window.replacementSources = {};
window.replacementSources[keyToStore] = 'user';
fromInput.value = "";
toInput.value = "";
renderReplacementsList(listElement);
saveReplacementWordsToStorage();
});
// Botones de Acción y Drop Area (usarán la lógica compartida)
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px";
// Botón para exportar solo reemplazos
const clearButton = document.createElement("button");
clearButton.textContent = "Limpiar Reemplazos";
clearButton.title = "Limpiar solo la lista de reemplazos";
clearButton.style.padding = "6px 10px";
clearButton.addEventListener("click", () =>
{
if (confirm("¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?"))
{
// Mantener los que provienen de Google Sheets (bloqueados)
const newRepl = {};
const newSources = {};
Object.entries(replacementWords).forEach(([k, v]) => {
if (window.replacementSources && window.replacementSources[k] === 'sheet') {
newRepl[k] = v;
newSources[k] = 'sheet';
}
});
replacementWords = newRepl;
window.replacementSources = newSources;
saveReplacementWordsToStorage();
renderReplacementsList(listElement);
}
});
actionButtonsContainer.appendChild(clearButton);
// Botón para importar desde XML
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
// Añadir estilos para el drop area
dropArea.addEventListener("dragover", (e) =>
{
e.preventDefault();
dropArea.style.background = "#e9e9e9";
});
// Cambiar el fondo al salir del área de arrastre
dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; });
// Manejar el evento de drop
dropArea.addEventListener("drop", (e) =>
{
e.preventDefault();
dropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0]);
});
// --- Ensamblar en replacementsContainer ---
replacementsContainer.appendChild(addSection);
replacementsContainer.appendChild(listElement);
replacementsContainer.appendChild(actionButtonsContainer);
replacementsContainer.appendChild(dropArea);
parentContainer.appendChild(replacementsContainer);
// --- Contenedor para swapStart/frases al inicio ---
const swapContainer = document.createElement("div");
swapContainer.id = "swapContainer";
swapContainer.style.display = "none";
// === TÍTULO Y EXPLICACIONES ===
const swapTitle = document.createElement("h4");
swapTitle.textContent = "Palabras de Intercambio (Swap)";
swapTitle.style.fontSize = "14px";
swapTitle.style.marginBottom = "8px";
swapContainer.appendChild(swapTitle);
const swapExplanationBox = document.createElement("div");
swapExplanationBox.style.background = "#f4f8ff";
swapExplanationBox.style.borderLeft = "4px solid #2d6df6";
swapExplanationBox.style.padding = "10px";
swapExplanationBox.style.margin = "10px 0";
swapExplanationBox.style.fontSize = "13px";
swapExplanationBox.style.lineHeight = "1.4";
swapExplanationBox.innerHTML =
"<strong>🔄 ¿Qué hace esta lista?</strong><br>" +
"Las palabras aquí se moverán al inicio o al final del nombre.<br>" +
"<em>Ej:</em> \"Las Palmas <b>Urbanización</b>\" → \"<b>Urbanización</b> Las Palmas\" (si se configura 'Al Inicio').";
swapContainer.appendChild(swapExplanationBox);
// =======================================================
// INICIO DE LA MODIFICACIÓN DEL LAYOUT
// =======================================================
// Contenedor principal para los controles, ahora apilado verticalmente
const swapInputContainer = document.createElement("div");
swapInputContainer.style.display = "flex";
swapInputContainer.style.flexDirection = "column"; // Apilado vertical
swapInputContainer.style.gap = "8px";
swapInputContainer.style.marginBottom = "8px";
// Fila 1: Input de la palabra
const swapInputDiv = document.createElement("div");
const swapInputLabel = document.createElement("label");
swapInputLabel.textContent = "Palabra a agregar:";
swapInputLabel.style.fontSize = "12px";
swapInputLabel.style.display = "block";
swapInputLabel.style.marginBottom = "2px";
const swapInput = document.createElement("input");
swapInput.type = "text";
swapInput.placeholder = "Ej: Urbanización";
swapInput.style.width = "calc(100% - 12px)"; // Ancho completo
swapInput.style.padding = "6px";
swapInput.setAttribute('spellcheck', 'false');
swapInputDiv.appendChild(swapInputLabel);
swapInputDiv.appendChild(swapInput);
// Fila 2: Controles de dirección y botón de añadir
const controlsRow = document.createElement("div");
controlsRow.style.display = "flex";
controlsRow.style.alignItems = "center";
controlsRow.style.gap = "10px";
// Contenedor para los radio buttons
const directionContainer = document.createElement("div");
directionContainer.style.display = "flex";
directionContainer.style.gap = "15px";
directionContainer.style.padding = "5px 10px";
directionContainer.style.border = "1px solid #ccc";
directionContainer.style.borderRadius = "4px";
['start', 'end'].forEach(dir => {
const optionContainer = document.createElement('div');
optionContainer.style.display = 'flex';
optionContainer.style.alignItems = 'center';
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "swapDirection";
radio.value = dir;
radio.id = `swap_${dir}`;
if (dir === 'start') radio.checked = true;
radio.style.marginRight = "4px";
const label = document.createElement("label");
label.htmlFor = `swap_${dir}`;
label.textContent = dir === 'start' ? 'Mover a Inicio' : 'Mover al Final'; // ETIQUETAS ACTUALIZADAS
label.style.fontSize = "13px";
label.style.cursor = "pointer";
optionContainer.appendChild(radio);
optionContainer.appendChild(label);
directionContainer.appendChild(optionContainer);
});
// Botón para añadir
const swapBtn = document.createElement("button");
swapBtn.textContent = "Añadir";
swapBtn.style.padding = "6px 12px";
swapBtn.style.height = "32px";
// Ensamblar la fila 2
controlsRow.appendChild(directionContainer);
controlsRow.appendChild(swapBtn);
// Ensamblar el contenedor principal
swapInputContainer.appendChild(swapInputDiv);
swapInputContainer.appendChild(controlsRow);
swapContainer.appendChild(swapInputContainer); // Añadir el contenedor principal al panel
// =======================================================
// FIN DE LA MODIFICACIÓN DEL LAYOUT
// =======================================================
// === EVENT LISTENER PARA EL BOTÓN AÑADIR (con protección de hoja) ===
swapBtn.addEventListener("click", () => {
const val = swapInput.value.trim();
const direction = document.querySelector('input[name="swapDirection"]:checked').value;
if (!val || /^[^a-zA-Z0-9]+$/.test(val)) {
alert("No se permiten caracteres especiales solos o palabras vacías.");
return;
}
// Bloquear si la palabra está protegida por la hoja Swap
try {
const norm = (val||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
if (window.fixedSwapNormSet && window.fixedSwapNormSet.has(norm)) {
alert('Esta palabra está protegida por la lista de la Comundiad de Editores (Swap) y no puede agregarse.');
return;
}
} catch(_) {}
if (window.swapWords.some(item => item.word === val)) {
alert("Esa palabra ya existe en la lista.");
return;
}
window.swapWords.push({ word: val, direction: direction });
saveSwapWordsToStorage();
swapInput.value = "";
renderSwapList();
});
// === CAMPO DE BÚSQUEDA ===
const searchSwapInput = document.createElement("input");
searchSwapInput.type = "text";
searchSwapInput.placeholder = "Buscar palabra...";
searchSwapInput.id = "searchSwapInput";
searchSwapInput.style.width = "calc(100% - 12px)";
searchSwapInput.style.padding = "6px";
searchSwapInput.style.marginBottom = "8px";
searchSwapInput.style.border = "1px solid #ccc";
searchSwapInput.addEventListener("input", () => renderSwapList());
swapContainer.appendChild(searchSwapInput);
parentContainer.appendChild(swapContainer);
// === LÓGICA DE RENDERIZADO DE LA LISTA (ACTUALIZADA) ===
function renderSwapList() {
const searchInput = document.getElementById("searchSwapInput");
const swapList = swapContainer.querySelector("ul") || (() => {
const ul = document.createElement("ul");
ul.id = "swapList";
ul.style.maxHeight = "120px";
ul.style.overflowY = "auto";
ul.style.border = "1px solid #ddd";
ul.style.padding = "8px";
ul.style.margin = "0";
ul.style.background = "#fff";
ul.style.listStyle = "none";
swapContainer.appendChild(ul);
return ul;
})();
swapList.innerHTML = "";
if (!window.swapWords || window.swapWords.length === 0) {
const li = document.createElement("li");
li.textContent = "No hay palabras de intercambio definidas.";
li.style.textAlign = "center";
li.style.color = "#777";
swapList.appendChild(li);
return;
}
const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : "";
const filteredSwapWords = window.swapWords.filter(item => item.word.toLowerCase().includes(searchTerm));
filteredSwapWords.forEach((item, index) => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
const wordSpan = document.createElement("span");
const directionIcon = item.direction === "start" ? "←" : "→";
const directionText = item.direction === "start" ? "Al Inicio" : "Al Final";
let isLocked = false;
try {
const norm = (item.word||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
isLocked = !!(window.fixedSwapNormSet && window.fixedSwapNormSet.has(norm));
} catch(_) { isLocked = false; }
wordSpan.innerHTML = `<b>${item.word}</b> <small style="color: #666;">(${directionIcon} ${directionText})</small>${isLocked ? ' 🔒' : ''}`;
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "4px";
// Botón Editar
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.addEventListener("click", () => {
const originalIndex = window.swapWords.findIndex(sw => sw.word === item.word);
if (originalIndex > -1) {
openSwapWordEditor(window.swapWords[originalIndex], originalIndex);
}
});
if (isLocked) { editBtn.disabled = true; editBtn.style.opacity = '0.4'; editBtn.style.cursor = 'not-allowed'; }
// Botón Eliminar
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Eliminar la palabra swap '${item.word}'?`)) {
const indexToDelete = window.swapWords.findIndex(sw => sw.word === item.word);
if (indexToDelete > -1) {
window.swapWords.splice(indexToDelete, 1);
saveSwapWordsToStorage();
renderSwapList();
}
}
});
if (isLocked) { deleteBtn.disabled = true; deleteBtn.style.opacity = '0.4'; deleteBtn.style.cursor = 'not-allowed'; }
btnContainer.appendChild(editBtn);
btnContainer.appendChild(deleteBtn);
li.appendChild(wordSpan);
li.appendChild(btnContainer);
swapList.appendChild(li);
});
}
// Render inicial y listener del selector
plnCanonicalizeReplacementsBySheet();
renderReplacementsList(listElement);
renderSwapList();
modeSelector.addEventListener("change", () => {
replacementsContainer.style.display = modeSelector.value === "words" ? "block" : "none";
swapContainer.style.display = modeSelector.value === "swapStart" ? "block" : "none";
});
}
// Crea el gestor de reemplazos
// Renderiza la lista de palabras excluidas
function renderExcludedWordsList(ulElement, filter = "")
{
if (!ulElement)
{
return;
}
// Asegurarse de que ulElement es válido
const currentFilter = filter.toLowerCase();
ulElement.innerHTML = "";
// Asegurarse de que excludedWords es un Set
const wordsToRender = Array.from(excludedWords).filter(word => word.toLowerCase().includes(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Si no hay palabras para renderizar, mostrar mensaje
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay palabras excluidas.";
li.style.textAlign = "center";
li.style.color = "#777";
ulElement.appendChild(li);
return;
}
// Renderizar cada palabra
wordsToRender.forEach(word =>
{
const li = document.createElement("li");
li.style.display = "flex"; // Agregado para alinear texto y botones
li.style.justifyContent = "space-between"; // Agregado para espacio entre texto y botones
li.style.alignItems = "center"; // Agregado para centrado vertical
li.style.padding = "5px";
li.style.borderBottom = "1px solid #ddd";
// Span para el texto de la palabra
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.flexGrow = "1"; // Permite que el texto ocupe el espacio disponible
wordSpan.style.marginRight = "10px"; // Espacio entre el texto y los botones
li.appendChild(wordSpan);
//Bloque para los botones de edición y eliminación ---
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "8px"; // Espacio entre los botones
// Botón de edición
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️"; // Icono de lápiz
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word) {
const validation = isValidExcludedWord(newWord.trim());
if (!validation.valid) {
alert(validation.msg);
return;
}
// Eliminar la palabra antigua del Set y Map
excludedWords.delete(word);
const oldFirstChar = word.charAt(0).toLowerCase();
if (excludedWordsMap.has(oldFirstChar)) {
excludedWordsMap.get(oldFirstChar).delete(word);
if (excludedWordsMap.get(oldFirstChar).size === 0) {
excludedWordsMap.delete(oldFirstChar);
}
}
// Añadir la nueva palabra al Set y Map
const trimmedNewWord = newWord.trim();
excludedWords.add(trimmedNewWord);
const newFirstChar = trimmedNewWord.charAt(0).toLowerCase();
if (!excludedWordsMap.has(newFirstChar)) {
excludedWordsMap.set(newFirstChar, new Set());
}
excludedWordsMap.get(newFirstChar).add(trimmedNewWord);
renderExcludedWordsList(ulElement, currentFilter);
saveExcludedWordsToLocalStorage();
}
});
btnContainer.appendChild(editBtn);
// Botón de eliminación
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️"; // Icono de bote de basura
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Eliminar la palabra '${word}' de la lista de especiales?`)) {
excludedWords.delete(word);
const firstChar = word.charAt(0).toLowerCase();
if (excludedWordsMap.has(firstChar)) {
excludedWordsMap.get(firstChar).delete(word);
if (excludedWordsMap.get(firstChar).size === 0) {
excludedWordsMap.delete(firstChar);
}
}
renderExcludedWordsList(ulElement, currentFilter);
saveExcludedWordsToLocalStorage();
}
});
btnContainer.appendChild(deleteBtn);
li.appendChild(btnContainer);
// --- FIN DEL BLOQUE PARA LOS BOTONES ---
ulElement.appendChild(li);
});//
}// renderExcludedWordsList
// Función para renderizar la lista de lugares excluidos
function renderExcludedPlacesList(ulElement, filter = "")
{
if (!ulElement) return;
ulElement.innerHTML = "";
const lowerFilter = filter.toLowerCase();
// Ahora excludedPlaces es un Map<ID, Name>.
const placesToRender = Array.from(excludedPlaces.entries()).filter(([placeId, placeName]) =>
// Filtra por ID o por el nombre guardado
placeId.toLowerCase().includes(lowerFilter) ||
(placeName && placeName.toLowerCase().includes(lowerFilter))
).sort(([idA, nameA], [idB, nameB]) => {
// Ordena alfabéticamente por el nombre guardado
const safeNameA = nameA || '';
const safeNameB = nameB || '';
return safeNameA.toLowerCase().localeCompare(safeNameB.toLowerCase());
});
if (placesToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay lugares excluidos.";
li.style.textAlign = "center";
li.style.color = "#777";
li.style.padding = "5px";
ulElement.appendChild(li);
return;
}
placesToRender.forEach(([placeId, placeNameSaved]) =>
{ // Ahora recibimos [ID, NombreGuardado]
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
// Muestra el nombre guardado, con un fallback si el nombre guardado está vacío.
const displayName = placeNameSaved || `ID: ${placeId}`;
const linkSpan = document.createElement("span");
linkSpan.style.flexGrow = "1";
linkSpan.style.marginRight = "10px";
const link = document.createElement("a");
link.href = "#";
link.textContent = displayName; // Muestra el nombre guardado
link.title = `Abrir lugar en WME (ID: ${placeId})`; // El tooltip sigue mostrando el ID
link.addEventListener("click", (e) =>
{
e.preventDefault();
// Intenta obtener el lugar del modelo para seleccionarlo y centrarlo
// Usamos W.model como fallback si wmeSDK.DataModel.Venues.getById no es eficiente aquí o no está diseñado para esta interacción
const venueObj = W.model.venues.getObjectById(placeId); // <---
const venueSDKForUse = venueSDKForRender; // Objeto del SDK que pasamos desde processNextPlace
if (venueObj)
{
if (W.map && typeof W.map.setCenter === 'function' && venueObj.getOLGeometry && venueObj.getOLGeometry().getCentroid)
{
W.map.setCenter(venueObj.getOLGeometry().getCentroid(), null, false, 0); // <--- REINTRODUCIMOS W.map.setCenter
}
if (W.selectionManager && typeof W.selectionManager.select === 'function') {
W.selectionManager.select(venueObj); // <--- REINTRODUCIMOS W.selectionManager.select
} else if (W.selectionManager && typeof W.selectionManager.setSelectedModels === 'function') {
W.selectionManager.setSelectedModels([venueObj]); // Fallback para versiones antiguas
}
}
else
{
// Si el lugar no está en el modelo (fuera de vista), avisa y ofrece abrir en nueva pestaña.
const confirmOpen = confirm(`Lugar '${displayName}' (ID: ${placeId}) no encontrado en el modelo actual. ¿Deseas abrirlo en una nueva pestaña del editor?`);
if (confirmOpen)
{
const wmeUrl = `https://www.waze.com/editor?env=row&venueId=${placeId}`;
window.open(wmeUrl, '_blank');
}
}
});
linkSpan.appendChild(link);
li.appendChild(linkSpan);
// Botón para eliminar el lugar de la lista de excluidos.
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar lugar de la lista de excluidos";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
// ************************************************************
// INICIO DE LA MODIFICACIÓN: Modal de confirmación "bonito"
// ************************************************************
const confirmModal = document.createElement("div");
confirmModal.style.position = "fixed";
confirmModal.style.top = "50%";
confirmModal.style.left = "50%";
confirmModal.style.transform = "translate(-50%, -50%)";
confirmModal.style.background = "#fff";
confirmModal.style.border = "1px solid #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000"; // Z-INDEX ALTO
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
// Ícono visual
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️"; // Ícono de advertencia
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
confirmModal.appendChild(iconElement);
// Mensaje principal
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>¿Eliminar de excluidos "${placeNameSaved}"?</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
confirmModal.appendChild(messageTitle);
// Mensaje explicativo
const explanationDiv = document.createElement("div");
explanationDiv.textContent = `Este lugar volverá a aparecer en futuras búsquedas del normalizador.`;
explanationDiv.style.fontSize = "15px";
explanationDiv.style.color = "#555";
explanationDiv.style.marginBottom = "18px";
confirmModal.appendChild(explanationDiv);
// Botones de confirmación
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
// Botón Cancelar
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "7px 18px";
cancelBtn.style.background = "#eee";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => confirmModal.remove());
// Botón Confirmar Eliminación
const confirmDeleteBtn = document.createElement("button");
confirmDeleteBtn.textContent = "Eliminar";
confirmDeleteBtn.style.padding = "7px 18px";
confirmDeleteBtn.style.background = "#d9534f"; // Rojo
confirmDeleteBtn.style.color = "#fff";
confirmDeleteBtn.style.border = "none";
confirmDeleteBtn.style.borderRadius = "4px";
confirmDeleteBtn.style.cursor = "pointer";
confirmDeleteBtn.style.fontWeight = "bold";
confirmDeleteBtn.addEventListener("click", () => {
// Aquí va la lógica que antes estaba directamente en el if(confirm)
excludedPlaces.delete(placeId); // Sigue eliminando por ID
renderExcludedPlacesList(ulElement, filter); // Vuelve a renderizar la lista después de eliminar.
saveExcludedPlacesToLocalStorage(); // Guarda los cambios en localStorage.
showTemporaryMessage("Lugar eliminado de la lista de excluidos.", 3000, 'success');
confirmModal.remove(); // Cerrar el modal después de la acción
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmDeleteBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal); // Añadir el modal al body
// ************************************************************
// FIN DE LA MODIFICACIÓN
// ************************************************************
});
li.appendChild(deleteBtn);
ulElement.appendChild(li);
});
}// renderExcludedPlacesList
// Crea un dropdown para seleccionar categorías recomendadas
function createRecommendedCategoryDropdown(placeId, currentCategoryKey, dynamicCategorySuggestions)
{
const wrapperDiv = document.createElement("div");
wrapperDiv.style.position = "relative";
wrapperDiv.style.width = "100%";
wrapperDiv.style.minWidth = "150px";
wrapperDiv.style.display = "flex";
wrapperDiv.style.flexDirection = "column";
// Parte de sugerencias dinámicas existentes
const suggestionsWrapper = document.createElement("div"); // Contenedor para sugerencias
suggestionsWrapper.style.display = "flex";
suggestionsWrapper.style.flexDirection = "column";
suggestionsWrapper.style.alignItems = "flex-start";
suggestionsWrapper.style.gap = "4px";
// Filtrar y ordenar las sugerencias dinámicas para la presentación
const filteredSuggestions = dynamicCategorySuggestions.filter(suggestion => suggestion.categoryKey.toUpperCase() !== currentCategoryKey.toUpperCase());
if (filteredSuggestions.length > 0)
{ // Solo si hay sugerencias diferentes a la actual
filteredSuggestions.forEach(suggestion => {
const suggestionEntry = document.createElement("div");
suggestionEntry.style.display = "flex";
suggestionEntry.style.alignItems = "center";
suggestionEntry.style.gap = "4px";
suggestionEntry.style.padding = "2px 4px";
suggestionEntry.style.border = "1px solid #dcdcdc";
suggestionEntry.style.borderRadius = "3px";
suggestionEntry.style.backgroundColor = "#eaf7ff"; // Un color distinto para sugerencias
suggestionEntry.style.cursor = "pointer";
suggestionEntry.title = `Sugerencia: ${getCategoryDetails(suggestion.categoryKey).description}`;
//Añadir icono y descripción de la categoría
const suggestedIconSpan = document.createElement("span");// Icono de la sugerencia
suggestedIconSpan.textContent = suggestion.icon;
suggestedIconSpan.style.fontSize = "16px";
suggestionEntry.appendChild(suggestedIconSpan);
// Añadir descripción de la categoría
const suggestedDescSpan = document.createElement("span");
suggestedDescSpan.textContent = getCategoryDetails(suggestion.categoryKey).description;
suggestionEntry.appendChild(suggestedDescSpan);
suggestionEntry.addEventListener("click", async function handler() { // Cambiado a función con nombre 'handler'
const placeToUpdate = W.model.venues.getObjectById(placeId);
if (!placeToUpdate)
{
console.error("[WME_PLN] Lugar no encontrado para actualizar categoría.");
return;
}
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(placeToUpdate, { categories: [suggestion.categoryKey] });
W.model.actionManager.add(action);
// Obtener la celda de la categoría original y aplicar un estilo de opacidad
const row = document.querySelector(`tr[data-place-id="${placeId}"]`); // Obtener la fila
row.dataset.categoryChanged = 'true'; // Marcar fila como modificada
// Habilitar el botón de aplicar sugerencia
const applyButton = row.querySelector('button[title="Aplicar sugerencia"]');
if (applyButton)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
}
//Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, suggestion.categoryKey);
// Asegurarse de que la fila existe antes de intentar acceder a sus celdas
if (row)
{
const originalCategoryCell = row.querySelector('td:nth-child(10)'); // La décima columna es "Categoría"
if (originalCategoryCell)
{
originalCategoryCell.style.opacity = '0.5'; // Atenuar la celda completa
originalCategoryCell.title += ' (Modificada)'; // Opcional, añadir un tooltip
}
}
// : Mostrar chulito verde en la sugerencia misma
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.marginLeft = "5px";
suggestionEntry.appendChild(successIcon); // Añadir el chulito a la entrada de la sugerencia
suggestionEntry.style.cursor = "default"; // Deshabilitar clic posterior
suggestionEntry.removeEventListener("click", handler); // Deshabilita el listener una vez que se ha hecho clic
suggestionEntry.style.opacity = "0.7"; // Opcional: Atenúa la sugerencia para indicar que ya se usó
optionsListDiv.style.display = "none"; // Ocultar lista
searchInput.blur(); // Quitar el foco
// : Eliminar la selección temporal para la categoría, ya se guardó
tempSelectedCategories.delete(placeId); // Si esta categoría se guardó directamente
}
catch (e)
{
//console.error("[WME_PLN] Error al actualizar la categoría desde sugerencia:", e);
alert("Error al actualizar la categoría: " + e.message); // Mantener alerta para errores
}
});
suggestionsWrapper.appendChild(suggestionEntry);
});
wrapperDiv.appendChild(suggestionsWrapper); // Añadir contenedor de sugerencias
}// createRecommendedCategoryDropdown
//Fin de parte de sugerencias dinámicas
// Input para buscar
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Buscar o Seleccionar Categoría";// Placeholder más descriptivo
searchInput.style.width = "calc(100% - 10px)";
searchInput.style.padding = "5px";
searchInput.style.marginTop = "5px"; // Espacio después de sugerencias
searchInput.style.marginBottom = "5px";
searchInput.style.border = "1px solid #ccc";
searchInput.style.borderRadius = "3px";
searchInput.setAttribute('spellcheck', 'false');// Evitar corrección ortográfica
searchInput.readOnly = false;// Permitir escribir pero no editar directamente
searchInput.style.cursor = 'auto';// Permitir escribir pero no editar directamente
searchInput.style.opacity = '1.0'; // Opacidad normal para el input
wrapperDiv.appendChild(searchInput); // Añadir el input al wrapper
// Div que actuará como la lista desplegable de opciones
const optionsListDiv = document.createElement("div");
optionsListDiv.style.position = "absolute";
// Ajuste de top para que aparezca debajo del input, incluso con sugerencias
optionsListDiv.style.top = "calc(100% + 5px)"; // Se ajusta dinámicamente o se puede hacer con position: relative dentro de un contenedor fijo.
optionsListDiv.style.left = "0";
optionsListDiv.style.width = "calc(100% - 2px)";
optionsListDiv.style.maxHeight = "200px";
optionsListDiv.style.overflowY = "auto";
optionsListDiv.style.border = "1px solid #ddd";
optionsListDiv.style.backgroundColor = "#fff";
optionsListDiv.style.zIndex = "1001";
optionsListDiv.style.display = "none";
optionsListDiv.style.borderRadius = "3px";
optionsListDiv.style.boxShadow = "0 2px 5px rgba(0,0,0,0.2)";
wrapperDiv.appendChild(optionsListDiv);
// --- Populate options list ---
function populateOptions(filterText = "")
{
optionsListDiv.innerHTML = ""; // Clear existing options
const lowerFilterText = filterText.toLowerCase(); // Normalize filter text for case-insensitive search
// Sort rules alphabetically by their Spanish description for display
const sortedRules = [...window.dynamicCategoryRules].sort((a, b) => {
const descA = (getWazeLanguage() === 'es' && a.desc_es) ? a.desc_es : a.desc_en;
const descB = (getWazeLanguage() === 'es' && b.desc_es) ? b.desc_es : b.desc_en;
return descA.localeCompare(descB);
});
sortedRules.forEach(rule => {// Iterate through each rule
const displayDesc = (getWazeLanguage() === 'es' && rule.desc_es) ? rule.desc_es : rule.desc_en;
if (filterText === "" || displayDesc.toLowerCase().includes(lowerFilterText) ||
rule.categoryKey.toLowerCase().includes(lowerFilterText))
{// Check if displayDesc or categoryKey contains the filter text
const optionDiv = document.createElement("div");
optionDiv.style.padding = "5px";
optionDiv.style.cursor = "pointer";
optionDiv.style.borderBottom = "1px solid #eee";
optionDiv.style.display = "flex";
optionDiv.style.alignItems = "center";
optionDiv.style.gap = "5px";
optionDiv.title = `Seleccionar: ${displayDesc} (${rule.categoryKey})`;
// Resaltar si es la categoría actual o la temporalmente seleccionada
const tempSelectedKey = tempSelectedCategories.get(placeId); // Obtener selección temporal
if (rule.categoryKey.toUpperCase() === currentCategoryKey.toUpperCase())
{// Resaltar la categoría actual
optionDiv.style.backgroundColor = "#e0f7fa"; // Azul claro para la actual
optionDiv.style.fontWeight = "bold";
}
else if (tempSelectedKey && rule.categoryKey.toUpperCase() === tempSelectedKey.toUpperCase()) // Resaltar selección temporal
optionDiv.style.backgroundColor = "#fffacd"; // Amarillo claro para la seleccionada temporalmente
else if (dynamicCategorySuggestions.some(s => s.categoryKey.toUpperCase() === rule.categoryKey.toUpperCase()))
optionDiv.style.backgroundColor = "#e6ffe6"; // Verde claro para sugerida por el sistema
const iconSpan = document.createElement("span");// Icono de la categoría
iconSpan.textContent = rule.icon;
iconSpan.style.fontSize = "16px";
optionDiv.appendChild(iconSpan);
const textSpan = document.createElement("span");// Descripción de la categoría
textSpan.textContent = displayDesc;
optionDiv.appendChild(textSpan);// Añadir descripción de la categoría
optionDiv.addEventListener("mouseenter", () => optionDiv.style.backgroundColor = "#f0f0f0");
optionDiv.addEventListener("mouseleave", () => {
if (tempSelectedKey && rule.categoryKey.toUpperCase() === tempSelectedKey.toUpperCase())
{
optionDiv.style.backgroundColor = "#fffacd";
}
else if (rule.categoryKey.toUpperCase() === currentCategoryKey.toUpperCase())
{
optionDiv.style.backgroundColor = "#e0f7fa";
}
else if (dynamicCategorySuggestions.some(s => s.categoryKey.toUpperCase() === rule.categoryKey.toUpperCase()))
{
optionDiv.style.backgroundColor = "#e6ffe6";
}
else
{
optionDiv.style.backgroundColor = "#fff";
}
});
// Añadir evento click para seleccionar la categoría
optionDiv.addEventListener("click", async () => {
const placeToUpdate = W.model.venues.getObjectById(placeId);
if (!placeToUpdate)
{
//console.error("[WME_PLN] Lugar no encontrado para actualizar categoría.");
return;
}
try {
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(placeToUpdate, { categories: [rule.categoryKey] });
W.model.actionManager.add(action);
// ✅ CORRECCIÓN: Se declara 'row' aquí, ANTES de su primer uso.
const row = document.querySelector(`tr[data-place-id="${placeId}"]`);
// Ahora es seguro usar la variable 'row'.
if (row)
{
row.dataset.categoryChanged = 'true'; // Marcar fila como modificada
const applyButton = row.querySelector('button[title="Aplicar sugerencia"]');
// Habilitar el botón de aplicar sugerencia
if (applyButton)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
}
}
// Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, rule.categoryKey);
// Atenuar la celda de la categoría original
if (row) {
const categoryCell = row.querySelector('td:nth-child(10)');
if (categoryCell) {
const currentCategoryDiv = categoryCell.querySelector('div');
if (currentCategoryDiv) {
currentCategoryDiv.style.opacity = '0.5';
currentCategoryDiv.title += ' (Modificada)';
}
}
}
// Actualizar el valor del input con icono y descripción de la selección
searchInput.value = `${rule.icon} ${displayDesc}`;
searchInput.style.setProperty('opacity', '1.0', 'important'); // Usar setProperty para asegurar visibilidad
// Ocultar la lista de opciones
optionsListDiv.style.display = "none";
searchInput.blur();
} catch (e) {
console.error("[WME_PLN] Error al actualizar la categoría desde dropdown:", e);
alert("Error al actualizar la categoría: " + e.message);
}
});
optionsListDiv.appendChild(optionDiv);
}
});
if (optionsListDiv.childElementCount === 0)
{// Si no hay opciones que coincidan con el filtro, mostrar mensaje
const noResults = document.createElement("div");
noResults.style.padding = "5px";
noResults.style.color = "#777";
noResults.textContent = "No hay resultados.";
optionsListDiv.appendChild(noResults);
}
}// populateOptions
// Limpiamos los listeners anteriores y los reescribimos de forma más robusta.
let debounceTimer;
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
// Muestra la lista y filtra mientras el usuario escribe.
debounceTimer = setTimeout(() => {
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
}, 200);
});
searchInput.addEventListener("focus", () => {
// Al hacer foco, muestra la lista completa.
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
});
// Usamos 'mousedown' en lugar de 'click' para cerrar el menú.
// Esto evita conflictos con el evento 'click' de las opciones.
document.addEventListener("mousedown", (e) =>
{
if (!wrapperDiv.contains(e.target))
{
optionsListDiv.style.display = "none";
}
});
populateOptions(""); // Cargar las opciones inicialmente (sin filtro)
return wrapperDiv;
}// createRecommendedCategoryDropdown
// Función auxiliar para actualizar el display de la categoría actual en la tabla
function updateCategoryDisplayInTable(placeId, newCategoryKey)
{
const row = document.querySelector(`tr[data-place-id="${placeId}"]`); // Asume que cada fila tiene un data-place-id
if (!row) return;
const categoryCell = row.querySelector('td:nth-child(8)'); // La décima columna es "Categoría"
if (!categoryCell) return;// Asegurarse de que la celda existe
const categoryDetails = getCategoryDetails(newCategoryKey); // Obtener detalles de la categoría
const currentCategoryDiv = categoryCell.querySelector('div'); // Contenedor del texto y el ícono
if (currentCategoryDiv)
{// Actualizar el contenido del div existente
currentCategoryDiv.querySelector('span:first-child').textContent = categoryDetails.description; // Actualiza el texto
currentCategoryDiv.querySelector('span:last-child').textContent = categoryDetails.icon; // Actualiza el ícono
currentCategoryDiv.querySelector('span:first-child').title = `Categoría Actual: ${categoryDetails.description}`; // Actualiza el título
}
}
// Renderizar lista de palabras del diccionario
function renderDictionaryList(ulElement, filter = "")
{
updateDictionaryWordsCountLabel();
// Asegurarse de que ulElement es válido
if (!ulElement || !window.dictionaryWords)
return;
// Asegurarse de que ulElement es válido
const currentFilter = filter.toLowerCase();
ulElement.innerHTML = "";
// Asegurarse de que dictionaryWords es un Set
const wordsToRender =
Array.from(window.dictionaryWords)
.filter(word => word.toLowerCase().startsWith(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Si no hay palabras que renderizar, mostrar mensaje
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = window.dictionaryWords.size === 0
? "El diccionario está vacío."
: "No hay coincidencias.";
li.style.textAlign = "center";
li.style.color = "#777";
ulElement.appendChild(li);
// Guardar diccionario también cuando está vacío
try
{
localStorage.setItem(
"dictionaryWordsList",
JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error( "[WME PLN] Error guardando el diccionario en localStorage:", e);
}
return;
}
// Renderizar cada palabra
wordsToRender.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
// Span para la palabra
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.maxWidth = "calc(100% - 60px)";
wordSpan.style.overflow = "hidden";
wordSpan.style.textOverflow = "ellipsis";
wordSpan.style.whiteSpace = "nowrap";
wordSpan.title = word;
li.appendChild(wordSpan);
// Contenedor para los iconos de acción
const iconContainer = document.createElement("span");
iconContainer.style.display = "flex";
iconContainer.style.gap = "8px";
// Botón de edición y eliminación
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word)
{
window.dictionaryWords.delete(word);
window.dictionaryWords.add(newWord.trim());
renderDictionaryList(ulElement, currentFilter);
}
});
// Botón de eliminación
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
// Confirmación antes de eliminar
if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`))
{
window.dictionaryWords.delete(word);
renderDictionaryList(ulElement, currentFilter);
}
});
iconContainer.appendChild(editBtn);
iconContainer.appendChild(deleteBtn);
li.appendChild(iconContainer);
ulElement.appendChild(li);
});
// Guardar el diccionario actualizado en localStorage después de cada render
try
{
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando el diccionario en localStorage:", e);
}
}// renderDictionaryList
// Función para obtener el ícono de categoría (acepta nombre o clave)
function getCategoryIcon(categoryName)
{
// Mapa de categorías a íconos con soporte bilingüe
const categoryIcons = {
// Comida y Restaurantes / Food & Restaurants
"FOOD_AND_DRINK": { icon: "🦞🍷", es: "Comida y Bebidas", en: "Food and Drinks" },
"RESTAURANT": { icon: "🍽️", es: "Restaurante", en: "Restaurant" },
"FAST_FOOD": { icon: "🍔", es: "Comida rápida", en: "Fast Food" },
"CAFE": { icon: "☕", es: "Cafetería", en: "Cafe" },
"BAR": { icon: "🍺", es: "Bar", en: "Bar" },
"BAKERY": { icon: "🥖", es: "Panadería", en: "Bakery" },
"ICE_CREAM": { icon: "🍦", es: "Heladería", en: "Ice Cream Shop" },
"DEPARTMENT_STORE": { icon: "🏬", es: "Tienda por departamentos", en: "Department Store" },
"PARK": { icon: "🌳", es: "Parque", en: "Park" },
// Compras y Servicios / Shopping & Services
"FASHION_AND_CLOTHING": { icon: "👗", es: "Moda y Ropa", en: "Fashion and Clothing" },
"SHOPPING_AND_SERVICES": { icon: "👜👝", es: "Mercado o Tienda", en: "Shopping and Services" },
"SHOPPING_CENTER": { icon: "🛍️", es: "Centro comercial", en: "Shopping Center" },
"SUPERMARKET_GROCERY": { icon: "🛒", es: "Supermercado", en: "Supermarket" },
"MARKET": { icon: "🛒", es: "Mercado", en: "Market" },
"CONVENIENCE_STORE": { icon: "🏪", es: "Tienda", en: "Convenience Store" },
"PHARMACY": { icon: "💊", es: "Farmacia", en: "Pharmacy" },
"BANK": { icon: "🏦", es: "Banco", en: "Bank" },
"ATM": { icon: "💳", es: "Cajero automático", en: "ATM" },
"HARDWARE_STORE": { icon: "🔧", es: "Ferretería", en: "Hardware Store" },
"COURTHOUSE": { icon: "⚖️", es: "Corte", en: "Courthouse" },
"FURNITURE_HOME_STORE": { icon: "🛋️", es: "Tienda de muebles", en: "Furniture Store" },
"TOURIST_ATTRACTION_HISTORIC_SITE": { icon: "🗿", es: "Atracción turística o Sitio histórico", en: "Tourist Attraction or Historic Site" },
"PET_STORE_VETERINARIAN_SERVICES": { icon: "🦮🐈", es: "Tienda de mascotas o Veterinaria", en: "Pet Store or Veterinary Services" },
"CEMETERY": { icon: "🪦", es: "Cementerio", en: "Cemetery" },
"KINDERGARDEN": { icon: "🍼", es: "Jardín Infantil", en: "Kindergarten" },
"JUNCTION_INTERCHANGE": { icon: "🔀", es: "Cruce o Intercambio", en: "Junction or Interchange" },
"OUTDOORS": { icon: "🏞️", es: "Aire libre", en: "Outdoors" },
"ORGANIZATION_OR_ASSOCIATION": { icon: "👔", es: "Organización o Asociación", en: "Organization or Association" },
"TRAVEL_AGENCY": { icon: "🧳", es: "Agencia de viajes", en: "Travel Agency" },
"BANK_FINANCIAL": { icon: "💰", es: "Banco o Financiera", en: "Bank or Financial Institution" },
"SPORTING_GOODS": { icon: "🛼🏀🏐", es: "Artículos deportivos", en: "Sporting Goods" },
"TOY_STORE": { icon: "🧸", es: "Tienda de juguetes", en: "Toy Store" },
"CURRENCY_EXCHANGE": { icon: "💶💱", es: "Casa de cambio", en: "Currency Exchange" },
"PHOTOGRAPHY": { icon: "📸", es: "Fotografía", en: "Photography" },
"DESSERT": { icon: "🍰", es: "Postre", en: "Dessert" },
"FOOD_COURT": { icon: "🥗", es: "Comedor o Patio de comidas", en: "Food Court" },
"CANAL": { icon: "〰", es: "Canal", en: "Canal" },
"JEWELRY": { icon: "💍", es: "Joyería", en: "Jewelry" },
// Transporte / Transportation
"TRAIN_STATION": { icon: "🚂", es: "Estación de tren", en: "Train Station" },
"GAS_STATION": { icon: "⛽", es: "Estación de servicio", en: "Gas Station" },
"PARKING_LOT": { icon: "🅿️", es: "Estacionamiento", en: "Parking Lot" },
"BUS_STATION": { icon: "🚍", es: "Terminal de bus", en: "Bus Station" },
"AIRPORT": { icon: "✈️", es: "Aeropuerto", en: "Airport" },
"CAR_WASH": { icon: "🚗💦", es: "Lavado de autos", en: "Car Wash" },
"CAR_RENTAL": { icon: "🚘🛺🛻🚙", es: "Alquiler de Vehículos", en: "Car Rental" },
"TAXI_STATION": { icon: "🚕", es: "Estación de taxis", en: "Taxi Station" },
"FOREST_GROVE": { icon: "🌳", es: "Bosque", en: "Forest Grove" },
"GARAGE_AUTOMOTIVE_SHOP": { icon: "🔧🚗", es: "Taller mecánico", en: "Automotive Garage" },
"GIFTS": { icon: "🎁", es: "Tienda de regalos", en: "Gift Shop" },
"TOLL_BOOTH": { icon: "🚧", es: "Peaje", en: "Toll Booth" },
"CHARGING_STATION": { icon: "🔋", es: "Estación de carga", en: "Charging Station" },
"CAR_SERVICES": { icon: "🚗🔧", es: "Servicios de automóviles", en: "Car Services" },
"STADIUM_ARENA": { icon: "🏟️", es: "Estadio o Arena", en: "Stadium or Arena" },
"CAR_DEALERSHIP": { icon: "🚘🏢", es: "Concesionario de autos", en: "Car Dealership" },
"FERRY_PIER": { icon: "⛴️", es: "Muelle de ferry", en: "Ferry Pier" },
"INFORMATION_POINT": { icon: "ℹ️", es: "Punto de información", en: "Information Point" },
"REST_AREAS": { icon: "🏜", es: "Áreas de descanso", en: "Rest Areas" },
"MUSIC_VENUE": { icon: "🎶", es: "Lugar de música", en: "Music Venue" },
"CASINO": { icon: "🎰", es: "Casino", en: "Casino" },
"CITY_HALL": { icon: "🎩", es: "Ayuntamiento", en: "City Hall" },
"PERFORMING_ARTS_VENUE": { icon: "🎭", es: "Lugar de artes escénicas", en: "Performing Arts Venue" },
"TUNNEL": { icon: "🔳", es: "Túnel", en: "Tunnel" },
"SEAPORT_MARINA_HARBOR": { icon: "⚓", es: "Puerto o Marina", en: "Seaport or Marina" },
// Alojamiento / Lodging
"HOTEL": { icon: "🏨", es: "Hotel", en: "Hotel" },
"HOSTEL": { icon: "🛏️", es: "Hostal", en: "Hostel" },
"LODGING": { icon: "⛺", es: "Alojamiento", en: "Lodging" },
"MOTEL": { icon: "🛕", es: "Motel", en: "Motel" },
"SWIMMING_POOL": { icon: "🏊", es: "Piscina", en: "Swimming Pool" },
"RIVER_STREAM": { icon: "🌊", es: "Río o Arroyo", en: "River or Stream" },
"CAMPING_TRAILER_PARK": { icon: "🏕️", es: "Camping o Parque de Trailers", en: "Camping or Trailer Park" },
"SEA_LAKE_POOL": { icon: "🏖️", es: "Mar, Lago o Piscina", en: "Sea, Lake or Pool" },
"FARM": { icon: "🚜", es: "Granja", en: "Farm" },
"NATURAL_FEATURES": { icon: "🌲", es: "Características naturales", en: "Natural Features" },
// Salud / Healthcare
"HOSPITAL": { icon: "🏥", es: "Hospital", en: "Hospital" },
"HOSPITAL_URGENT_CARE": { icon: "🏥🚑", es: "Urgencias", en: "Urgent Care" },
"DOCTOR_CLINIC": { icon: "🏥⚕️", es: "Clínica", en: "Clinic" },
"DOCTOR": { icon: "👨⚕️", es: "Consultorio médico", en: "Doctor's Office" },
"VETERINARY": { icon: "🐾", es: "Veterinaria", en: "Veterinary" },
"PERSONAL_CARE": { icon: "💅💇🦷", es: "Cuidado personal", en: "Personal Care" },
"FACTORY_INDUSTRIAL": { icon: "🏭", es: "Fábrica o Industrial", en: "Factory or Industrial" },
"MILITARY": { icon: "🪖", es: "Militar", en: "Military" },
"LAUNDRY_DRY_CLEAN": { icon: "🧺", es: "Lavandería o Tintorería", en: "Laundry or Dry Clean" },
"PLAYGROUND": { icon: "🛝", es: "Parque infantil", en: "Playground" },
"TRASH_AND_RECYCLING_FACILITIES": { icon: "🗑️♻️", es: "Instalaciones de basura y reciclaje", en: "Trash and Recycling Facilities" },
// Educación / Education
"UNIVERSITY": { icon: "🎓", es: "Universidad", en: "University" },
"COLLEGE_UNIVERSITY": { icon: "🏫", es: "Colegio", en: "College" },
"SCHOOL": { icon: "🎒", es: "Escuela", en: "School" },
"LIBRARY": { icon: "📖", es: "Biblioteca", en: "Library" },
"FLOWERS": { icon: "💐", es: "Floristería", en: "Flower Shop" },
"CONVENTIONS_EVENT_CENTER": { icon: "🎤🥂", es: "Centro de convenciones o eventos", en: "Convention or Event Center" },
"CLUB": { icon: "♣", es: "Club", en: "Club" },
"ART_GALLERY": { icon: "🖼️", es: "Galería de arte", en: "Art Gallery" },
"NATURAL_FEATURES": { icon: "🌄", es: "Características naturales", en: "Natural Features" },
// Entretenimiento / Entertainment
"CINEMA": { icon: "🎬", es: "Cine", en: "Cinema" },
"THEATER": { icon: "🎭", es: "Teatro", en: "Theater" },
"MUSEUM": { icon: "🖼", es: "Museo", en: "Museum" },
"CULTURE_AND_ENTERTAINEMENT": { icon: "🎨", es: "Cultura y Entretenimiento", en: "Culture and Entertainment" },
"STADIUM": { icon: "🏟️", es: "Estadio", en: "Stadium" },
"GYM": { icon: "💪", es: "Gimnasio", en: "Gym" },
"GYM_FITNESS": { icon: "🏋️", es: "Gimnasio o Fitness", en: "Gym or Fitness" },
"GAME_CLUB": { icon: "⚽🏓", es: "Club de juegos", en: "Game Club" },
"BOOKSTORE": { icon: "📖📚", es: "Librería", en: "Bookstore" },
"ELECTRONICS": { icon: "📱💻", es: "Electrónica", en: "Electronics" },
"SPORTS_COURT": { icon: "⚽🏀", es: "Cancha deportiva", en: "Sports Court" },
"GOLF_COURSE": { icon: "⛳", es: "Campo de golf", en: "Golf Course" },
"SKI_AREA": { icon: "⛷️", es: "Área de esquí", en: "Ski Area" },
"RACING_TRACK": { icon: "🛷⛸🏎️", es: "Pista de carreras", en: "Racing Track" },
// Gobierno y Servicios Públicos / Government & Public Services
"GOVERNMENT": { icon: "🏛️", es: "Oficina gubernamental", en: "Government Office" },
"POLICE_STATION": { icon: "👮", es: "Estación de policía", en: "Police Station" },
"FIRE_STATION": { icon: "🚒", es: "Estación de bomberos", en: "Fire Station" },
"FIRE_DEPARTMENT": { icon: "🚒", es: "Departamento de bomberos", en: "Fire Department" },
"POST_OFFICE": { icon: "📫", es: "Correo", en: "Post Office" },
"TRANSPORTATION": { icon: "🚌", es: "Transporte", en: "Transportation" },
"PRISON_CORRECTIONAL_FACILITY": { icon: "👁️🗨️", es: "Prisión o Centro Correccional", en: "Prison or Correctional Facility" },
// Religión / Religion
"RELIGIOUS_CENTER": { icon: "⛪", es: "Iglesia", en: "Church" },
// Otros / Others
"RESIDENTIAL": { icon: "🏘️", es: "Residencial", en: "Residential" },
"RESIDENCE_HOME": { icon: "🏠", es: "Residencia o Hogar", en: "Residence or Home" },
"OFFICES": { icon: "🏢", es: "Oficina", en: "Office" },
"FACTORY": { icon: "🏭", es: "Fábrica", en: "Factory" },
"CONSTRUCTION_SITE": { icon: "🏗️", es: "Construcción", en: "Construction" },
"MONUMENT": { icon: "🗽", es: "Monumento", en: "Monument" },
"BRIDGE": { icon: "🌉", es: "Puente", en: "Bridge" },
"PROFESSIONAL_AND_PUBLIC": { icon: "🗄💼", es: "Profesional y Público", en: "Professional and Public" },
"OTHER": { icon: "🚪", es: "Otro", en: "Other" },
"ARTS_AND_CRAFTS": { icon: "🎨", es: "Artes y Manualidades", en: "Arts and Crafts" },
"COTTAGE_CABIN": { icon: "🏡", es: "Cabaña", en: "Cottage Cabin" },
"TELECOM": { icon: "📡", es: "Telecomunicaciones", en: "Telecommunications" }
};
// Si no hay categoría, devolver ícono por defecto
if (!categoryName)
{
return { icon: "❓", title: "Sin categoría / No category" };
}
// Normalizar el nombre de la categoría
const normalizedInput = String(categoryName).toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
//console.log("[WME_PLN][DEBUG] Buscando ícono para categoría:", categoryName);
//console.log("[WME_PLN][DEBUG] Nombre normalizado:", normalizedInput);
// 1. Buscar coincidencia exacta por clave interna (ej: "PARK")
for (const [key, data] of Object.entries(categoryIcons))
{
if (key.toLowerCase() === normalizedInput)
{
return { icon: data.icon, title: `${data.es} / ${data.en}` };
}
}
// Buscar coincidencia en el mapa de categorías
for (const [key, data] of Object.entries(categoryIcons))
{
// Normalizar los nombres en español e inglés para la comparación
const normalizedES = data.es.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
const normalizedEN = data.en.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
if (normalizedInput === normalizedES || normalizedInput === normalizedEN)
{
return { icon: data.icon, title: `${data.es} / ${data.en}` };
}
}
// Si no se encuentra coincidencia, devolver ícono por defecto
//console.log("[WME_PLN][DEBUG] No se encontró coincidencia, usando ícono por defecto");
return {
icon: "⚪",
title: `${categoryName} (Sin coincidencia / No match)`
};
}// getCategoryIcon
// Función para manejar el archivo XML arrastrado
function exportExcludedWordsList()
{
// Verificar si hay palabras excluidas
if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
{
alert("No hay palabras especiales ni reemplazos para exportar.");
return;
}
// Crear el contenido XML
let xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n`;
xmlContent +=
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map(w => ` <word>${xmlEscape(w)}</word>`)
.join("\n");
// Añadir reemplazos si existen
if (Object.keys(replacementWords).length > 0)
{
xmlContent += "\n";
xmlContent +=
Object.entries(replacementWords)
.map(([ from, to ]) => ` <replacement from="${
xmlEscape(from)}">${xmlEscape(to)}</replacement>`)
.join("\n");
}
xmlContent += "\n</ExcludedWords>";
// Crear el Blob y descargarlo
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
// Crear un enlace temporal para descargar el archivo
const url = URL.createObjectURL(blob);
// Crear un elemento <a> para descargar el archivo
const a = document.createElement("a");
a.href = url;
a.download = "wme_excluded_words_export.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}// exportExcludedWordsList
// Función para exportar palabras del diccionario a XML
function exportDictionaryWordsList()
{
// Verificar si hay palabras en el diccionario
if (window.dictionaryWords.size === 0)
{
alert(
"La lista de palabras del diccionario está vacía. Nada que exportar.");
return;
}
// Crear el contenido XML
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<diccionario>\n${
Array.from(window.dictionaryWords)
.sort((a, b) => a.toLowerCase().localeCompare(
b.toLowerCase())) // Exportar ordenado
.map(w => ` <word>${xmlEscape(w)}</word>`) // Indentación y escape
.join("\n")}\n</diccionario>`;
// Crear el Blob y descargarlo
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" }); // Añadir charset
// Crear un enlace temporal para descargar el archivo
const url = URL.createObjectURL(blob);
// Crear un elemento <a> para descargar el archivo
const a = document.createElement("a");
a.href = url;
a.download = "wme_dictionary_words_export.xml"; // Nombre más descriptivo
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}// exportDictionaryWordsList
// Función para exportar datos compartidos a XML
function xmlEscape(str)
{
return str.replace(/[<>&"']/g, function (match)
{
switch (match)
{
case '<':
return '<';
case '>':
return '>';
case '&':
return '&';
case '"':
return '"';
case "'":
return ''';
default:
return match;
}
});
}// xmlEscape
// Add this near the end of your script init
window.addEventListener('beforeunload', function() {
// Cancel any pending requests or cleanup tasks
pendingRequests = [];
// Save any unsaved data to localStorage
if (window.dynamicCategoryRules && window.dynamicCategoryRules.length) {
try {
localStorage.setItem("wme_pln_categories_cache", JSON.stringify({
data: window.dynamicCategoryRules,
timestamp: Date.now()
}));
} catch (e) {
console.warn('[WME PLN] Error saving categories on unload:', e);
}
}
if (window.dictionaryWords && window.dictionaryWords.size) {
try {
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
} catch (e) {
console.warn('[WME PLN] Error saving dictionary on unload:', e);
}
}
});
// Función para manejar el archivo XML arrastrado
waitForSidebarAPI();
// Iniciar el bucle de procesamiento para el efecto de titilado
requestAnimationFrame(processingLoop);
//Llamar a la función para mostrar el changelog
showChangelogOnUpdate();
// Agregar un observador para detectar cuándo se cierra el panel de resultados
// Esto ayudará a restablecer los estados correctamente cuando el usuario cierra el panel manualmente
function setupResultsPanelObserver() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const panel = document.getElementById('wme-place-inspector-panel');
if (panel && panel.style.display === 'none') {
// El panel se ha cerrado, restablecer los estados
isResultsPanelOpen = false;
isProcessingActive = false;
isNormalizationActive = true;
console.log("[WME PLN] Panel cerrado, estados restablecidos");
}
}
}
});
// Observar el panel principal para detectar cuando se cierra
const panel = document.getElementById('wme-place-inspector-panel');
if (panel) {
observer.observe(panel, { attributes: true });
} else {
// Si el panel aún no existe, configurar un temporizador para intentar nuevamente
setTimeout(setupResultsPanelObserver, 1000);
}
}
// Llamar a esta función al inicio
setTimeout(setupResultsPanelObserver, 1000);
})();
// Función reutilizable para mostrar el spinner de carga
function showLoadingSpinner()
{
const scanSpinner = document.createElement("div");
scanSpinner.id = "scanSpinnerOverlay";
scanSpinner.style.position = "fixed";
scanSpinner.style.top = "0";
scanSpinner.style.left = "0";
scanSpinner.style.width = "100%";
scanSpinner.style.height = "100%";
scanSpinner.style.background = "rgba(0, 0, 0, 0.5)";
scanSpinner.style.zIndex = "10000";
scanSpinner.style.display = "flex";
scanSpinner.style.justifyContent = "center";
scanSpinner.style.alignItems = "center";
// Estilos para centrar el contenido
const scanContent = document.createElement("div");
scanContent.style.background = "#fff";
scanContent.style.padding = "20px";
scanContent.style.borderRadius = "8px";
scanContent.style.textAlign = "center";
// Spinner de carga
const spinner = document.createElement("div");
spinner.classList.add("spinner");
spinner.style.width = "40px";
spinner.style.height = "40px";
spinner.style.margin = "0 auto 10px auto";
// Texto de progreso
const progressText = document.createElement("div");
progressText.id = "scanProgressText";
progressText.textContent = "Analizando lugares: 0%";
progressText.style.fontSize = "14px";
progressText.style.color = "#333";
// Añadir spinner y texto al contenido
scanContent.appendChild(spinner);
scanContent.appendChild(progressText);
scanSpinner.appendChild(scanContent);
document.body.appendChild(scanSpinner);
// Añadir estilos de animación al documento si no existen
if (!document.getElementById('wme-pln-animations')) {
const style = document.createElement("style");
style.id = 'wme-pln-animations';
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes area-blink {
0% { opacity: 1; }
50% { opacity: 0.3; }
100% { opacity: 1; }
}
.area-blink {
animation: area-blink 1s infinite;
}
.spinner {
border: 6px solid #f3f3f3;
border-top: 6px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(style);
}
// Añadir estilos para lugares sin nombre
const emptyNameStyles = document.createElement('style');
emptyNameStyles.textContent = `
.pln-empty-name-row {
background-color: #fff3e0;
}
.pln-empty-name-row:hover {
background-color: #ffe0b2;
}
`;
document.head.appendChild(emptyNameStyles);
}// showLoadingSpinner
// Función para agregar una palabra al diccionario
function addWordToDictionary(input)
{
const newWord = input.value.trim().toLowerCase();
if (!newWord)
{
alert("La palabra no puede estar vacía.");
return;
}
// Validaciones básicas antes de añadir
if (newWord.length === 1 && !newWord.match(/[a-zA-Z0-9]/))
{
alert("No se permite agregar un solo carácter que no sea alfanumérico.");
return;
}
if (commonWords.includes(newWord))
{
alert("Esa palabra es muy común y no debe agregarse al diccionario.");
return;
}
if (excludedWords.has(newWord))
{
alert("Esa palabra ya existe en la lista de especiales (excluidas).");
return;
}
if (window.dictionaryWords.has(newWord))
{
alert("La palabra ya existe en el diccionario.");
return;
}
if (!window.dictionaryWords) window.dictionaryWords = new Set();
if (!window.dictionaryIndex) window.dictionaryIndex = {};
window.dictionaryWords.add(newWord); // Añadir al Set
// === AÑADIR AL ÍNDICE ===
const firstChar = newWord.charAt(0).toLowerCase();
if (!window.dictionaryIndex[firstChar])
{
window.dictionaryIndex[firstChar] = [];
}
window.dictionaryIndex[firstChar].push(newWord); // Añadir al índice
input.value = ""; // Limpiar el input
renderDictionaryList(document.getElementById("dictionaryWordsList")); // Re-renderizar la lista
// Guardar en localStorage después de añadir
try
{
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando diccionario en localStorage después de añadir manualmente:", e);
}
}// addWordToDictionary