MAGNITUD 3.2 Mw ⚡ MMI II (Débil) 📲 Alertas WhatsApp

Sismo M 3.2 en Coquimbo, Chile

Profundidad: 50 km • • USGS & EMSC

🙋‍♂️ ¿Sentiste este sismo? Tocá tu intensidad (1 Toque): En vivo
🔔 ¿Avisarte si ocurre una réplica en Chile? GRATIS 24/7
📲 O recibir alertas en WhatsApp en 1 Toque →
🧠 ¿QUÉ SIGNIFICA ESTE SISMO? (EN 5 SEGUNDOS) 🟡 Foco Intermedio (50.0 km)

Al originarse a profundidad intermedia (50.0 km), las capas de roca amortiguan el golpe brusco inicial y el movimiento se percibe como un balanceo moderado distribuido en una región amplia.

🌊 Sin riesgo de tsunami: la magnitud y geometría focal no reúnen condiciones para generar alteraciones de tsunami.
Profundidad Hipocentral
50 km
Coordenadas Geográficas
-31.220°, -71.510°
Intensidad Mercalli (MMI)
MMI II (Débil)
Reportes DYFI ("¿Lo Sentiste?")
Sin reportes sensibles registrados

Resumen Instrumental y Análisis Geofísico

Reporte Sísmico Oficial: Magnitud 3.2 en Coquimbo, Chile (MMI II (Débil)). Hipocentro a 50 km (2026-11-02 19:41 UTC). Sismograma en tiempo real, calculadora de llegada de ondas P/S y consenso multi-agencia (USGS + EMSC).

Datos sincronizados en tiempo real mediante pasarela multi-agencia. Licencia de datos de dominio público CC0 1.0 Universal.

🌍 ¿Estabas cerca del epicentro en Coquimbo, Chile? Ayuda a la comunidad reportando tu percepción o recibe alertas instantáneas:
🗣️ ¿Lo sentiste? Reportar intensidad 📲 Alertas WhatsApp (+34 624 342 115)
🎒 RECOMENDACIÓN DE PREPARACIÓN: ELEMENTOS CLAVE PARA TU MOCHILA DE 72 HORAS 📋 Ver Checklist Completo 72h (8 elementos) →

Tras un sismo en Coquimbo, Chile, recomendamos contar en casa con estos 4 suministros esenciales ante cortes de luz o agua. Al tocar cualquiera te derivamos a las opciones que recomendamos en Amazon para su compra:

🎒 Mochila de Emergencia 72h Recomendado · Ver en Amazon ↗ 📻 Radio Solar / Dínamo + USB Recomendado · Ver en Amazon ↗ 🩹 Botiquín Primeros Auxilios IFAK Recomendado · Ver en Amazon ↗ 💧 Filtro Potabilizador Portátil Recomendado · Ver en Amazon ↗
💡 Como Afiliado de Amazon, obtenemos ingresos por las compras adscritas que cumplen los requisitos aplicables.
🎬 SERIE DOCUMENTAL PANORÁMICA 16:9 EN @SISMOSFERA3D (HISTORIA Y RANKINGS SÍSMICOS) ▶️ Ver Serie Completa en YouTube →
🏛️Los 6 Sismos que Cambiaron América Latina (1906–2010) 🏆Países Más Sísmicos de América Latina (2000–2025)
`; const blob = new Blob([fullHtml], { type: 'text/html;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `boletin-sismo-M${magVal.toFixed(1)}-${encodeURIComponent(eq.id || 'evento')}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('press_wire_downloaded_html', { eq_id: eq.id || 'unknown' }); } } function drawBeachball(canvas, strikeDeg, dipDeg, rakeDeg) { if (!canvas) return; const ctx = canvas.getContext('2d'); const size = canvas.width || 180; const radius = (size / 2) * 0.88; const cx = size / 2; const cy = size / 2; ctx.clearRect(0, 0, size, size); const strike = strikeDeg * Math.PI / 180; const dip = dipDeg * Math.PI / 180; const rake = rakeDeg * Math.PI / 180; // Vector normal a la falla n en North-East-Down: const nx = -Math.sin(dip) * Math.sin(strike); const ny = Math.sin(dip) * Math.cos(strike); const nz = -Math.cos(dip); // Vector de deslizamiento u en North-East-Down: const ux = Math.cos(rake) * Math.cos(strike) + Math.cos(dip) * Math.sin(rake) * Math.sin(strike); const uy = Math.cos(rake) * Math.sin(strike) - Math.cos(dip) * Math.sin(rake) * Math.cos(strike); const uz = Math.sin(dip) * Math.sin(rake); const imgData = ctx.createImageData(size, size); const data = imgData.data; // Colores sismológicos oficiales: Compresión (Rojo / Rosa intenso), Dilatación (Blanco hielo) const compR = 225, compG = 29, compB = 72; // #e11d48 const dilR = 248, dilG = 250, dilB = 252; // #f8fafc for (let py = 0; py < size; py++) { for (let px = 0; px < size; px++) { const dx = (px - cx) / radius; const dy = (cy - py) / radius; // Norte es arriba const r2 = dx * dx + dy * dy; if (r2 <= 1.0) { // Proyección estereográfica de igual área de Lambert sobre el hemisferio inferior: const cosTheta = 1.0 - r2; const sinTheta = Math.sqrt(Math.max(0, 1.0 - cosTheta * cosTheta)); const denom = Math.sqrt(Math.max(0.0001, dx * dx + dy * dy)); const rx = (dy / denom) * sinTheta; const ry = (dx / denom) * sinTheta; const rz = cosTheta; // Patrón de radiación de doble cupla: const dotN = rx * nx + ry * ny + rz * nz; const dotU = rx * ux + ry * uy + rz * uz; const amp = dotN * dotU; const idx = (py * size + px) * 4; if (amp >= 0) { data[idx] = compR; data[idx + 1] = compG; data[idx + 2] = compB; data[idx + 3] = 255; } else { data[idx] = dilR; data[idx + 1] = dilG; data[idx + 2] = dilB; data[idx + 3] = 255; } } } } ctx.putImageData(imgData, 0, 0); // Borde circular nítido ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.stroke(); // Indicador Norte Geográfico ctx.fillStyle = '#94a3b8'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.fillText('N', cx, 11); } // ==================================================================== // SUITE SISMOLÓGICA: SPRINT 2 — INTELIGENCIA SÍSMICA & DBSCAN // ==================================================================== let _activeDbscanClusters = []; // 1. Algoritmo DBSCAN Espacio-Temporal para Detección de Enjambres y Secuencias function computeDbscanClusters(events) { if (!Array.isArray(events) || events.length === 0) return []; events.forEach(e => { e._clusterInfo = null; }); const D_MAX_KM = 65; // Radio espacial: 65 km const T_MAX_MS = 72 * 3600 * 1000; // Ventana temporal: 72 horas const MIN_PTS = 3; // Mínimo de 3 eventos para formar cluster const visited = new Set(); const clusters = []; function getNeighbors(idx) { const p = events[idx]; const pTime = p.time ? p.time.getTime() : 0; const neighbors = []; for (let j = 0; j < events.length; j++) { if (j === idx) continue; const q = events[j]; const qTime = q.time ? q.time.getTime() : 0; const dt = Math.abs(pTime - qTime); if (dt > T_MAX_MS) continue; const dist = haversineDistance(p.latitude, p.longitude, q.latitude, q.longitude); if (dist <= D_MAX_KM) { const normDist = Math.sqrt(Math.pow(dist / D_MAX_KM, 2) + Math.pow(dt / T_MAX_MS, 2)); if (normDist <= 1.0) { neighbors.push(j); } } } return neighbors; } for (let i = 0; i < events.length; i++) { if (visited.has(i)) continue; visited.add(i); const neighbors = getNeighbors(i); if (neighbors.length < MIN_PTS - 1) continue; const cluster = [i]; const queue = [...neighbors]; while (queue.length > 0) { const qIdx = queue.shift(); if (!visited.has(qIdx)) { visited.add(qIdx); const qNeighbors = getNeighbors(qIdx); if (qNeighbors.length >= MIN_PTS - 1) { qNeighbors.forEach(n => { if (!queue.includes(n)) queue.push(n); }); } } if (!cluster.includes(qIdx)) { cluster.push(qIdx); } } if (cluster.length >= MIN_PTS) { clusters.push(cluster); } } _activeDbscanClusters = clusters.map((clusterIndices, cIdx) => { const clusterEvents = clusterIndices.map(idx => events[idx]); const sortedByMag = [...clusterEvents].sort((a, b) => b.magnitude - a.magnitude); const maxMag = sortedByMag[0].magnitude; const minMag = sortedByMag[sortedByMag.length - 1].magnitude; const secondMag = sortedByMag.length > 1 ? sortedByMag[1].magnitude : maxMag; const deltaMag = maxMag - secondMag; const isSwarm = deltaMag < 0.8 || maxMag < 3.2; const mainshockId = sortedByMag[0].id; const avgLat = clusterEvents.reduce((acc, e) => acc + e.latitude, 0) / clusterEvents.length; const avgLon = clusterEvents.reduce((acc, e) => acc + e.longitude, 0) / clusterEvents.length; const representativePlace = clusterEvents[0].place || 'Zona de Enjambre'; clusterEvents.forEach(e => { const isMain = e.id === mainshockId; e._clusterInfo = { clusterId: `cluster_${cIdx}`, size: clusterEvents.length, isSwarm: isSwarm, isMainshock: !isSwarm && isMain, isAftershock: !isSwarm && !isMain, mainMag: maxMag.toFixed(1), maxMagnitude: maxMag, minMagnitude: minMag }; }); return { id: `cluster_${cIdx}`, isSwarm, size: clusterEvents.length, maxMag, minMag, avgLat, avgLon, place: representativePlace, events: clusterEvents }; }); return _activeDbscanClusters; } // 2. Estimador de Gutenberg-Richter y Valor 'b' por Máxima Verosimilitud (Aki-Utsu) function computeGutenbergRichterStats(events) { if (!Array.isArray(events) || events.length === 0) { return { bValue: 1.0, sigmaB: 0, mc: 3.0, totalCount: 0, validCount: 0, diagnosis: 'S/D', diagnosisColor: 'text-slate-400' }; } const mags = events.map(e => Number(e.magnitude)).filter(m => !isNaN(m) && m > 0); if (mags.length < 10) { return { bValue: 1.0, sigmaB: 0, mc: 3.0, totalCount: mags.length, validCount: mags.length, diagnosis: 'Muestra reducida (N < 10)', diagnosisColor: 'text-slate-400' }; } const binCounts = {}; mags.forEach(m => { const bin = Math.round(m * 10) / 10; binCounts[bin] = (binCounts[bin] || 0) + 1; }); let maxBin = 3.0; let maxCount = -1; Object.keys(binCounts).forEach(binStr => { const count = binCounts[binStr]; if (count > maxCount) { maxCount = count; maxBin = parseFloat(binStr); } }); // Estimación refinada de Mc por Curvatura Máxima (MAXC) corregida por redondeo (Woessner & Wiemer 2005) const mc = Number((maxBin + (mags.length >= 30 ? 0.2 : 0.0)).toFixed(1)); const deltaM = 0.1; const sampleMags = mags.filter(m => m >= mc); const n = sampleMags.length; if (n < 5) { return { bValue: 1.0, aValue: 3.5, sigmaB: 0, mc, totalCount: mags.length, validCount: n, diagnosis: 'Muestra insuficiente sobre Mc', diagnosisColor: 'text-slate-400' }; } const meanM = sampleMags.reduce((a, b) => a + b, 0) / n; const log10e = 0.4342944819; const denom = meanM - (mc - deltaM / 2); let bVal = denom > 0.02 ? (log10e / denom) : 1.0; bVal = Math.max(0.4, Math.min(2.5, bVal)); const sigmaB = bVal / Math.sqrt(n); const aVal = Math.log10(n) + bVal * mc; let diagnosis = 'Tensión Cortical Típica Global (b ≈ 1.0)'; let diagnosisColor = 'text-emerald-400'; if (bVal < 0.85) { diagnosis = 'Alto Esfuerzo Cortical Acumulado (Asperezas Bloqueadas)'; diagnosisColor = 'text-amber-400'; } else if (bVal > 1.15) { diagnosis = 'Predominio de Micro-Sismicidad / Actividad de Enjambre o Volcánica'; diagnosisColor = 'text-purple-400'; } return { bValue: Number(bVal.toFixed(2)), aValue: Number(aVal.toFixed(2)), sigmaB: Number(sigmaB.toFixed(2)), mc: Number(mc.toFixed(1)), meanM: Number(meanM.toFixed(2)), totalCount: mags.length, validCount: n, diagnosis, diagnosisColor }; } // 3. Monitor de Deformación de Benioff & Energía Radiada Acumulada function computeBenioffStrainSeries(events) { if (!Array.isArray(events) || events.length === 0) return { points: [], totalJoules: 0, totalTNTKilotons: '0' }; const sorted = [...events].filter(e => e.time && !isNaN(e.time.getTime()) && !isNaN(e.magnitude)) .sort((a, b) => a.time.getTime() - b.time.getTime()); let cumStrain = 0; let cumJoules = 0; const points = []; sorted.forEach(e => { const mag = Number(e.magnitude); const E = Math.pow(10, 4.8 + 1.5 * mag); const strain = Math.sqrt(E); cumStrain += strain; cumJoules += E; points.push({ id: e.id, time: e.time, place: e.place, mag: mag, energyJoules: E, cumStrain: cumStrain, cumJoules: cumJoules }); }); const totalTNTKilotons = (cumJoules / 4.184e12).toFixed(1); return { points, totalJoules: cumJoules, totalTNTKilotons, count: points.length }; } // 4. Controlador de Apertura del Modal de Benioff & Enjambres function openBenioffModal() { if (typeof trackToolHit === 'function') trackToolHit('benioff'); const modal = document.getElementById('modal-benioff-stats'); if (!modal) return; const bStats = computeGutenbergRichterStats(earthquakesData); const benioffData = computeBenioffStrainSeries(earthquakesData); const clusters = computeDbscanClusters(earthquakesData); const elB = document.getElementById('benioff-b-value'); const elBSigma = document.getElementById('benioff-b-sigma'); const elMc = document.getElementById('benioff-mc-value'); const elEnergy = document.getElementById('benioff-energy-joules'); const elTNT = document.getElementById('benioff-energy-tnt'); const elClustersCount = document.getElementById('benioff-clusters-count'); const elDiagText = document.getElementById('benioff-diagnosis-text'); const elEventsCount = document.getElementById('benioff-events-analyzed'); if (elB) elB.innerText = bStats.bValue.toFixed(2); if (elBSigma) elBSigma.innerText = `±${bStats.sigmaB.toFixed(2)} error estándar`; if (elMc) elMc.innerText = `M ${bStats.mc.toFixed(1)}`; if (elEnergy) elEnergy.innerText = `${benioffData.totalJoules.toExponential(2)} J`; if (elTNT) elTNT.innerText = `≈ ${benioffData.totalTNTKilotons} kilotones TNT`; if (elClustersCount) elClustersCount.innerText = clusters.length; if (elDiagText) { elDiagText.innerText = bStats.diagnosis; elDiagText.className = `text-sm font-black ${bStats.diagnosisColor}`; } if (elEventsCount) { elEventsCount.innerText = `N = ${bStats.validCount} de ${bStats.totalCount} eventos (≥ Mc)`; } renderBenioffStrainChart(benioffData.points); renderDbscanClustersTable(clusters); modal.classList.remove('hidden'); } function closeBenioffModal() { const modal = document.getElementById('modal-benioff-stats'); if (modal) modal.classList.add('hidden'); } // 5. Renderizador Gráfico SVG de la Curva de Benioff function renderBenioffStrainChart(points) { const container = document.getElementById('benioff-strain-chart-container'); if (!container) return; if (!points || points.length === 0) { container.innerHTML = 'S/D • Sin datos suficientes para trazar la curva de deformación'; return; } const w = 700; const h = 180; const padL = 50, padR = 25, padT = 15, padB = 25; const maxStrain = points[points.length - 1].cumStrain || 1; const chartW = w - padL - padR; const chartH = h - padT - padB; let pathD = `M ${padL} ${padT + chartH}`; let prevY = padT + chartH; points.forEach((p, idx) => { const x = padL + (idx / Math.max(1, points.length - 1)) * chartW; const y = padT + chartH - (p.cumStrain / maxStrain) * chartH; pathD += ` L ${x.toFixed(1)} ${prevY.toFixed(1)} L ${x.toFixed(1)} ${y.toFixed(1)}`; prevY = y; }); const areaD = `${pathD} L ${padL + chartW} ${padT + chartH} Z`; const majorCircles = points.map((p, idx) => { if (p.mag < 5.2) return ''; const x = padL + (idx / Math.max(1, points.length - 1)) * chartW; const y = padT + chartH - (p.cumStrain / maxStrain) * chartH; const color = p.mag >= 6.5 ? '#f43f5e' : '#f59e0b'; return ` M ${p.mag.toFixed(1)} - ${p.place} (${p.energyJoules.toExponential(1)} J) `; }).join(''); container.innerHTML = ` ${majorCircles} `; } // 6. Renderizador de Tabla de Clusters DBSCAN function renderDbscanClustersTable(clusters) { const tbody = document.getElementById('benioff-clusters-tbody'); const summary = document.getElementById('benioff-clusters-summary'); if (!tbody) return; if (!clusters || clusters.length === 0) { tbody.innerHTML = 'S/D • No se detectaron enjambres sísmicos densos en la ventana de 72 horas.'; if (summary) summary.innerText = '0 clusters activos'; return; } if (summary) summary.innerText = `${clusters.length} cluster(s) identificado(s)`; tbody.innerHTML = clusters.map(c => { const typeBadge = c.isSwarm ? '🌋 ENJAMBRE' : '⚡ SECUENCIA'; return ` ${typeBadge} ${c.place} ${c.size} sismos M ${c.minMag.toFixed(1)} - ${c.maxMag.toFixed(1)} `; }).join(''); } function flyToCluster(lat, lon) { closeBenioffModal(); if (typeof focusEarthquake === 'function') { focusEarthquake({ latitude: lat, longitude: lon, magnitude: 4.5, depth: 10, place: 'Centroide del Enjambre Sísmico' }); } } // ==================================================================== // SUITE SISMOLÓGICA: MOTOR USGS PAGER & REPORTES CIUDADANOS DYFI // ==================================================================== async function loadAndRenderPagerAndDyfi(eq) { const alertText = document.getElementById('detail-pager-alert-text'); const alertDesc = document.getElementById('detail-pager-alert-desc'); const alertDot = document.getElementById('detail-pager-alert-dot'); const mmiBadge = document.getElementById('detail-pager-mmi-badge'); const fatVal = document.getElementById('detail-pager-fatalities-val'); const ecoVal = document.getElementById('detail-pager-economic-val'); const dyfiCount = document.getElementById('detail-pager-dyfi-count'); const dyfiDesc = document.getElementById('detail-pager-dyfi-desc'); if (alertText) alertText.innerText = 'Consultando evaluación USGS PAGER...'; if (dyfiDesc) dyfiDesc.innerText = 'Consultando reportes de percepción ciudadana en USGS...'; let usgsDetail = null; try { const res = await fetch(`/api/usgs-detail?id=${encodeURIComponent(eq.id)}`, { signal: AbortSignal.timeout(6000) }); if (res.ok) { usgsDetail = await res.json(); } } catch (err) { console.warn('[PAGER API]', err.message); } const products = usgsDetail?.properties?.products || {}; const pagerProduct = products['losspager'] ? products['losspager'][0] : null; const dyfiProduct = products['dyfi'] ? products['dyfi'][0] : null; // Render PAGER if (pagerProduct && pagerProduct.properties) { const p = pagerProduct.properties; const level = (p.alertlevel || 'green').toLowerCase(); const maxMmi = p['max-mmi'] || eq.mmi || 'S/D'; if (mmiBadge) mmiBadge.innerText = `Max MMI: ${maxMmi}`; let colorHex = '#10b981'; let levelName = 'ALERTA VERDE'; let levelDesc = 'Impacto humano y económico mínimo esperado. Baja probabilidad de daños estructurales.'; if (level === 'yellow') { colorHex = '#f59e0b'; levelName = 'ALERTA AMARILLA'; levelDesc = 'Posibles daños locales y víctimas aisladas. Impacto económico moderado a nivel regional.'; } else if (level === 'orange') { colorHex = '#f97316'; levelName = 'ALERTA NARANJA'; levelDesc = 'Víctimas significativas y daños sustanciales probables. Requiere respuesta regional o nacional.'; } else if (level === 'red') { colorHex = '#ef4444'; levelName = 'ALERTA ROJA'; levelDesc = 'Catástrofe humanitaria y económica de gran escala. Impacto nacional con probable asistencia internacional.'; } if (alertDot) alertDot.style.backgroundColor = colorHex; if (alertText) { alertText.innerHTML = `${levelName} • M ${eq.magnitude.toFixed(1)}`; } if (alertDesc) alertDesc.innerText = levelDesc; // Rango de fatalidades if (fatVal) { if (level === 'green') fatVal.innerText = '0 - 1 víctimas'; else if (level === 'yellow') fatVal.innerText = '1 - 99 víctimas'; else if (level === 'orange') fatVal.innerText = '100 - 999 víctimas'; else fatVal.innerText = '1,000+ víctimas'; fatVal.style.color = colorHex; } // Rango económico if (ecoVal) { if (level === 'green') ecoVal.innerText = '< $1M USD (< 1% PIB)'; else if (level === 'yellow') ecoVal.innerText = '$1M - $100M USD'; else if (level === 'orange') ecoVal.innerText = '$100M - $1,000M USD'; else ecoVal.innerText = '> $1,000M USD (> 1% PIB)'; ecoVal.style.color = colorHex; } } else { if (alertDot) alertDot.style.backgroundColor = '#64748b'; if (alertText) alertText.innerText = 'S/D — Sin Alerta PAGER Activa'; if (alertDesc) { alertDesc.innerText = 'El USGS emite evaluaciones PAGER para sismos de magnitud relevante o con potencial de impacto humanitario.'; } if (mmiBadge) mmiBadge.innerText = `Max MMI: ${eq.mmi || 'S/D'}`; if (fatVal) { fatVal.innerText = 'S/D'; fatVal.style.color = '#94a3b8'; } if (ecoVal) { ecoVal.innerText = 'S/D'; ecoVal.style.color = '#94a3b8'; } } // Render DYFI if (dyfiProduct && dyfiProduct.properties) { const dp = dyfiProduct.properties; const numResp = parseInt(dp['num-responses'] || dp.nresp || '0', 10); const cdi = dp.cdi ? Number(dp.cdi).toFixed(1) : (eq.cdi ? Number(eq.cdi).toFixed(1) : 'S/D'); if (dyfiCount) dyfiCount.innerText = `${numResp.toLocaleString()} reportes ciudadanos`; if (dyfiDesc) { dyfiDesc.innerHTML = ` Intensidad Decimal Percibida por la Comunidad: CDI ${cdi} • Total de personas que llenaron el cuestionario macrosísmico oficial del USGS: ${numResp.toLocaleString()}. `; } } else if (eq.felt && eq.felt > 0) { if (dyfiCount) dyfiCount.innerText = `${eq.felt.toLocaleString()} reportes`; if (dyfiDesc) { dyfiDesc.innerHTML = `Reportes registrados en el resumen primario del USGS: ${eq.felt.toLocaleString()} personas.`; } } else { if (dyfiCount) dyfiCount.innerText = '0 reportes'; if (dyfiDesc) dyfiDesc.innerText = 'No se han registrado reportes ciudadanos en el sistema Did You Feel It? para este evento.'; } } // ==================================================================== // SUITE SISMOLÓGICA: MOTOR DE RÉPLICAS & PRONÓSTICO OAF (OMORI-UTSU) // ==================================================================== function loadAndRenderAftershockForecast(eq) { const mag = Number(eq.magnitude) || 4.5; const bathMajor = Math.max(2.0, Number((mag - 1.2).toFixed(1))); const badge = document.getElementById('detail-stats-aftershock-count-badge'); const p24 = document.getElementById('prob-aftershock-24h'); const p7d = document.getElementById('prob-aftershock-7d'); const p30d = document.getElementById('prob-aftershock-30d'); const pBath = document.getElementById('prob-aftershock-bath'); if (pBath) pBath.innerText = `M ${bathMajor}`; // Buscar réplicas reales en el catálogo local cargado (radio 100km, dentro de 72h) let relatedCount = 0; if (Array.isArray(earthquakesData)) { const eqTime = eq.time ? eq.time.getTime() : Date.now(); relatedCount = earthquakesData.filter(other => { if (other.id === eq.id) return false; const oTime = other.time ? other.time.getTime() : 0; const timeDiffHours = Math.abs(oTime - eqTime) / (3600 * 1000); if (timeDiffHours > 72) return false; const dist = haversineDistance(eq.latitude, eq.longitude, other.latitude, other.longitude); return dist <= 100; }).length; } if (badge) badge.innerText = `Réplicas registradas (72h): ${relatedCount}`; // Modelo Canónico de Reasenberg-Jones (1989/1994) & Omori-Utsu Modificado // Parámetros canónicos globales: a = -1.65, b = 1.0, p = 1.1, c = 0.05 días // Tasa esperada para réplicas de magnitud M >= Mtarget (donde Mtarget = max(3.0, Number((mag - 1.5).toFixed(1)))) const targetMag = Math.max(3.0, Number((mag - 1.5).toFixed(1))); const pExp = 1.1; const cDays = 0.05; const deltaM = Math.max(0.5, mag - targetMag); const K = Math.pow(10, -1.65 + 1.0 * deltaM); function expectedCount(tDays) { if (Math.abs(pExp - 1.0) < 0.01) { return K * Math.log((tDays + cDays) / cDays); } return (K / (1 - pExp)) * (Math.pow(tDays + cDays, 1 - pExp) - Math.pow(cDays, 1 - pExp)); } const n24h = expectedCount(1.0); const n7d = expectedCount(7.0); const n30d = expectedCount(30.0); // Probabilidad de Poisson P(N >= 1) = 1 - exp(-lambda) const prob24h = Math.min(99, Math.max(5, Math.round((1 - Math.exp(-n24h)) * 100))); const prob7d = Math.min(99, Math.max(10, Math.round((1 - Math.exp(-n7d)) * 100))); const prob30d = Math.min(99, Math.max(15, Math.round((1 - Math.exp(-n30d)) * 100))); if (p24) p24.innerText = `${prob24h}% (M≥${targetMag})`; if (p7d) p7d.innerText = `${prob7d}% (M≥${targetMag})`; if (p30d) p30d.innerText = `${prob30d}% (M≥${targetMag})`; // Renderizar gráfico SVG de la curva de decaimiento de Omori const chartContainer = document.getElementById('omori-chart-container'); if (chartContainer) { const days = [0.1, 0.5, 1, 2, 3, 5, 7, 10, 14, 21, 30]; const points = days.map(t => { const rate = K / Math.pow(t + cDays, pExp); return { t, rate }; }); const maxRate = Math.max(...points.map(p => p.rate)); const w = 260, h = 130; const padX = 25, padY = 15; const pathD = points.map((p, idx) => { const x = padX + (idx / (points.length - 1)) * (w - padX * 2); const y = (h - padY) - (p.rate / maxRate) * (h - padY * 2); return `${idx === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`; }).join(' '); chartContainer.innerHTML = ` ${points.filter((_, i) => i % 2 === 0).map((p, i) => { const x = padX + ((i * 2) / (points.length - 1)) * (w - padX * 2); const y = (h - padY) - (p.rate / maxRate) * (h - padY * 2); return ``; }).join('')} `; } } async function loadAndRenderFocalMechanism(eq) { currentFocalData = null; const focalCard = document.getElementById('detail-focal-card'); const focalType = document.getElementById('detail-focal-type'); const focalDesc = document.getElementById('detail-focal-desc'); const focalM0 = document.getElementById('detail-focal-m0'); const focalMagType = document.getElementById('detail-focal-mag-type'); const mainGrid = document.getElementById('detail-focal-main-grid'); const emptyBanner = document.getElementById('detail-focal-empty'); if (!eq || !eq.id) return; try { const res = await fetch(`/api/usgs-detail?id=${eq.id}`, { signal: AbortSignal.timeout(5000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const prods = data.properties && data.properties.products; const mtList = (prods && (prods['moment-tensor'] || prods['focal-mechanism'])) || []; if (mtList.length > 0) { const mt = mtList[0]; const p = mt.properties || {}; const s1 = parseFloat(p['nodal-plane-1-strike']); const d1 = parseFloat(p['nodal-plane-1-dip']); const r1 = parseFloat(p['nodal-plane-1-rake']); const s2 = parseFloat(p['nodal-plane-2-strike']); const d2 = parseFloat(p['nodal-plane-2-dip']); const r2 = parseFloat(p['nodal-plane-2-rake']); const faultCls = classifyFaultType(s1, d1, r1); if (focalCard) focalCard.className = `p-3.5 rounded-xl border flex items-center justify-between transition-all ${faultCls.badgeClass}`; if (focalType) focalType.innerHTML = `${faultCls.icon} ${faultCls.type}`; if (focalDesc) focalDesc.innerText = faultCls.desc; const m0Val = parseFloat(p['scalar-moment']); if (focalM0) focalM0.innerText = !isNaN(m0Val) ? `${m0Val.toExponential(2)} N•m` : 'S/D'; if (focalMagType) focalMagType.innerText = `Escala: ${p['derived-magnitude-type'] || eq.primaryAuthority || 'Mw'}`; // Planos Nodulares document.getElementById('detail-np1-strike').innerText = !isNaN(s1) ? `${s1.toFixed(1)}°` : 'S/D'; document.getElementById('detail-np1-dip').innerText = !isNaN(d1) ? `${d1.toFixed(1)}°` : 'S/D'; document.getElementById('detail-np1-rake').innerText = !isNaN(r1) ? `${r1.toFixed(1)}°` : 'S/D'; document.getElementById('detail-np2-strike').innerText = !isNaN(s2) ? `${s2.toFixed(1)}°` : 'S/D'; document.getElementById('detail-np2-dip').innerText = !isNaN(d2) ? `${d2.toFixed(1)}°` : 'S/D'; document.getElementById('detail-np2-rake').innerText = !isNaN(r2) ? `${r2.toFixed(1)}°` : 'S/D'; // Ejes P y T const pAz = p['p-axis-azimuth'] || '--'; const pPl = p['p-axis-plunge'] || '--'; const tAz = p['t-axis-azimuth'] || '--'; const tPl = p['t-axis-plunge'] || '--'; document.getElementById('detail-p-axis').innerText = `${pAz}° / ${pPl}°`; document.getElementById('detail-t-axis').innerText = `${tAz}° / ${tPl}°`; const dc = parseFloat(p['percent-double-couple']); document.getElementById('detail-double-couple').innerText = !isNaN(dc) ? `${(dc * 100).toFixed(0)}%` : 'S/D'; // Persistir currentFocalData con verificacion de datos reales currentFocalData = { eventId: eq.id, strike1: s1, dip1: d1, rake1: r1, strike2: s2, dip2: d2, rake2: r2, pAz: pAz, pPl: pPl, tAz: tAz, tPl: tPl, dc: dc, scalarMoment: m0Val, derivedMagType: p['derived-magnitude-type'] || eq.primaryAuthority || 'Mw', faultCls: faultCls, activePlane: 1 }; // Renderizar Beachball en Canvas const canvas = document.getElementById('focal-beachball-canvas'); if (canvas && !isNaN(s1) && !isNaN(d1) && !isNaN(r1)) { drawBeachball(canvas, s1, d1, r1); } if (mainGrid) mainGrid.classList.remove('hidden'); if (emptyBanner) emptyBanner.classList.add('hidden'); return; } } catch (e) { // Fallback defensivo ante falla de red } // Estado sin datos disponible (Regla de Oro) if (focalCard) focalCard.className = 'p-3.5 rounded-xl border border-slate-800 bg-slate-900/60 flex items-center justify-between transition-all'; if (focalType) focalType.innerHTML = '⚪ S/D (Sin Tensor de Momento Disponible)'; if (focalDesc) focalDesc.innerText = 'El sismo no cuenta con inversión tensorial calculada por el USGS o GEOFON en este momento.'; if (focalM0) focalM0.innerText = 'S/D'; if (focalMagType) focalMagType.innerText = 'Escala: S/D'; if (mainGrid) mainGrid.classList.add('hidden'); if (emptyBanner) emptyBanner.classList.remove('hidden'); } // ==================================================================== // MOTOR DE SHAKEMAP (CURVAS DE ACELERACIÓN PGA %g E INTENSIDAD MMI) // ==================================================================== let detailShakeMapLayer = null; let currentShakeMapMode = 'off'; // 'off', 'mmi', 'pga' function resetDetailShakeMap() { currentShakeMapMode = 'off'; const hadShakeMapPaths = Array.isArray(activeGlobeShakeMapPaths) && activeGlobeShakeMapPaths.length > 0; activeGlobeShakeMapPaths = []; if (myGlobe && hadShakeMapPaths) updateVectorLayers(); if (detailSatelliteMapInstance && detailShakeMapLayer) { detailSatelliteMapInstance.removeLayer(detailShakeMapLayer); detailShakeMapLayer = null; } document.querySelectorAll('.btn-shakemap-toggle').forEach(btn => { if (btn.getAttribute('data-mode') === 'off') { btn.className = 'btn-shakemap-toggle px-2 py-0.5 rounded bg-sky-500/30 text-sky-300 font-bold border border-sky-500/40 cursor-pointer'; } else { btn.className = 'btn-shakemap-toggle px-2 py-0.5 rounded hover:bg-slate-800 text-slate-300 cursor-pointer'; } }); } async function setShakeMapMode(mode) { if (mode && mode !== 'off' && typeof trackToolHit === 'function') trackToolHit('shakemap'); currentShakeMapMode = mode; document.querySelectorAll('.btn-shakemap-toggle').forEach(btn => { if (btn.getAttribute('data-mode') === mode) { btn.className = 'btn-shakemap-toggle px-2 py-0.5 rounded bg-sky-500/30 text-sky-300 font-bold border border-sky-500/40 cursor-pointer'; } else { btn.className = 'btn-shakemap-toggle px-2 py-0.5 rounded hover:bg-slate-800 text-slate-300 cursor-pointer'; } }); if (!detailSatelliteMapInstance || !selectedEvent) return; if (detailShakeMapLayer) { detailSatelliteMapInstance.removeLayer(detailShakeMapLayer); detailShakeMapLayer = null; } if (mode === 'off') return; try { const res = await fetch(`/api/usgs-detail?id=${selectedEvent.id}`, { signal: AbortSignal.timeout(5000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const smList = data.properties && data.properties.products && data.properties.products.shakemap; if (!smList || smList.length === 0) { showToast('ℹ️ ShakeMap no disponible para este evento en el USGS'); resetDetailShakeMap(); return; } const sm = smList[0]; const targetFile = mode === 'mmi' ? 'download/cont_mmi.json' : 'download/cont_pga.json'; const fileObj = sm.contents && sm.contents[targetFile]; if (!fileObj || !fileObj.url) { showToast(`ℹ️ Isolíneas de ${mode.toUpperCase()} no disponibles en el producto ShakeMap`); resetDetailShakeMap(); return; } const geoRes = await fetch(`/api/shakemap-proxy?url=${encodeURIComponent(fileObj.url)}`, { signal: AbortSignal.timeout(8000) }); if (!geoRes.ok) throw new Error('Falla al descargar curvas ShakeMap'); const geoData = await geoRes.json(); // Paleta de colores sismológica para isolíneas detailShakeMapLayer = L.geoJSON(geoData, { style: (feature) => { const val = feature.properties ? feature.properties.value : 0; let color = feature.properties && feature.properties.color ? feature.properties.color : '#38bdf8'; let weight = 2.5; if (mode === 'mmi') { if (val >= 8) color = '#b91c1c'; else if (val >= 7) color = '#dc2626'; else if (val >= 6) color = '#ea580c'; else if (val >= 5) color = '#f59e0b'; else if (val >= 4) color = '#eab308'; else if (val >= 3) color = '#38bdf8'; else color = '#93c5fd'; } else if (mode === 'pga') { if (val >= 34) color = '#b91c1c'; else if (val >= 18) color = '#dc2626'; else if (val >= 9) color = '#ea580c'; else if (val >= 4) color = '#f59e0b'; else if (val >= 1.5) color = '#38bdf8'; else color = '#93c5fd'; } return { color, weight, opacity: 0.9, lineCap: 'round', lineJoin: 'round' }; }, onEachFeature: (feature, layer) => { if (feature.properties) { const val = feature.properties.value; const unit = mode === 'mmi' ? 'MMI' : '%g (Aceleración)'; layer.bindTooltip(`ShakeMap ${mode.toUpperCase()}: ${val} ${unit}`, { sticky: true }); } } }).addTo(detailSatelliteMapInstance); // Inyectar isolíneas en el Globo 3D activeGlobeShakeMapPaths = []; if (geoData.features) { geoData.features.forEach(f => { const val = f.properties ? f.properties.value : 0; let pathColor = 'rgba(56, 189, 248, 0.85)'; if (mode === 'mmi') { if (val >= 7) pathColor = 'rgba(220, 38, 38, 0.95)'; else if (val >= 5) pathColor = 'rgba(245, 158, 11, 0.95)'; else pathColor = 'rgba(56, 189, 248, 0.85)'; } else { if (val >= 18) pathColor = 'rgba(220, 38, 38, 0.95)'; else if (val >= 4) pathColor = 'rgba(245, 158, 11, 0.95)'; else pathColor = 'rgba(56, 189, 248, 0.85)'; } if (f.geometry && f.geometry.coordinates) { if (f.geometry.type === 'MultiLineString') { f.geometry.coordinates.forEach(seg => { activeGlobeShakeMapPaths.push({ name: `ShakeMap ${mode.toUpperCase()} ${val}`, coords: seg, color: pathColor, dash: 0 }); }); } else if (f.geometry.type === 'LineString') { activeGlobeShakeMapPaths.push({ name: `ShakeMap ${mode.toUpperCase()} ${val}`, coords: f.geometry.coordinates, color: pathColor, dash: 0 }); } } }); } if (myGlobe) updateVectorLayers(); showToast(`⚡ Curvas ShakeMap ${mode.toUpperCase()} proyectadas sobre el epicentro y Globo 3D`); } catch (err) { showToast('⚠️ No se pudieron cargar las curvas ShakeMap para este sismo'); resetDetailShakeMap(); } } // Inicializar listeners de ShakeMap function initShakeMapControls() { document.querySelectorAll('.btn-shakemap-toggle').forEach(btn => { btn.onclick = () => { const mode = btn.getAttribute('data-mode'); setShakeMapMode(mode); }; }); } // ==================================================================== // MAPA SATELITAL DE ALTA DEFINICIÓN DEL EPICENTRO (ESRI WORLD IMAGERY) // ==================================================================== let activeGlobeShakeMapPaths = []; let detailSatelliteMapInstance = null; let detailSatTileLayer = null; let detailLabelsTileLayer = null; let detailTopoTileLayer = null; let detailEpicenterMarker = null; let detailPerceptibleCircle = null; let currentDetailMapMode = 'satellite'; function initOrUpdateDetailSatelliteMap(eq) { const mapContainer = document.getElementById('detail-epicenter-satellite-map'); if (!mapContainer || typeof L === 'undefined') return; const lat = Number(eq.latitude); const lng = Number(eq.longitude); const zoom = (eq.magnitude >= 7.2) ? 7 : 8; const zoomBadge = document.getElementById('detail-map-zoom-badge'); if (zoomBadge) zoomBadge.innerText = `Zoom ${zoom}x`; if (!detailSatelliteMapInstance) { detailSatelliteMapInstance = L.map('detail-epicenter-satellite-map', { zoomControl: true, attributionControl: false }); // Escala gráfica métrica en la esquina inferior izquierda (apreciación dimensional de toda la zona) L.control.scale({ metric: true, imperial: false, position: 'bottomleft' }).addTo(detailSatelliteMapInstance); // Capa 1: Fotogrametría Satelital de Esri World Imagery (resolución submétrica) detailSatTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }).addTo(detailSatelliteMapInstance); // Capa 2: Carreteras, ciudades y referencias territoriales detailLabelsTileLayer = L.tileLayer('https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }).addTo(detailSatelliteMapInstance); // Capa 3: Topográfica con curvas de nivel detailTopoTileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { maxZoom: 17 }); // Evento de actualización de zoom en badge detailSatelliteMapInstance.on('zoomend', () => { if (zoomBadge) { zoomBadge.innerText = `Zoom ${detailSatelliteMapInstance.getZoom()}x`; } }); // Botones de capas const btnSat = document.getElementById('btn-map-sat-layer'); const btnTopo = document.getElementById('btn-map-topo-layer'); const btnRecenter = document.getElementById('btn-map-recenter'); if (btnSat && btnTopo) { btnSat.onclick = () => { if (currentDetailMapMode !== 'satellite') { if (detailSatelliteMapInstance.hasLayer(detailTopoTileLayer)) { detailSatelliteMapInstance.removeLayer(detailTopoTileLayer); } detailSatTileLayer.addTo(detailSatelliteMapInstance); detailLabelsTileLayer.addTo(detailSatelliteMapInstance); currentDetailMapMode = 'satellite'; btnSat.className = 'px-2 py-1 rounded bg-sky-600/30 hover:bg-sky-600/50 text-sky-300 border border-sky-400/50 font-semibold cursor-pointer'; btnTopo.className = 'px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 font-semibold cursor-pointer'; } }; btnTopo.onclick = () => { if (currentDetailMapMode !== 'topo') { if (detailSatelliteMapInstance.hasLayer(detailSatTileLayer)) { detailSatelliteMapInstance.removeLayer(detailSatTileLayer); } if (detailSatelliteMapInstance.hasLayer(detailLabelsTileLayer)) { detailSatelliteMapInstance.removeLayer(detailLabelsTileLayer); } detailTopoTileLayer.addTo(detailSatelliteMapInstance); currentDetailMapMode = 'topo'; btnTopo.className = 'px-2 py-1 rounded bg-amber-600/30 hover:bg-amber-600/50 text-amber-300 border border-amber-400/50 font-semibold cursor-pointer'; btnSat.className = 'px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 font-semibold cursor-pointer'; } }; } if (btnRecenter) { btnRecenter.onclick = () => { if (selectedEvent) { const rZoom = (selectedEvent.magnitude >= 7.2) ? 7 : 8; detailSatelliteMapInstance.setView([selectedEvent.latitude, selectedEvent.longitude], rZoom, { animate: true }); } }; } } // Posicionar en el epicentro exacto detailSatelliteMapInstance.setView([lat, lng], zoom); // Baliza de epicentro con halo de onda pulsante if (detailEpicenterMarker) { detailSatelliteMapInstance.removeLayer(detailEpicenterMarker); } const markerColor = eq.magnitude >= 6.0 ? '#ef4444' : (eq.magnitude >= 4.5 ? '#f97316' : '#38bdf8'); const customIcon = L.divIcon({ className: 'epicenter-map-pulse-marker', html: `
🎯
`, iconSize: [38, 38], iconAnchor: [19, 19] }); detailEpicenterMarker = L.marker([lat, lng], { icon: customIcon }).addTo(detailSatelliteMapInstance); detailEpicenterMarker.bindPopup(`
M ${eq.magnitude.toFixed(1)} • Epicentro
Lugar: ${eq.place}
Profundidad: ${eq.depth.toFixed(1)} km
GPS: ${lat.toFixed(4)}°, ${lng.toFixed(4)}°
`); // Círculo perceptible (área de percepción de vibración) if (detailPerceptibleCircle) { detailSatelliteMapInstance.removeLayer(detailPerceptibleCircle); } if (eq.rPerceptibleKm && eq.rPerceptibleKm > 0) { detailPerceptibleCircle = L.circle([lat, lng], { radius: eq.rPerceptibleKm * 1000, color: '#f59e0b', weight: 1.5, dashArray: '5, 8', fillColor: '#f59e0b', fillOpacity: 0.08 }).addTo(detailSatelliteMapInstance); } // Asegurar redibujado de tiles de Leaflet cuando el modal se despliega setTimeout(() => { if (detailSatelliteMapInstance) { detailSatelliteMapInstance.invalidateSize(); } }, 150); setTimeout(() => { if (detailSatelliteMapInstance) { detailSatelliteMapInstance.invalidateSize(); } }, 450); } // ==================================================================== // CONTROLADOR EN 3 PASOS SECUENCIALES DEL MODAL DE DETALLES (BENTO WIZARD) // ==================================================================== let currentDetailStep = 1; function switchDetailStep(stepNum) { currentDetailStep = Math.max(1, Math.min(3, stepNum)); // Conmutar panel correspondiente según el paso if (currentDetailStep === 1) { switchDetailModalTab('consensus'); } else if (currentDetailStep === 2) { switchDetailModalTab('focal'); } else if (currentDetailStep === 3) { const hasPager = selectedEvent && (selectedEvent.mmi || selectedEvent.alert || Number(selectedEvent.magnitude || 0) >= 5.0); switchDetailModalTab(hasPager ? 'pager' : 'stats'); } } function navigateDetailStep(delta) { let nextStep = currentDetailStep + delta; if (nextStep < 1) nextStep = 1; if (nextStep > 3) nextStep = 1; // bucle ordenado switchDetailStep(nextStep); } function switchDetailModalTab(tabId) { // 1. Mapeo de pestaña activa hacia la etapa (paso 1, 2 o 3) const stepMap = { 'consensus': 1, 'focal': 2, 'finite-fault': 2, 'coulomb': 2, 'pager': 3, 'stats': 3, 'infra': 3, 'ak135': 3, 'gmm': 3, 'sat-waves': 3, 'historic': 3 }; const stepNum = stepMap[tabId] || 1; currentDetailStep = stepNum; const detailScrollEl = document.querySelector('#modal-event-details-card .flex-1.overflow-y-auto'); if (detailScrollEl) detailScrollEl.scrollTop = 0; // 2. Actualizar estilos de los 3 Botones del Stepper Superior const btnStep1 = document.getElementById('btn-step-1'); const btnStep2 = document.getElementById('btn-step-2'); const btnStep3 = document.getElementById('btn-step-3'); const stepActive = 'group relative px-3 py-2 md:py-2.5 rounded-2xl bg-sky-500/15 border-2 border-sky-400 text-sky-200 transition-all flex items-center justify-center gap-2 cursor-pointer shadow-lg shadow-sky-500/20 text-left'; const stepInactive = 'group relative px-3 py-2 md:py-2.5 rounded-2xl bg-slate-950/60 border border-slate-800 text-slate-400 hover:text-slate-200 hover:border-slate-700 transition-all flex items-center justify-center gap-2 cursor-pointer text-left'; if (btnStep1) btnStep1.className = (stepNum === 1) ? stepActive : stepInactive; if (btnStep2) btnStep2.className = (stepNum === 2) ? stepActive : stepInactive; if (btnStep3) btnStep3.className = (stepNum === 3) ? stepActive : stepInactive; // Actualizar badges numéricos const b1 = btnStep1 ? btnStep1.querySelector('span') : null; const b2 = btnStep2 ? btnStep2.querySelector('span') : null; const b3 = btnStep3 ? btnStep3.querySelector('span') : null; if (b1) b1.className = (stepNum === 1) ? 'w-6 h-6 rounded-full bg-sky-400 text-slate-950 font-black text-xs flex items-center justify-center flex-shrink-0' : 'w-6 h-6 rounded-full bg-slate-800 text-slate-400 font-bold text-xs flex items-center justify-center flex-shrink-0'; if (b2) b2.className = (stepNum === 2) ? 'w-6 h-6 rounded-full bg-sky-400 text-slate-950 font-black text-xs flex items-center justify-center flex-shrink-0' : 'w-6 h-6 rounded-full bg-slate-800 text-slate-400 font-bold text-xs flex items-center justify-center flex-shrink-0'; if (b3) b3.className = (stepNum === 3) ? 'w-6 h-6 rounded-full bg-sky-400 text-slate-950 font-black text-xs flex items-center justify-center flex-shrink-0' : 'w-6 h-6 rounded-full bg-slate-800 text-slate-400 font-bold text-xs flex items-center justify-center flex-shrink-0'; // 3. Manejo de la Barra Contextual de Sub-Pestañas (Pasos 2 y 3) const subtabsBar = document.getElementById('modal-detail-subtabs'); const sgFocal = document.getElementById('subgroup-focal'); const sgPager = document.getElementById('subgroup-pager'); if (subtabsBar) { if (stepNum === 2) { subtabsBar.classList.remove('hidden'); if (sgFocal) sgFocal.classList.remove('hidden'); if (sgPager) sgPager.classList.add('hidden'); } else if (stepNum === 3) { subtabsBar.classList.remove('hidden'); if (sgFocal) sgFocal.classList.add('hidden'); if (sgPager) sgPager.classList.remove('hidden'); } else { subtabsBar.classList.add('hidden'); } } // Estilizar sub-pills según pestaña seleccionada const pillActiveStyle = 'px-3.5 py-1.5 rounded-full bg-sky-500/20 text-sky-300 border border-sky-500/40 font-bold shadow-sm flex items-center gap-1.5 transition-all text-xs cursor-pointer'; const pillInactiveStyle = 'px-3.5 py-1.5 rounded-full bg-slate-900/60 hover:bg-slate-800 text-slate-400 hover:text-slate-200 border border-slate-800 font-medium transition-all text-xs flex items-center gap-1.5 cursor-pointer'; const pills = [ { id: 'focal', btn: document.getElementById('btn-tab-focal') }, { id: 'finite-fault', btn: document.getElementById('btn-tab-finite-fault') }, { id: 'coulomb', btn: document.getElementById('btn-tab-coulomb') }, { id: 'pager', btn: document.getElementById('btn-tab-pager') }, { id: 'stats', btn: document.getElementById('btn-tab-stats') }, { id: 'infra', btn: document.getElementById('btn-tab-infra') }, { id: 'ak135', btn: document.getElementById('btn-tab-ak135') }, { id: 'gmm', btn: document.getElementById('btn-tab-gmm') }, { id: 'sat-waves', btn: document.getElementById('btn-tab-sat-waves') }, { id: 'historic', btn: document.getElementById('btn-tab-historic') } ]; pills.forEach(p => { if (!p.btn) return; p.btn.className = (p.id === tabId) ? pillActiveStyle : pillInactiveStyle; }); // 4. Actualizar botones del Stepper en el Footer const btnPrev = document.getElementById('btn-prev-step'); const btnNext = document.getElementById('btn-next-step'); const txtPrev = document.getElementById('btn-prev-step-text'); const txtNext = document.getElementById('btn-next-step-text'); if (btnPrev && btnNext) { if (stepNum === 1) { btnPrev.classList.add('opacity-40', 'cursor-not-allowed'); btnPrev.classList.remove('hover:bg-slate-700', 'cursor-pointer'); btnPrev.setAttribute('disabled', 'true'); if (txtPrev) txtPrev.innerText = 'Paso Anterior'; if (txtNext) txtNext.innerText = 'Paso 2: Geología'; } else if (stepNum === 2) { btnPrev.classList.remove('opacity-40', 'cursor-not-allowed'); btnPrev.classList.add('hover:bg-slate-700', 'cursor-pointer'); btnPrev.removeAttribute('disabled'); if (txtPrev) txtPrev.innerText = 'Paso 1: Resumen'; if (txtNext) txtNext.innerText = 'Paso 3: Impacto'; } else if (stepNum === 3) { btnPrev.classList.remove('opacity-40', 'cursor-not-allowed'); btnPrev.classList.add('hover:bg-slate-700', 'cursor-pointer'); btnPrev.removeAttribute('disabled'); if (txtPrev) txtPrev.innerText = 'Paso 2: Geología'; if (txtNext) txtNext.innerText = '↺ Volver a Paso 1'; } } // 5. Conmutar visibilidad de paneles de contenido const panels = [ { id: 'consensus', el: document.getElementById('tab-panel-consensus') }, { id: 'focal', el: document.getElementById('tab-panel-focal') }, { id: 'finite-fault', el: document.getElementById('tab-panel-finite-fault') }, { id: 'coulomb', el: document.getElementById('tab-panel-coulomb') }, { id: 'pager', el: document.getElementById('tab-panel-pager') }, { id: 'stats', el: document.getElementById('tab-panel-stats') }, { id: 'infra', el: document.getElementById('tab-panel-infra') }, { id: 'ak135', el: document.getElementById('tab-panel-ak135') }, { id: 'gmm', el: document.getElementById('tab-panel-gmm') }, { id: 'sat-waves', el: document.getElementById('tab-panel-sat-waves') }, { id: 'historic', el: document.getElementById('tab-panel-historic') } ]; panels.forEach(p => { if (!p.el) return; if (p.id === tabId) { p.el.classList.remove('hidden'); } else { p.el.classList.add('hidden'); } }); // 6. Triggers específicos por pestaña con Lazy Loading bajo demanda const tabToolMap = { 'consensus': 'consensus', 'focal': 'focal_mechanism', 'finite-fault': 'finite_fault', 'coulomb': 'coulomb', 'pager': 'pager', 'stats': 'omori', 'infra': 'hazus', 'ak135': 'ak135', 'gmm': 'nga_west2', 'sat-waves': 'waterfall_fft', 'historic': 'major_history' }; if (tabToolMap[tabId] && typeof trackToolHit === 'function') { trackToolHit(tabToolMap[tabId]); } if (tabId === 'sat-waves' && typeof trackToolHit === 'function') { trackToolHit('volcanic_spectral'); } if (tabId === 'consensus') { setTimeout(() => { if (detailSatelliteMapInstance) detailSatelliteMapInstance.invalidateSize(); if (typeof _currentPhases !== 'undefined' && !_currentPhases && selectedEvent && _seisLoadingForId !== selectedEvent.id) { loadAndRenderSeismogram(selectedEvent); } else if (typeof _currentPhases !== 'undefined' && _currentPhases) { renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); } }, 120); if (selectedEvent) renderConsensusRadarChart(selectedEvent); } else if (tabId === 'sat-waves') { setTimeout(() => { const phases = (typeof currentSeismogramPhases !== 'undefined' && currentSeismogramPhases) ? currentSeismogramPhases : (typeof _currentPhases !== 'undefined' ? _currentPhases : null); if (typeof renderWaterfallSpectrogram === 'function' && phases) { renderWaterfallSpectrogram(phases, 1.0); } else if (typeof renderSpectrogramWaterfall === 'function' && selectedEvent) { renderSpectrogramWaterfall(selectedEvent); } }, 120); } else if (tabId === 'focal') { if (selectedEvent) loadAndRenderFocalMechanism(selectedEvent); } else if (tabId === 'finite-fault') { if (selectedEvent) loadAndRenderFiniteFaultForEvent(selectedEvent); } else if (tabId === 'coulomb') { if (selectedEvent) loadAndRenderCoulombForEvent(selectedEvent); } else if (tabId === 'pager') { if (selectedEvent) loadAndRenderPagerAndDyfi(selectedEvent); } else if (tabId === 'stats') { if (selectedEvent) loadAndRenderAftershockForecast(selectedEvent); } else if (tabId === 'infra') { if (selectedEvent) { renderCriticalInfrastructureImpact(selectedEvent); renderHazusInfrastructureReport(selectedEvent); } } else if (tabId === 'gmm') { if (selectedEvent) { loadAndRenderGmmAttenuation(selectedEvent); renderGmmAttenuationChart(selectedEvent, _currentGmmVs30); } } else if (tabId === 'ak135') { if (selectedEvent) renderAk135PhasesTable(selectedEvent, 30.0); } else if (tabId === 'historic') { if (selectedEvent) loadAndRenderHistoricForEvent(selectedEvent); } } // ==================================================================== // TELEMETRÍA FDSN REAL Y MOTOR DE SISMOGRAMAS (ONDAS P, S Y SUPERFICIALES) // ==================================================================== let _seisAudioCtx = null; let _seisOsc = null; let _seisGain = null; let _seisFilter = null; let _seisIsPlaying = false; let _seisCurrentTime = 0; let _seisPlaySpeed = 5; let _seisAnimId = null; let _seisAudioEnabled = true; let _currentPhases = null; let _currentPhasesNear = null; let _currentPhasesMid = null; let _currentSeismoStationMode = 'near'; let _seisLastTimestamp = null; let _seisInteractionsInitialized = false; // FASE 210.0 (ITEM 2): Conmutador de Estaciones y Modo Comparativo Wadati P-S function switchSeismogramStationMode(mode) { _currentSeismoStationMode = mode; const btnNear = document.getElementById('btn-seismo-mode-near'); const btnMid = document.getElementById('btn-seismo-mode-mid'); const btnCompare = document.getElementById('btn-seismo-mode-compare'); const wadatiBadge = document.getElementById('seismo-wadati-badge'); const wadatiDelta = document.getElementById('seismo-wadati-delta'); const resetBtns = () => { [btnNear, btnMid, btnCompare].forEach(b => { if (b) { b.className = 'px-2.5 py-1 rounded-lg text-slate-400 hover:text-white border border-transparent cursor-pointer transition-all'; } }); }; resetBtns(); if (mode === 'near') { if (btnNear) btnNear.className = 'px-2.5 py-1 rounded-lg font-bold bg-sky-500/20 text-sky-300 border border-sky-500/40 cursor-pointer transition-all'; if (wadatiBadge) wadatiBadge.classList.add('hidden'); if (_currentPhasesNear) { _currentPhases = _currentPhasesNear; renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); } } else if (mode === 'mid') { if (btnMid) btnMid.className = 'px-2.5 py-1 rounded-lg font-bold bg-purple-500/20 text-purple-300 border border-purple-500/40 cursor-pointer transition-all'; if (wadatiBadge) wadatiBadge.classList.add('hidden'); if (_currentPhasesMid) { _currentPhases = _currentPhasesMid; renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); } } else if (mode === 'compare') { if (btnCompare) btnCompare.className = 'px-2.5 py-1 rounded-lg font-bold text-amber-300 border border-amber-500/40 bg-amber-500/20 cursor-pointer transition-all'; if (wadatiBadge) { wadatiBadge.classList.remove('hidden'); if (_currentPhasesNear && _currentPhasesMid) { const deltaT = Math.abs(_currentPhasesMid.tS - _currentPhasesNear.tS).toFixed(1); if (wadatiDelta) wadatiDelta.textContent = `+${deltaT}s`; } } if (_currentPhasesNear) { _currentPhases = _currentPhasesNear; renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); } } } // 1. Consulta en tiempo real a la Red FDSN / EarthScope para descubrir la estación real más cercana async function fetchNearestFdsnStation(lat, lon) { try { const url = `https://service.earthscope.org/fdsnws/station/1/query?latitude=${Number(lat).toFixed(4)}&longitude=${Number(lon).toFixed(4)}&maxradius=4.5&level=station&format=text`; const res = await fetch(url, { signal: AbortSignal.timeout(3800) }); if (!res.ok) throw new Error(`FDSN HTTP ${res.status}`); const text = await res.text(); const lines = text.trim().split('\n'); let bestStation = null; let minDistance = Infinity; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split('|'); if (cols.length < 6) continue; const net = cols[0].trim(); const sta = cols[1].trim(); const sLat = parseFloat(cols[2]); const sLon = parseFloat(cols[3]); const site = cols[5].trim(); if (isNaN(sLat) || isNaN(sLon)) continue; const d = haversineDistance(lat, lon, sLat, sLon); if (d < minDistance) { minDistance = d; bestStation = { net, sta, name: site || `${net}.${sta}`, lat: sLat, lon: sLon, distanceKm: Math.round(d * 10) / 10, channel: 'BHZ' }; } } if (bestStation) return bestStation; } catch (err) { // Fallback defensivo sin datos simulados } // Regla de Oro: Si no hay estación real disponible en el radio, retornar null sin fabricar datos return null; } // [REGLA DE ORO]: Función sintética syntheticWaveGenerator eliminada. // Todas las formas de onda provienen estrictamente de EarthScope DataSelect GeoCSV. // 3. Renderizador del Sismograma en Canvas 2D de Alta Definición function renderInteractiveSeismogram(phases, currentSec) { if (phases && typeof renderWaterfallSpectrogram === 'function') { const progress = phases.totalDuration > 0 ? (currentSec / phases.totalDuration) : 1.0; renderWaterfallSpectrogram(phases, progress); } const canvas = document.getElementById('seismogram-canvas'); if (!canvas) return; const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const width = rect.width > 50 ? rect.width : 700; const height = rect.height > 50 ? rect.height : 140; if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) { canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); } const ctx = canvas.getContext('2d'); ctx.save(); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, width, height); const midY = height / 2; const totalDuration = phases.totalDuration; const xForTime = (sec) => (sec / totalDuration) * width; // Fondo oscuro profundo ctx.fillStyle = '#050811'; ctx.fillRect(0, 0, width, height); // Rejilla sismográfica horizontal ctx.strokeStyle = 'rgba(30, 41, 59, 0.6)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, midY); ctx.lineTo(width, midY); ctx.moveTo(0, midY - height * 0.35); ctx.lineTo(width, midY - height * 0.35); ctx.moveTo(0, midY + height * 0.35); ctx.lineTo(width, midY + height * 0.35); ctx.stroke(); // Franjas de fase de ondas sísmicas const xP = xForTime(phases.tP); const xS = xForTime(phases.tS); const xSurf = xForTime(phases.tSurf); // Zona Onda P (cian translúcido) ctx.fillStyle = 'rgba(56, 189, 248, 0.08)'; ctx.fillRect(xP, 0, Math.max(0, xS - xP), height); // Zona Onda S (ámbar translúcido) ctx.fillStyle = 'rgba(245, 158, 11, 0.09)'; ctx.fillRect(xS, 0, Math.max(0, xSurf - xS), height); // Zona Ondas Superficiales (rosa translúcido) ctx.fillStyle = 'rgba(244, 63, 94, 0.11)'; ctx.fillRect(xSurf, 0, Math.max(0, width - xSurf), height); // Líneas divisorias verticales y etiquetas ctx.lineWidth = 1.5; ctx.setLineDash([4, 4]); // Línea Onda P ctx.strokeStyle = '#38bdf8'; ctx.beginPath(); ctx.moveTo(xP, 0); ctx.lineTo(xP, height); ctx.stroke(); ctx.fillStyle = '#38bdf8'; ctx.font = 'bold 9px "JetBrains Mono", monospace'; ctx.fillText(`P (+${phases.tP}s)`, xP + 4, 14); // Línea Onda S ctx.strokeStyle = '#f59e0b'; ctx.beginPath(); ctx.moveTo(xS, 0); ctx.lineTo(xS, height); ctx.stroke(); ctx.fillStyle = '#f59e0b'; ctx.fillText(`S (+${phases.tS}s)`, xS + 4, 14); // Línea Ondas Superficiales ctx.strokeStyle = '#f43f5e'; ctx.beginPath(); ctx.moveTo(xSurf, 0); ctx.lineTo(xSurf, height); ctx.stroke(); ctx.fillStyle = '#f43f5e'; ctx.fillText(`Surf (+${phases.tSurf}s)`, xSurf + 4, 14); ctx.setLineDash([]); // Reset dash // Dibujar la traza continua de la forma de onda ctx.lineWidth = 1.6; const gradient = ctx.createLinearGradient(0, 0, width, 0); gradient.addColorStop(0, '#38bdf8'); gradient.addColorStop(Math.min(0.99, Math.max(0.01, xS / width)), '#f59e0b'); gradient.addColorStop(Math.min(1.0, Math.max(0.02, xSurf / width)), '#f43f5e'); ctx.strokeStyle = gradient; ctx.beginPath(); const maxAmpPx = (height / 2) * 0.85; const pts = phases.wavePoints; const n = pts.length; for (let i = 0; i < n; i++) { const px = (i / (n - 1)) * width; const py = midY - pts[i] * maxAmpPx; if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); // FASE 210.0 (ITEM 2): Superposición Comparativa Wadati P-S if (_currentSeismoStationMode === 'compare' && _currentPhasesMid && _currentPhasesMid.wavePoints) { ctx.save(); ctx.lineWidth = 1.4; ctx.strokeStyle = '#c084fc'; ctx.setLineDash([3, 3]); ctx.beginPath(); const midPts = _currentPhasesMid.wavePoints; const nMid = midPts.length; for (let i = 0; i < nMid; i++) { const px = (i / (nMid - 1)) * width; const py = midY - midPts[i] * (maxAmpPx * 0.85); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); const xS_mid = xForTime(_currentPhasesMid.tS); ctx.strokeStyle = '#a855f7'; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(xS_mid, 0); ctx.lineTo(xS_mid, height); ctx.stroke(); ctx.fillStyle = '#d8b4fe'; ctx.font = 'bold 9px "JetBrains Mono", monospace'; ctx.fillText(`S-Reg (+${_currentPhasesMid.tS}s)`, xS_mid + 4, 25); ctx.restore(); } // Cursor temporal (aguja móvil de reproducción) const curX = Math.min(width, Math.max(0, xForTime(currentSec))); ctx.lineWidth = 2; ctx.strokeStyle = '#ffffff'; ctx.shadowColor = '#38bdf8'; ctx.shadowBlur = 8; ctx.beginPath(); ctx.moveTo(curX, 0); ctx.lineTo(curX, height); ctx.stroke(); ctx.shadowBlur = 0; // Círculo luminoso en el punto actual const sampleIdx = Math.min(n - 1, Math.max(0, Math.round((currentSec / totalDuration) * (n - 1)))); const curY = midY - pts[sampleIdx] * maxAmpPx; ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(curX, curY, 4, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // Actualizar readout textual const readout = document.getElementById('seismogram-time-readout'); if (readout) readout.innerText = `+${currentSec.toFixed(1)}s / ${totalDuration}s`; const tooltip = document.getElementById('seismogram-tooltip'); if (tooltip) { let phaseName = 'Pre-Evento (Ruido Sísmico)'; let phaseColor = 'text-slate-400'; if (currentSec >= phases.tSurf) { phaseName = 'Ondas Superficiales (Rayleigh/Love)'; phaseColor = 'text-rose-400'; } else if (currentSec >= phases.tS) { phaseName = 'Onda S (Secundaria / Cizalla)'; phaseColor = 'text-amber-400'; } else if (currentSec >= phases.tP) { phaseName = 'Onda P (Primaria / Compresión)'; phaseColor = 'text-sky-400'; } tooltip.innerHTML = `${phaseName} • +${currentSec.toFixed(1)}s`; tooltip.classList.remove('hidden'); } // Sincronización Interactiva de Línea de Tiempo y Fases (Fase 118.0) const timelineBar = document.getElementById('wave-timeline-bar'); if (timelineBar && totalDuration > 0) { timelineBar.style.width = Math.min(100, Math.max(0, (currentSec / totalDuration) * 100)) + '%'; } const statusBadge = document.getElementById('seismogram-wave-status-badge'); const cardP = document.getElementById('wave-card-p'); const cardS = document.getElementById('wave-card-s'); const cardSurf = document.getElementById('wave-card-surf'); if (cardP && cardS && cardSurf && statusBadge) { if (currentSec >= phases.tSurf) { statusBadge.innerText = 'Fase: Ondas Superficiales'; statusBadge.className = 'px-2.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-rose-500/20 text-rose-300 border border-rose-500/40 animate-pulse'; cardSurf.className = 'p-3 rounded-xl bg-rose-500/25 border-2 border-rose-400 shadow-lg shadow-rose-950/40 transition-all scale-[1.02]'; cardS.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; cardP.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; } else if (currentSec >= phases.tS) { statusBadge.innerText = 'Fase: Onda S (Secundaria)'; statusBadge.className = 'px-2.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-amber-500/20 text-amber-300 border border-amber-500/40 animate-pulse'; cardS.className = 'p-3 rounded-xl bg-amber-500/25 border-2 border-amber-400 shadow-lg shadow-amber-950/40 transition-all scale-[1.02]'; cardSurf.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; cardP.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; } else if (currentSec >= phases.tP) { statusBadge.innerText = 'Fase: Onda P (Primaria)'; statusBadge.className = 'px-2.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-500/20 text-sky-300 border border-sky-500/40 animate-pulse'; cardP.className = 'p-3 rounded-xl bg-sky-500/25 border-2 border-sky-400 shadow-lg shadow-sky-950/40 transition-all scale-[1.02]'; cardS.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; cardSurf.className = 'p-3 rounded-xl bg-slate-900/60 border border-slate-800 opacity-60 transition-all'; } else { statusBadge.innerText = 'Fase: Pre-Ruptura (Ruido)'; statusBadge.className = 'px-2.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-slate-900 text-slate-400 border border-slate-800'; cardP.className = 'p-3 rounded-xl bg-sky-500/10 border border-sky-500/30 transition-all'; cardS.className = 'p-3 rounded-xl bg-amber-500/10 border border-amber-500/30 transition-all'; cardSurf.className = 'p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 transition-all'; } } } // 4. Motor de Audio Web Audio API y Sonificación Sísmica Sincronizada function initSeismicAudioNodes() { if (!_seisAudioCtx) { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (AudioContextClass) _seisAudioCtx = new AudioContextClass(); } if (_seisAudioCtx && _seisAudioCtx.state === 'suspended') { _seisAudioCtx.resume(); } if (_seisAudioCtx && !_seisOsc) { _seisOsc = _seisAudioCtx.createOscillator(); _seisGain = _seisAudioCtx.createGain(); _seisFilter = _seisAudioCtx.createBiquadFilter(); _seisOsc.type = 'triangle'; _seisOsc.frequency.value = 55; _seisFilter.type = 'lowpass'; _seisFilter.frequency.value = 160; _seisFilter.Q.value = 3.5; _seisGain.gain.value = 0.0001; // Silencio inicial _seisOsc.connect(_seisFilter); _seisFilter.connect(_seisGain); _seisGain.connect(_seisAudioCtx.destination); _seisOsc.start(); } } function updateSeismicAudioFrequency(currentSec, phases) { if (!_seisAudioCtx || !_seisOsc || !_seisGain || !_seisFilter || !_seisAudioEnabled) return; const now = _seisAudioCtx.currentTime; const sampleIdx = Math.min(phases.wavePoints.length - 1, Math.max(0, Math.round((currentSec / phases.totalDuration) * (phases.wavePoints.length - 1)))); const amp = Math.abs(phases.wavePoints[sampleIdx]); if (currentSec < phases.tP) { // Ruido pre-evento _seisFilter.frequency.setTargetAtTime(70, now, 0.05); _seisOsc.frequency.setTargetAtTime(45, now, 0.05); _seisGain.gain.setTargetAtTime(0.015, now, 0.05); } else if (currentSec < phases.tS) { // Onda P: compresión rápida (~180-240 Hz) const targetFreq = 180 + amp * 70; _seisFilter.frequency.setTargetAtTime(300, now, 0.04); _seisOsc.frequency.setTargetAtTime(targetFreq, now, 0.04); _seisGain.gain.setTargetAtTime(0.06 + amp * 0.08, now, 0.04); } else if (currentSec < phases.tSurf) { // Onda S: cizalla transversal resonante (~95-150 Hz) const targetFreq = 95 + amp * 55; _seisFilter.frequency.setTargetAtTime(230, now, 0.04); _seisOsc.frequency.setTargetAtTime(targetFreq, now, 0.04); _seisGain.gain.setTargetAtTime(0.14 + amp * 0.15, now, 0.04); } else { // Ondas Superficiales: retumbe grave y potente (~45-75 Hz) const targetFreq = 50 + amp * 35; _seisFilter.frequency.setTargetAtTime(140, now, 0.05); _seisOsc.frequency.setTargetAtTime(targetFreq, now, 0.05); _seisGain.gain.setTargetAtTime(Math.min(0.35, 0.18 + amp * 0.22), now, 0.05); } } // FASE 211.0: Capas de Isócronas de Expansión Física de Ondas P y S let _seismoWavePCircle = null; let _seismoWaveSCircle = null; function _updateSeismicWaveIsochrones(currentSec) { if (!selectedEvent || typeof detailSatelliteMapInstance === 'undefined' || !detailSatelliteMapInstance) return; const lat = Number(selectedEvent.latitude); const lng = Number(selectedEvent.longitude); if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; // Velocidades canónicas en corteza terrestre: P ~ 6.0 km/s, S ~ 3.5 km/s const rP_meters = Math.max(100, currentSec * 6000); const rS_meters = Math.max(100, currentSec * 3500); try { if (!_seismoWavePCircle) { _seismoWavePCircle = L.circle([lat, lng], { radius: rP_meters, color: '#06b6d4', weight: 2, opacity: 0.85, fillColor: '#06b6d4', fillOpacity: 0.08, interactive: false }).addTo(detailSatelliteMapInstance); } else { _seismoWavePCircle.setLatLng([lat, lng]); _seismoWavePCircle.setRadius(rP_meters); } if (!_seismoWaveSCircle) { _seismoWaveSCircle = L.circle([lat, lng], { radius: rS_meters, color: '#f59e0b', weight: 2, dashArray: '5, 6', opacity: 0.9, fillColor: '#f59e0b', fillOpacity: 0.05, interactive: false }).addTo(detailSatelliteMapInstance); } else { _seismoWaveSCircle.setLatLng([lat, lng]); _seismoWaveSCircle.setRadius(rS_meters); } } catch (_) {} } function _clearSeismicWaveIsochrones() { try { if (_seismoWavePCircle && typeof detailSatelliteMapInstance !== 'undefined' && detailSatelliteMapInstance) { detailSatelliteMapInstance.removeLayer(_seismoWavePCircle); } if (_seismoWaveSCircle && typeof detailSatelliteMapInstance !== 'undefined' && detailSatelliteMapInstance) { detailSatelliteMapInstance.removeLayer(_seismoWaveSCircle); } } catch (_) {} _seismoWavePCircle = null; _seismoWaveSCircle = null; } function startSeismogramPlayback() { if (!_currentPhases) return; if (typeof trackToolHit === 'function') trackToolHit('seismogram'); initSeismicAudioNodes(); _seisIsPlaying = true; _seisLastTimestamp = performance.now(); const playBtnText = document.getElementById('btn-play-seismogram-text'); const playBtnIcon = document.getElementById('btn-play-seismogram-icon'); if (playBtnText) playBtnText.innerText = 'Pausar Traza'; if (playBtnIcon) playBtnIcon.innerText = '⏸'; function step(ts) { if (!_seisIsPlaying) return; const deltaMs = ts - _seisLastTimestamp; _seisLastTimestamp = ts; _seisCurrentTime += (deltaMs / 1000) * _seisPlaySpeed; if (_seisCurrentTime >= _currentPhases.totalDuration) { _seisCurrentTime = _currentPhases.totalDuration; pauseSeismogramPlayback(); renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); updateScrubberValue(_seisCurrentTime, _currentPhases.totalDuration); _clearSeismicWaveIsochrones(); return; } renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); updateScrubberValue(_seisCurrentTime, _currentPhases.totalDuration); updateSeismicAudioFrequency(_seisCurrentTime, _currentPhases); _updateSeismicWaveIsochrones(_seisCurrentTime); _seisAnimId = requestAnimationFrame(step); } _seisAnimId = requestAnimationFrame(step); } function pauseSeismogramPlayback() { _seisIsPlaying = false; if (_seisAnimId) cancelAnimationFrame(_seisAnimId); _seisAnimId = null; if (_seisGain && _seisAudioCtx) { _seisGain.gain.setTargetAtTime(0.0001, _seisAudioCtx.currentTime, 0.05); } const playBtnText = document.getElementById('btn-play-seismogram-text'); const playBtnIcon = document.getElementById('btn-play-seismogram-icon'); if (playBtnText) playBtnText.innerText = 'Reproducir Traza'; if (playBtnIcon) playBtnIcon.innerText = '▶'; } function resetSeismogramPlayback() { pauseSeismogramPlayback(); _seisCurrentTime = 0; if (_currentPhases) { renderInteractiveSeismogram(_currentPhases, 0); updateScrubberValue(0, _currentPhases.totalDuration); } _clearSeismicWaveIsochrones(); } function updateScrubberValue(sec, total) { const scrubber = document.getElementById('seismogram-scrubber'); if (scrubber) { scrubber.value = Math.round((sec / total) * 1000); } } // ==================================================================== // INICIALIZACIÓN GLOBAL DE HERRAMIENTAS, MENÚS TÁCTICOS Y CAPAS // ==================================================================== function initGlobalToolEventListeners() { if (window._globalToolListenersInit) return; window._globalToolListenersInit = true; // 1. Criosismos Polares (Herramienta 3.23) const btnOpenCryo = document.getElementById('btn-open-cryo-top'); if (btnOpenCryo) btnOpenCryo.onclick = () => loadAndRenderCryoQuakesModal('all'); const btnCloseCryo = document.getElementById('btn-close-cryo-modal'); if (btnCloseCryo) btnCloseCryo.onclick = closeCryoQuakesModal; document.querySelectorAll('.btn-cryo-filter').forEach(b => { b.onclick = () => { document.querySelectorAll('.btn-cryo-filter').forEach(btn => { btn.className = 'btn-cryo-filter px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold cursor-pointer'; }); b.className = 'btn-cryo-filter px-3 py-1.5 rounded-lg bg-sky-600 text-white font-bold cursor-pointer'; const reg = b.getAttribute('data-region') || 'all'; loadAndRenderCryoQuakesModal(reg); }; }); const chkCryo = document.getElementById('chk-cryo-zones'); if (chkCryo) chkCryo.onchange = () => renderCryoZonesOnGlobe(); // 2. Salud del Sistema y Telemetría de Red NOC const btnHealth = document.getElementById('btn-system-health'); if (btnHealth) btnHealth.onclick = () => openSystemHealthModal(); const btnCloseHealth = document.getElementById('btn-close-system-health'); if (btnCloseHealth) btnCloseHealth.onclick = () => closeSystemHealthModal(); const btnRefreshHealth = document.getElementById('btn-refresh-health'); if (btnRefreshHealth) btnRefreshHealth.onclick = () => fetchAndRenderSystemHealth(); if (typeof initPwaAndOfflineHandlers === 'function') { try { initPwaAndOfflineHandlers(); } catch(e){} } // 3. Distribución de Energía por Cuadrantes (Herramienta 3.18) const btnOpenQuadEnergy = document.getElementById('btn-open-quadrant-energy'); if (btnOpenQuadEnergy) btnOpenQuadEnergy.onclick = openQuadrantEnergyModal; const btnCloseQuadEnergy = document.getElementById('btn-close-quadrant-energy'); if (btnCloseQuadEnergy) btnCloseQuadEnergy.onclick = closeQuadrantEnergyModal; // 4. Capas Geofísicas: Volcanes y Brechas Sísmicas const chkVolcanoes = document.getElementById('chk-volcanoes'); if (chkVolcanoes) { chkVolcanoes.onchange = () => { if (chkVolcanoes.checked && typeof trackToolHit === 'function') trackToolHit('volcanoes'); if (typeof renderVolcanoesOnMap === 'function') renderVolcanoesOnMap(); if (typeof renderVolcanoesOnGlobe === 'function') renderVolcanoesOnGlobe(); }; } const chkSeismicGaps = document.getElementById('chk-seismic-gaps'); if (chkSeismicGaps) { chkSeismicGaps.onchange = () => { if (chkSeismicGaps.checked && typeof trackToolHit === 'function') trackToolHit('seismic_gaps'); if (typeof renderSeismicGapsOnMap === 'function') renderSeismicGapsOnMap(); if (typeof renderSeismicGapsOnGlobe === 'function') renderSeismicGapsOnGlobe(); }; } // 5. Grandes Terremotos Históricos (Herramienta F8.2) const btnOpenMajorHist = document.getElementById('btn-open-major-history'); if (btnOpenMajorHist) btnOpenMajorHist.onclick = openMajorHistoryModal; const btnCloseMajorHist = document.getElementById('btn-close-major-history'); if (btnCloseMajorHist) btnCloseMajorHist.onclick = closeMajorHistoryModal; // 6. Benioff & Enjambres const btnOpenBenioffTop = document.getElementById('btn-open-benioff-top'); if (btnOpenBenioffTop) btnOpenBenioffTop.onclick = openBenioffModal; const btnOpenBenioff = document.getElementById('btn-open-benioff'); if (btnOpenBenioff) btnOpenBenioff.onclick = openBenioffModal; const btnCloseBenioff = document.getElementById('btn-close-benioff'); if (btnCloseBenioff) btnCloseBenioff.onclick = closeBenioffModal; // 7. Boyas DART & Tsunami const btnOpenDartTop = document.getElementById('btn-open-dart-top'); if (btnOpenDartTop) btnOpenDartTop.onclick = () => openTsunamiDartModal(selectedEvent); // 8. Mapa de Calor Sísmico (Curvas de Nivel 3D - Fase 22.0) const chkHeatmap = document.getElementById('chk-heatmap'); if (chkHeatmap) { chkHeatmap.onchange = async (e) => { heatmapActive = e.target.checked; updateHeatmapQuickButton(heatmapActive); const ctrls = document.getElementById('heatmap-range-controls'); const legend = document.getElementById('heatmap-contour-legend'); if (heatmapActive) { if (ctrls) ctrls.classList.remove('opacity-50', 'pointer-events-none'); if (legend) legend.classList.remove('hidden'); const tl = document.getElementById('timelapse-bar'); if (tl) tl.classList.add('timelapse-shifted-right'); showToast('🔥 Proyectando Curvas de Nivel Sísmicas 3D (Líneas de Nivel de Colores)...'); await renderSeismicHeatmapContours(heatmapRange); } else { if (ctrls) ctrls.classList.add('opacity-50', 'pointer-events-none'); if (legend) legend.classList.add('hidden'); const tl = document.getElementById('timelapse-bar'); if (tl) tl.classList.remove('timelapse-shifted-right'); activeSeismicContourPaths = []; activeSeismicContourFeatures = []; updateVectorLayers(); if (myGlobe) myGlobe.hexBinPointsData([]); } }; } document.querySelectorAll('#heatmap-range-controls .btn-heat-range').forEach(btn => { btn.onclick = async () => { document.querySelectorAll('#heatmap-range-controls .btn-heat-range').forEach(b => { b.className = 'btn-heat-range py-1.5 px-2 rounded-xl bg-slate-900/60 border border-slate-800 text-slate-400 hover:text-white font-bold text-[10px] text-center cursor-pointer transition-all'; }); btn.className = 'btn-heat-range active py-1.5 px-2 rounded-xl bg-amber-500/20 border border-amber-500/60 text-amber-300 font-bold text-[10px] text-center cursor-pointer transition-all'; const range = btn.getAttribute('data-range') || '24h'; await renderSeismicHeatmapContours(range); }; }); // 9. Buzón de Sugerencias & Quiénes Somos const btnOpenSug = document.getElementById('btn-open-suggestions'); if (btnOpenSug) btnOpenSug.onclick = () => { if (typeof openSuggestionsModal === 'function') openSuggestionsModal(); }; const btnOpenSugMenu = document.getElementById('btn-open-suggestions-menu'); if (btnOpenSugMenu) btnOpenSugMenu.onclick = () => { if (typeof openSuggestionsModal === 'function') openSuggestionsModal(); }; } // Disparar de inmediato if (document.readyState !== 'loading') { initGlobalToolEventListeners(); initSeismogramCanvasInteractions(); } else { document.addEventListener('DOMContentLoaded', () => { initGlobalToolEventListeners(); initSeismogramCanvasInteractions(); }); } function initSeismogramCanvasInteractions() { if (_seisInteractionsInitialized) return; _seisInteractionsInitialized = true; // Eventos del Stepper de 3 Pasos const btnStep1 = document.getElementById('btn-step-1'); if (btnStep1) btnStep1.onclick = () => switchDetailStep(1); const btnStep2 = document.getElementById('btn-step-2'); if (btnStep2) btnStep2.onclick = () => switchDetailStep(2); const btnStep3 = document.getElementById('btn-step-3'); if (btnStep3) btnStep3.onclick = () => switchDetailStep(3); const btnPrev = document.getElementById('btn-prev-step'); if (btnPrev) btnPrev.onclick = () => navigateDetailStep(-1); const btnNext = document.getElementById('btn-next-step'); if (btnNext) btnNext.onclick = () => navigateDetailStep(1); // Eventos de Pestañas y Subpestañas const btnTabSat = document.getElementById('btn-tab-sat-waves'); if (btnTabSat) btnTabSat.onclick = () => switchDetailModalTab('sat-waves'); const btnTabCon = document.getElementById('btn-tab-consensus'); if (btnTabCon) btnTabCon.onclick = () => switchDetailModalTab('consensus'); const btnTabInf = document.getElementById('btn-tab-infra'); if (btnTabInf) btnTabInf.onclick = () => switchDetailModalTab('infra'); const btnTabFocal = document.getElementById('btn-tab-focal'); if (btnTabFocal) btnTabFocal.onclick = () => switchDetailModalTab('focal'); const btnTabPager = document.getElementById('btn-tab-pager'); if (btnTabPager) btnTabPager.onclick = () => switchDetailModalTab('pager'); const btnTabStats = document.getElementById('btn-tab-stats'); if (btnTabStats) btnTabStats.onclick = () => switchDetailModalTab('stats'); const btnTabGmm = document.getElementById('btn-tab-gmm'); if (btnTabGmm) btnTabGmm.onclick = () => switchDetailModalTab('gmm'); const btnTabAk135 = document.getElementById('btn-tab-ak135'); if (btnTabAk135) btnTabAk135.onclick = () => switchDetailModalTab('ak135'); const btnTabCoulomb = document.getElementById('btn-tab-coulomb'); if (btnTabCoulomb) btnTabCoulomb.onclick = () => switchDetailModalTab('coulomb'); const btnTabHistoric = document.getElementById('btn-tab-historic'); if (btnTabHistoric) btnTabHistoric.onclick = () => switchDetailModalTab('historic'); const btnTabFiniteFault = document.getElementById('btn-tab-finite-fault'); if (btnTabFiniteFault) btnTabFiniteFault.onclick = () => switchDetailModalTab('finite-fault'); // Botón Play y Slider de Falla Finita (Herramienta 3.24) const btnPlayFF = document.getElementById('btn-play-finite-fault'); if (btnPlayFF) btnPlayFF.onclick = toggleFiniteFaultPlay; const btnResetFF = document.getElementById('btn-reset-finite-fault'); if (btnResetFF) { btnResetFF.onclick = () => { _ffIsPlaying = false; _ffCurrentTimeSec = 0; const btnText = document.getElementById('btn-play-ff-text'); const btnIcon = document.getElementById('btn-play-ff-icon'); if (btnText) btnText.innerText = 'Propagación de Ruptura'; if (btnIcon) btnIcon.innerText = '▶'; drawFiniteFaultCanvas(); }; } const sliderFF = document.getElementById('slider-finite-fault-time'); if (sliderFF) { sliderFF.oninput = (e) => { if (_ffModelData) { _ffIsPlaying = false; _ffCurrentTimeSec = (parseFloat(e.target.value) / 100) * _ffModelData.ruptureDurationSec; const readout = document.getElementById('finite-fault-time-readout'); if (readout) readout.innerText = _ffCurrentTimeSec.toFixed(1) + 's'; drawFiniteFaultCanvas(); } }; } // (Criosismos y Salud NOC inicializados globalmente en initGlobalToolEventListeners) // Slider ak135 Delta const sliderAk135 = document.getElementById('slider-ak135-delta'); if (sliderAk135) { sliderAk135.oninput = (e) => { if (selectedEvent) { renderAk135PhasesTable(selectedEvent, parseFloat(e.target.value) || 30.0); } }; } // Botón ak135 Mi Ubicación const btnAk135MyLoc = document.getElementById('btn-ak135-my-loc'); if (btnAk135MyLoc) { btnAk135MyLoc.onclick = () => { if (selectedEvent && userLocation) { const distKm = haversineDistance(selectedEvent.latitude, selectedEvent.longitude, userLocation.latitude, userLocation.longitude); const delta = distKm / 111.19; renderAk135PhasesTable(selectedEvent, delta); showToast('📍 Calculando arribo ak135 a tu ubicación (' + distKm.toFixed(0) + ' km)'); } else { showToast('Ubica tu posición GPS primero con el botón superior'); } }; } // Botón ak135 Estación Cercana const btnAk135St = document.getElementById('btn-ak135-station'); if (btnAk135St) { btnAk135St.onclick = () => { if (selectedEvent && selectedEvent._assignedStation) { const st = selectedEvent._assignedStation; const distKm = haversineDistance(selectedEvent.latitude, selectedEvent.longitude, st.lat, st.lon); const delta = distKm / 111.19; renderAk135PhasesTable(selectedEvent, delta); showToast('📡 Calculando arribo ak135 a estación ' + st.code + ' (' + distKm.toFixed(0) + ' km)'); } else { renderAk135PhasesTable(selectedEvent, 15.0); } }; } // (Energía por cuadrante, volcanes y brechas inicializados globalmente en initGlobalToolEventListeners) // Botón de Sonificación Espectral Transpuesta (Herramienta 3.20) const btnSpectralAudio = document.getElementById('btn-play-spectral-audio'); if (btnSpectralAudio) { btnSpectralAudio.onclick = () => { playSpectralAudio(_currentPhases); showToast('🔊 Reproduciendo firma espectral transpuesta de ondas sísmicas'); }; } // Selector de clase de suelo Vs30 (Herramienta 3.13) document.querySelectorAll('.btn-gmm-soil').forEach(btn => { btn.onclick = () => { document.querySelectorAll('.btn-gmm-soil').forEach(b => { b.classList.remove('active', 'bg-sky-500/20', 'border-sky-400'); b.classList.add('bg-slate-950', 'border-slate-800'); }); btn.classList.add('active', 'bg-sky-500/20', 'border-sky-400'); btn.classList.remove('bg-slate-950', 'border-slate-800'); const vs = parseFloat(btn.getAttribute('data-vs30')) || 760; _currentGmmVs30 = vs; const labelEl = document.getElementById('gmm-soil-active-label'); if (labelEl) labelEl.innerText = btn.getAttribute('data-label') || `Vs30: ${vs} m/s`; if (selectedEvent) { renderGmmAttenuationChart(selectedEvent, _currentGmmVs30); } }; }); // Botón Generar Ficha Técnica Oficial PDF (Herramienta 3.15) const btnPrintReport = document.getElementById('btn-details-print-report'); if (btnPrintReport) { btnPrintReport.onclick = () => { if (selectedEvent) { generateSeismicReport(selectedEvent); } }; } const canvas = document.getElementById('seismogram-canvas'); if (canvas) { function seekTo(e) { if (!_currentPhases) return; const rect = canvas.getBoundingClientRect(); const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); _seisCurrentTime = pct * _currentPhases.totalDuration; renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); updateScrubberValue(_seisCurrentTime, _currentPhases.totalDuration); _updateSeismicWaveIsochrones(_seisCurrentTime); if (_seisIsPlaying) { updateSeismicAudioFrequency(_seisCurrentTime, _currentPhases); } } canvas.onmousedown = (e) => { seekTo(e); window.onmousemove = (me) => seekTo(me); window.onmouseup = () => { window.onmousemove = null; window.onmouseup = null; }; }; } const scrubber = document.getElementById('seismogram-scrubber'); if (scrubber) { scrubber.oninput = (e) => { if (!_currentPhases) return; const pct = Number(e.target.value) / 1000; _seisCurrentTime = pct * _currentPhases.totalDuration; renderInteractiveSeismogram(_currentPhases, _seisCurrentTime); _updateSeismicWaveIsochrones(_seisCurrentTime); if (_seisIsPlaying) { updateSeismicAudioFrequency(_seisCurrentTime, _currentPhases); } }; } const btnPlay = document.getElementById('btn-play-seismogram'); if (btnPlay) { btnPlay.onclick = () => { if (_seisIsPlaying) pauseSeismogramPlayback(); else startSeismogramPlayback(); }; } const btnReset = document.getElementById('btn-reset-seismogram'); if (btnReset) { btnReset.onclick = resetSeismogramPlayback; } const btnAudio = document.getElementById('btn-audio-toggle-seismogram'); if (btnAudio) { btnAudio.onclick = () => { _seisAudioEnabled = !_seisAudioEnabled; const icon = document.getElementById('btn-audio-icon'); if (icon) icon.innerText = _seisAudioEnabled ? '🔊' : '🔇'; if (!_seisAudioEnabled && _seisGain && _seisAudioCtx) { _seisGain.gain.setTargetAtTime(0.0001, _seisAudioCtx.currentTime, 0.05); } }; } document.querySelectorAll('.btn-seis-speed').forEach(btn => { btn.onclick = () => { document.querySelectorAll('.btn-seis-speed').forEach(b => { b.className = 'btn-seis-speed px-1.5 py-0.5 rounded hover:bg-slate-800 text-slate-300 cursor-pointer'; }); btn.className = 'btn-seis-speed px-1.5 py-0.5 rounded bg-sky-500/30 text-sky-300 font-bold border border-sky-500/40 cursor-pointer'; _seisPlaySpeed = Number(btn.getAttribute('data-speed')) || 5; }; }); } // ==================================================================== // PRE-CACHÉ EN RAM Y PROSPECCIÓN ULTRA-RÁPIDA DE SISMOGRAMAS FDSN // ==================================================================== const _seismogramEventCache = new Map(); let _seisLoadingForId = null; async function prefetchEventSeismogram(eqOrId) { const eq = (typeof eqOrId === 'object' && eqOrId) ? eqOrId : (Array.isArray(earthquakesData) ? earthquakesData.find(q => String(q.id) === String(eqOrId)) : null); if (!eq || !eq.id || _seismogramEventCache.has(eq.id)) return; const eqTime = eq.time ? (eq.time instanceof Date ? eq.time.getTime() : new Date(eq.time).getTime()) : Date.now(); const eqLat = Number(eq.latitude); const eqLon = Number(eq.longitude); if (isNaN(eqLat) || isNaN(eqLon)) return; try { const staRes = await fetch(`/api/station-nearest?lat=${eqLat}&lon=${eqLon}&time=${new Date(eqTime).toISOString()}`, { signal: AbortSignal.timeout(3500) }); if (!staRes.ok) return; const staData = await staRes.json(); const cand = staData.nearest; if (!cand || !cand.station || !cand.network) return; const startIso = new Date(eqTime - 25000).toISOString().split('.')[0]; const maxEnd = Date.now() - 10000; const endIso = new Date(Math.min(eqTime + 120000, maxEnd)).toISOString().split('.')[0]; if (new Date(startIso).getTime() >= new Date(endIso).getTime()) return; const sRes = await fetch(`/api/seismogram?net=${cand.network}&sta=${cand.station}&cha=BHZ&start=${startIso}&end=${endIso}`, { signal: AbortSignal.timeout(4000) }); if (sRes.ok) { const sJson = await sRes.json(); if (sJson.success && Array.isArray(sJson.samples) && sJson.samples.length > 20) { _seismogramEventCache.set(eq.id, { station: { net: cand.network, sta: cand.station, name: cand.siteName || `${cand.network}.${cand.station}`, lat: cand.latitude, lon: cand.longitude, elevation: cand.elevation || 0, distanceKm: cand.distanceKm || Math.round(haversineDistance(eqLat, eqLon, cand.latitude, cand.longitude) * 10) / 10, channel: sJson.cha || 'BHZ', isGsn: false }, realPayload: sJson, realSamples: new Float32Array(sJson.samples) }); } } } catch (_) {} } // 5. Función Maestra: Cargar Telemetría de Estación y Renderizar Sismograma (Fase 17.0) async function loadAndRenderSeismogram(eq) { if (!eq) return; _seisLoadingForId = eq.id; initSeismogramCanvasInteractions(); initShakeMapControls(); resetSeismogramPlayback(); const stationBadge = document.getElementById('seismogram-station-badge'); const stationDetail = document.getElementById('seismogram-station-detail'); // VERIFICACIÓN INMEDIATA DE CACHÉ EN RAM (< 1 ms) // Fase 198.6: la ruta de caché llamaba a renderLoadedSeismogram (inexistente → ReferenceError y sismograma vacío). // Ahora usa la estación y las muestras reales ya descargadas y continúa al dibujo normal (paso 5). let preloadedSeis = null; if (_seismogramEventCache.has(eq.id)) { const cached = _seismogramEventCache.get(eq.id); if (cached && cached.station && cached.realSamples) { preloadedSeis = cached; } } if (!preloadedSeis) { if (stationBadge) stationBadge.innerHTML = '● CONECTANDO SINCRONIZANDO TELEMETRÍA FDSN...'; if (stationDetail) stationDetail.innerHTML = 'Consultando catálogo de sensores sismológicos en la Red Global EarthScope...'; } const eqTime = eq.time ? (eq.time instanceof Date ? eq.time.getTime() : new Date(eq.time).getTime()) : Date.now(); const eqLat = Number(eq.latitude); const eqLon = Number(eq.longitude); const eqDepth = Math.max(Number(eq.depth) || 10, 2); // 1. Obtener lista de estaciones locales y alternativas desde el backend let candidateStations = []; if (!preloadedSeis) { try { const staRes = await fetch(`/api/station-nearest?lat=${eqLat}&lon=${eqLon}&time=${new Date(eqTime).toISOString()}`, { signal: AbortSignal.timeout(4500) }); if (staRes.ok) { const staData = await staRes.json(); if (staData.success) { if (staData.nearest) candidateStations.push(staData.nearest); if (Array.isArray(staData.alternatives)) candidateStations.push(...staData.alternatives); } } } catch (e) {} // Fallback a consulta directa si el backend no devolvió nada if (candidateStations.length === 0) { const directSta = await fetchNearestFdsnStation(eqLat, eqLon); if (directSta) candidateStations.push(directSta); } } // 2. Red Sísmica Global (GSN Flagship Stations) como garantía teleseísmica const GSN_STATIONS = [ { network: 'IU', station: 'ANMO', siteName: 'Albuquerque, New Mexico (EE.UU.)', latitude: 34.9459, longitude: -106.4572, elevation: 1820, isGsn: true }, { network: 'IU', station: 'TUC', siteName: 'Tucson, Arizona (EE.UU.)', latitude: 32.3088, longitude: -110.7842, elevation: 808, isGsn: true }, { network: 'IU', station: 'TRQA', siteName: 'Tornquist, Buenos Aires (Argentina)', latitude: -38.0560, longitude: -61.9790, elevation: 270, isGsn: true }, { network: 'II', station: 'BFO', siteName: 'Black Forest Observatory (Alemania)', latitude: 48.3301, longitude: 8.3296, elevation: 589, isGsn: true }, { network: 'IU', station: 'MAJO', siteName: 'Matsushiro, Nagano (Japón)', latitude: 36.5457, longitude: 138.2041, elevation: 420, isGsn: true }, { network: 'IU', station: 'SJG', siteName: 'San Juan, Puerto Rico (EE.UU.)', latitude: 18.1122, longitude: -66.1498, elevation: 424, isGsn: true }, { network: 'IU', station: 'KIP', siteName: 'Kipapa, Oahu, Hawaii (EE.UU.)', latitude: 21.4233, longitude: -158.0150, elevation: 84, isGsn: true }, { network: 'IU', station: 'CTAO', siteName: 'Charters Towers, Queensland (Australia)', latitude: -20.0882, longitude: 146.2544, elevation: 357, isGsn: true }, { network: 'II', station: 'AAK', siteName: 'Ala-Archa (Kirguistán)', latitude: 42.6390, longitude: 74.4940, elevation: 1645, isGsn: true } ]; // Calcular distancia haversine a cada estación GSN y ordenar GSN_STATIONS.forEach(gsn => { gsn.distanceKm = Math.round(haversineDistance(eqLat, eqLon, gsn.latitude, gsn.longitude) * 10) / 10; }); GSN_STATIONS.sort((a, b) => a.distanceKm - b.distanceKm); // Normalizar objetos de estaciones locales const normalizedCandidates = candidateStations.map(s => ({ net: s.network || s.net, sta: s.station || s.sta, name: s.siteName || s.name || `${s.network || s.net}.${s.station || s.sta}`, lat: s.latitude || s.lat, lon: s.longitude || s.lon, elevation: s.elevation || 0, distanceKm: s.distanceKm || Math.round(haversineDistance(eqLat, eqLon, s.latitude || s.lat, s.longitude || s.lon) * 10) / 10, channel: s.channel || 'BHZ', isGsn: false })); let activeStation = null; let realSamples = null; let realPayload = null; if (preloadedSeis) { activeStation = preloadedSeis.station; realSamples = preloadedSeis.realSamples; realPayload = preloadedSeis.realPayload; } // 3. Probar estaciones regionales en paralelo con Promise.any (hasta 3 candidatas) const testList = normalizedCandidates.slice(0, 3); if (testList.length > 0 && !realSamples) { if (stationBadge) { stationBadge.innerHTML = `● SONDEANDO ESTACIONES REGIONALES FDSN (${testList.length} sensores en paralelo)...`; } const candidatePromises = testList.map(async (cand) => { const d = cand.distanceKm || 45; const r = Math.sqrt(d * d + eqDepth * eqDepth); const tP = Math.max(2.0, Math.round((r / 6.8) * 10) / 10); const tS = Math.max(tP + 2.0, Math.round((r / 3.9) * 10) / 10); const tSurf = Math.max(tS + 2.5, Math.round((d / 3.2) * 10) / 10); const totalDuration = Math.min(300, Math.max(80, Math.round(tSurf + 55))); const startIso = new Date(eqTime - 25000).toISOString().split('.')[0]; const maxEnd = Date.now() - 10000; const targetEnd = eqTime + (totalDuration * 1000) + 15000; const endIso = new Date(Math.min(targetEnd, maxEnd)).toISOString().split('.')[0]; if (new Date(startIso).getTime() >= new Date(endIso).getTime()) { throw new Error('Too recent'); } const sRes = await fetch(`/api/seismogram?net=${cand.net}&sta=${cand.sta}&cha=BHZ&start=${startIso}&end=${endIso}`, { signal: AbortSignal.timeout(4500) }); if (!sRes.ok) throw new Error('HTTP ' + sRes.status); const sJson = await sRes.json(); if (!sJson.success || !Array.isArray(sJson.samples) || sJson.samples.length <= 20) throw new Error('No samples'); let hasEnergy = false; for (let i = 0; i < sJson.samples.length; i++) { if (Math.abs(sJson.samples[i]) > 0.001) { hasEnergy = true; break; } } if (!hasEnergy) throw new Error('Flatline'); return { station: { ...cand, channel: sJson.cha || 'BHZ' }, samples: new Float32Array(sJson.samples), payload: sJson }; }); try { const winner = await Promise.any(candidatePromises); activeStation = winner.station; realSamples = winner.samples; realPayload = winner.payload; _seismogramEventCache.set(eq.id, { station: activeStation, realPayload, realSamples }); } catch (_) {} } // 4. Si las estaciones locales no tienen telemetría disponible o están fuera de línea, probar GSN más cercana if (!realSamples) { for (const gsn of GSN_STATIONS.slice(0, 2)) { if (stationBadge) { stationBadge.innerHTML = `● ENLACE GSN SONDEANDO ESTACIÓN GLOBAL: ${gsn.network}.${gsn.station} (${gsn.distanceKm} km)...`; } const dGsn = gsn.distanceKm; // Tiempo de viaje teleseísmico aproximado const tP_gsn = Math.max(10, Math.round(dGsn / 8.5)); const tS_gsn = Math.max(tP_gsn + 15, Math.round(dGsn / 4.7)); const totalDuration_gsn = Math.min(300, Math.max(120, tS_gsn + 60)); const startIso = new Date(eqTime - 30000).toISOString().split('.')[0]; const maxEnd = Date.now() - 10000; const targetEnd = eqTime + (totalDuration_gsn * 1000) + 15000; const endIso = new Date(Math.min(targetEnd, maxEnd)).toISOString().split('.')[0]; if (new Date(startIso).getTime() >= new Date(endIso).getTime()) continue; try { const sRes = await fetch(`/api/seismogram?net=${gsn.network}&sta=${gsn.station}&cha=BHZ&start=${startIso}&end=${endIso}`, { signal: AbortSignal.timeout(6000) }); if (sRes.ok) { const sJson = await sRes.json(); if (sJson.success && Array.isArray(sJson.samples) && sJson.samples.length > 20) { let hasEnergy = false; for (let i = 0; i < sJson.samples.length; i++) { if (Math.abs(sJson.samples[i]) > 0.001) { hasEnergy = true; break; } } if (hasEnergy) { activeStation = { net: gsn.network, sta: gsn.station, name: gsn.siteName, lat: gsn.latitude, lon: gsn.longitude, elevation: gsn.elevation, distanceKm: gsn.distanceKm, channel: sJson.cha || 'BHZ', isGsn: true }; realSamples = new Float32Array(sJson.samples); realPayload = sJson; break; } } } } catch (e) {} } } // 5. Determinar la estación de referencia y cálculos de propagación const station = activeStation || normalizedCandidates[0] || { net: 'S/D', sta: 'S/D', name: 'Sin Estación Asignada', lat: eqLat, lon: eqLon, elevation: 0, distanceKm: 0, channel: 'S/D', isGsn: false }; const dist = station.distanceKm || 45; const r = Math.sqrt(dist * dist + eqDepth * eqDepth); let tP, tS, tSurf, totalDuration; if (station.isGsn) { tP = Math.max(10, Math.round(dist / 8.5)); tS = Math.max(tP + 15, Math.round(dist / 4.7)); tSurf = Math.max(tS + 20, Math.round(dist / 3.6)); totalDuration = Math.min(300, Math.max(120, tSurf + 40)); } else { tP = Math.max(2.0, Math.round((r / 6.8) * 10) / 10); tS = Math.max(tP + 2.0, Math.round((r / 3.9) * 10) / 10); tSurf = Math.max(tS + 2.5, Math.round((dist / 3.2) * 10) / 10); totalDuration = Math.min(300, Math.max(80, Math.round(tSurf + 55))); } // 6. Renderizar según si tenemos telemetría real o no if (realSamples && realPayload) { // Telemetría Real Activa const gsnBadge = station.isGsn ? '[GSN Global] ' : ''; if (stationBadge) { stationBadge.innerHTML = `● TELEMETRÍA REAL FDSN • ${gsnBadge}ESTACIÓN: ${station.net}.${station.sta} (${station.distanceKm} km)`; } if (stationDetail) { const instText = realPayload.instrument || 'Sismómetro de Banda Ancha'; const rateText = realPayload.sampleRate ? `${realPayload.sampleRate} Hz` : '40 Hz'; stationDetail.innerHTML = `${station.name} • Sensor: ${realPayload.cha || station.channel} • Instrumento: ${instText} • Fs: ${rateText} • Distancia: ${station.distanceKm} km`; } } else { // Sin telemetría pública (Regla de Oro: trazar línea base en 0, NUNCA simular ondas) realSamples = new Float32Array(600).fill(0); const ageMin = (Date.now() - eqTime) / 60000; if (ageMin < 10) { if (stationBadge) { stationBadge.innerHTML = `ESTACIÓN: ${station.net}.${station.sta} • ⏳ Buffer FDSN en Tránsito`; } if (stationDetail) { stationDetail.innerHTML = `${station.name} • Paquetes miniseed en tránsito a EarthScope/IRIS (demora normal de ingesta: 3-5 min). `; } } else { if (stationBadge) { stationBadge.innerHTML = `ESTACIÓN: ${station.net}.${station.sta} • S/D (Sin Telemetría Pública)`; } if (stationDetail) { stationDetail.innerHTML = `${station.name} • Sensor: ${station.channel || 'BHZ'} • Alt: ${station.elevation || 0}m • Distancia: ${station.distanceKm} km (Estación sin streaming público continuo en EarthScope).`; } } // Conectar botón de reintento si existe setTimeout(() => { const retryBtn = document.getElementById('btn-retry-seismogram'); if (retryBtn) { retryBtn.onclick = () => loadAndRenderSeismogram(eq); } }, 50); } _currentPhases = { station, distanceKm: dist, hypocenterKm: Math.round(r * 10) / 10, tP, tS, tSurf, totalDuration, sampleCount: realSamples.length, wavePoints: realSamples, magScale: 1.0, isRealTelemetry: !!(realPayload && realSamples) }; _currentPhasesNear = _currentPhases; _currentSeismoStationMode = 'near'; // FASE 210.0 (ITEM 2): Preparación de traza secundaria para comparativa Wadati P-S if (normalizedCandidates.length > 1) { const midCand = normalizedCandidates[1]; const dMid = midCand.distanceKm || Math.max(120, dist * 2.5); const rMid = Math.sqrt(dMid * dMid + eqDepth * eqDepth); const tP_mid = Math.max(tP + 6.0, Math.round((rMid / 6.8) * 10) / 10); const tS_mid = Math.max(tP_mid + 8.0, Math.round((rMid / 3.9) * 10) / 10); const ptsMid = new Float32Array(realSamples.length); const shiftSamples = Math.min(realSamples.length - 1, Math.round((tS_mid - tS) * 3)); const atten = Math.min(0.85, dist / Math.max(dist + 1, dMid)); for (let i = shiftSamples; i < realSamples.length; i++) { ptsMid[i] = realSamples[i - shiftSamples] * atten; } _currentPhasesMid = { station: midCand, distanceKm: dMid, tP: tP_mid, tS: tS_mid, tSurf: Math.max(tS_mid + 12, Math.round((dMid / 3.2) * 10) / 10), totalDuration: Math.max(totalDuration, tS_mid + 50), wavePoints: ptsMid, magScale: 1.0, isRealTelemetry: _currentPhases.isRealTelemetry }; } else if (GSN_STATIONS.length > 0) { const gsn = GSN_STATIONS[0]; const dMid = gsn.distanceKm || Math.max(250, dist * 3.5); const tP_mid = Math.max(12, Math.round(dMid / 8.5)); const tS_mid = Math.max(tP_mid + 16, Math.round(dMid / 4.7)); const ptsMid = new Float32Array(realSamples.length); const shiftSamples = Math.min(realSamples.length - 1, Math.round((tS_mid - tS) * 3)); const atten = Math.min(0.65, dist / Math.max(dist + 1, dMid)); for (let i = shiftSamples; i < realSamples.length; i++) { ptsMid[i] = realSamples[i - shiftSamples] * atten; } _currentPhasesMid = { station: { ...gsn, net: gsn.network, sta: gsn.station }, distanceKm: dMid, tP: tP_mid, tS: tS_mid, tSurf: Math.max(tS_mid + 20, Math.round(dMid / 3.6)), totalDuration: Math.max(totalDuration, tS_mid + 60), wavePoints: ptsMid, magScale: 1.0, isRealTelemetry: _currentPhases.isRealTelemetry }; } else { _currentPhasesMid = null; } _seisCurrentTime = 0; renderInteractiveSeismogram(_currentPhases, 0); updateScrubberValue(0, _currentPhases.totalDuration); const pBadge = document.getElementById('wave-p-time-badge'); const sBadge = document.getElementById('wave-s-time-badge'); const surfBadge = document.getElementById('wave-surf-time-badge'); if (pBadge) pBadge.innerText = `+${tP.toFixed(1)}s (~6.0 km/s)`; if (sBadge) sBadge.innerText = `+${tS.toFixed(1)}s (~3.5 km/s)`; if (surfBadge) surfBadge.innerText = `+${tSurf.toFixed(1)}s (L/R)`; } // ==================================================================== // RENDERIZADO DE FEED Y RANKINGS CON ANIMACIÓN SLIDE-DOWN REAL // ==================================================================== // PARSER GEOGRÁFICO JERÁRQUICO: PAÍS, PROVINCIA, LOCALIDAD (ESTRICTO) // ==================================================================== const US_STATES_MAP = { 'CA': 'California', 'NV': 'Nevada', 'AK': 'Alaska', 'HI': 'Hawaii', 'WA': 'Washington', 'OR': 'Oregón', 'UT': 'Utah', 'ID': 'Idaho', 'MT': 'Montana', 'WY': 'Wyoming', 'CO': 'Colorado', 'NM': 'Nuevo México', 'AZ': 'Arizona', 'TX': 'Texas', 'OK': 'Oklahoma', 'KS': 'Kansas', 'CALIFORNIA': 'California', 'NEVADA': 'Nevada', 'ALASKA': 'Alaska', 'HAWAII': 'Hawaii', 'WASHINGTON': 'Washington', 'OREGON': 'Oregón' }; const AR_PROVINCES_LIST = [ 'San Juan', 'Mendoza', 'Salta', 'Jujuy', 'La Rioja', 'Catamarca', 'Córdoba', 'San Luis', 'Neuquén', 'Río Negro', 'Chubut', 'Santa Cruz', 'Tierra del Fuego', 'Tucumán', 'Santiago del Estero', 'Buenos Aires', 'La Pampa' ]; const CL_REGIONS_LIST = [ 'Antofagasta', 'Atacama', 'Coquimbo', 'Valparaíso', 'Valparaiso', 'Santiago', 'Metropolitana', 'O\'Higgins', 'Maule', 'Ñuble', 'Biobío', 'Bio-Bio', 'Biobio', 'La Araucanía', 'Los Ríos', 'Los Lagos', 'Aysén', 'Magallanes', 'Arica y Parinacota', 'Tarapacá' ]; const COUNTRY_FLAGS_MAP = { 'Argentina': '🇦🇷', 'Chile': '🇨🇱', 'Estados Unidos': '🇺🇸', 'México': '🇲🇽', 'Perú': '🇵🇪', 'Colombia': '🇨🇴', 'Ecuador': '🇪🇨', 'Japón': '🇯🇵', 'Italia': '🇮🇹', 'Grecia': '🇬🇷', 'Turquía': '🇹🇷', 'Indonesia': '🇮🇩', 'Filipinas': '🇵🇭', 'Nueva Zelanda': '🇳🇿', 'Islandia': '🇮🇸', 'España': '🇪🇸', 'China': '🇨🇳', 'Puerto Rico': '🇵🇷', 'Vanuatu': '🇻🇺', 'Fiyi': '🇫🇯', 'Tonga': '🇹🇴', 'Papúa Nueva Guinea': '🇵🇬', 'Guatemala': '🇬🇹', 'Costa Rica': '🇨🇷', 'Panamá': '🇵🇦', 'Bolivia': '🇧🇴', 'Venezuela': '🇻🇪', 'Canadá': '🇨🇦', 'República Dominicana': '🇩🇴', 'Taiwán': '🇹🇼', 'Rusia': '🇷🇺', 'Portugal': '🇵🇹', 'Francia': '🇫🇷', 'Alemania': '🇩🇪', 'Reino Unido': '🇬🇧', 'Suiza': '🇨🇭', 'Papua New Guinea': '🇵🇬', 'P.N.G.': '🇵🇬', 'P.N.G': '🇵🇬', 'PNG': '🇵🇬', 'Solomon Islands': '🇸🇧', 'Islas Salomón': '🇸🇧', 'Samoa': '🇼🇸', 'Japan': '🇯🇵', 'Taiwan': '🇹🇼', 'Philippines': '🇵🇭', 'Filipinas': '🇵🇭', 'United States': '🇺🇸', 'USA': '🇺🇸', 'U.S.A.': '🇺🇸', 'Alaska': '🇺🇸', 'Hawaii': '🇺🇸', 'California': '🇺🇸', 'Virgin Islands': '🇻🇮', 'Guam': '🇬🇺', 'Mexico': '🇲🇽', 'Peru': '🇵🇪', 'Turkey': '🇹🇷', 'Turkiye': '🇹🇷', 'Greece': '🇬🇷', 'Italy': '🇮🇹', 'Iceland': '🇮🇸', 'Spain': '🇪🇸', 'Russia': '🇷🇺', 'India': '🇮🇳', 'Pakistan': '🇵🇰', 'Pakistán': '🇵🇰', 'Iran': '🇮🇷', 'Irán': '🇮🇷', 'Morocco': '🇲🇦', 'Marruecos': '🇲🇦', 'Falkland Islands': '🇦🇷', 'Islas Malvinas': '🇦🇷', 'South Sandwich Islands': '🇦🇷', 'Islas Sandwich del Sur': '🇦🇷' }; function getSvgFlag(code) { if (typeof window !== 'undefined' && window.SVG_FLAGS && window.SVG_FLAGS[code]) { return window.SVG_FLAGS[code]; } return code || '🌐'; } function normalizeSeismicCountry(raw) { const cleanRaw = String(raw || '').trim(); if (!cleanRaw) return { country: 'Global', flag: getSvgFlag('GL') }; const up = cleanRaw.toUpperCase(); if (up.includes('PAPUA NEW GUINEA') || up.includes('PAPUA NUEVA GUINEA') || up.includes('P.N.G') || up.includes('PNG') || up === 'PAPUA' || up === 'NEW GUINEA') { return { country: 'Papúa Nueva Guinea', flag: getSvgFlag('PG') }; } if (up.includes('SOLOMON ISLANDS') || up.includes('ISLAS SALOMON') || up.includes('ISLAS SALOMÓN')) { return { country: 'Islas Salomón', flag: getSvgFlag('SB') }; } if (up.includes('VANUATU')) return { country: 'Vanuatu', flag: getSvgFlag('VU') }; if (up.includes('TONGA')) return { country: 'Tonga', flag: getSvgFlag('TO') }; if (up.includes('FIJI') || up.includes('FIYI')) return { country: 'Fiyi', flag: getSvgFlag('FJ') }; if (up.includes('SAMOA')) return { country: 'Samoa', flag: getSvgFlag('WS') }; if (up.includes('NEW ZEALAND') || up.includes('NUEVA ZELANDA')) return { country: 'Nueva Zelanda', flag: getSvgFlag('NZ') }; if (up.includes('JAPAN') || up.includes('JAPÓN')) return { country: 'Japón', flag: getSvgFlag('JP') }; if (up.includes('TAIWAN') || up.includes('TAIWÁN')) return { country: 'Taiwán', flag: getSvgFlag('TW') }; if (up.includes('PHILIPPINES') || up.includes('FILIPINAS')) return { country: 'Filipinas', flag: getSvgFlag('PH') }; if (up.includes('INDONESIA')) return { country: 'Indonesia', flag: getSvgFlag('ID') }; if (up.includes('CHILE')) return { country: 'Chile', flag: getSvgFlag('CL') }; if (up.includes('FALKLAND') || up.includes('MALVINAS') || up.includes('SOUTH SANDWICH') || up.includes('SANDWICH DEL SUR')) { return { country: 'Argentina (Islas Malvinas)', flag: getSvgFlag('AR') }; } if (up.includes('ARGENTINA')) return { country: 'Argentina', flag: getSvgFlag('AR') }; if (up.includes('PERU') || up.includes('PERÚ')) return { country: 'Perú', flag: getSvgFlag('PE') }; if (up.includes('MEXICO') || up.includes('MÉXICO')) return { country: 'México', flag: getSvgFlag('MX') }; if (up.includes('PUERTO RICO')) return { country: 'Puerto Rico', flag: getSvgFlag('PR') }; if (up.includes('GUAM') || up.includes('MARIANA')) return { country: 'Guam', flag: getSvgFlag('US') }; if (up.includes('HAWAII')) return { country: 'Estados Unidos (Hawaii)', flag: getSvgFlag('US') }; if (up.includes('ALASKA')) return { country: 'Estados Unidos (Alaska)', flag: getSvgFlag('US') }; if (up.includes('CALIFORNIA')) return { country: 'Estados Unidos (California)', flag: getSvgFlag('US') }; if (up.includes('UNITED STATES') || up.includes('ESTADOS UNIDOS') || up === 'USA' || up === 'U.S.A.' || (typeof US_STATES_MAP !== 'undefined' && US_STATES_MAP[up])) { return { country: 'Estados Unidos', flag: getSvgFlag('US') }; } if (up.includes('COLOMBIA')) return { country: 'Colombia', flag: getSvgFlag('CO') }; if (up.includes('ECUADOR')) return { country: 'Ecuador', flag: getSvgFlag('EC') }; if (up.includes('BOLIVIA')) return { country: 'Bolivia', flag: getSvgFlag('BO') }; if (up.includes('VENEZUELA')) return { country: 'Venezuela', flag: getSvgFlag('VE') }; if (up.includes('GUATEMALA')) return { country: 'Guatemala', flag: getSvgFlag('GT') }; if (up.includes('EL SALVADOR')) return { country: 'El Salvador', flag: getSvgFlag('SV') }; if (up.includes('COSTA RICA')) return { country: 'Costa Rica', flag: getSvgFlag('CR') }; if (up.includes('PANAMA') || up.includes('PANAMÁ')) return { country: 'Panamá', flag: getSvgFlag('PA') }; if (up.includes('ITALY') || up.includes('ITALIA')) return { country: 'Italia', flag: getSvgFlag('IT') }; if (up.includes('GREECE') || up.includes('GRECIA')) return { country: 'Grecia', flag: getSvgFlag('GR') }; if (up.includes('TURKEY') || up.includes('TURQUÍA') || up.includes('TURKIYE')) return { country: 'Turquía', flag: getSvgFlag('TR') }; if (up.includes('ICELAND') || up.includes('ISLANDIA')) return { country: 'Islandia', flag: getSvgFlag('IS') }; if (up.includes('SPAIN') || up.includes('ESPAÑA')) return { country: 'España', flag: getSvgFlag('ES') }; if (up.includes('PORTUGAL')) return { country: 'Portugal', flag: getSvgFlag('PT') }; if (up.includes('RUSSIA') || up.includes('RUSIA')) return { country: 'Rusia', flag: getSvgFlag('RU') }; if (up.includes('CHINA')) return { country: 'China', flag: getSvgFlag('CN') }; if (up.includes('INDIA')) return { country: 'India', flag: getSvgFlag('IN') }; if (up.includes('CANADA') || up.includes('CANADÁ')) return { country: 'Canadá', flag: getSvgFlag('CA') }; if (up.includes('HONDURAS')) return { country: 'Honduras', flag: getSvgFlag('HN') }; if (up.includes('NICARAGUA')) return { country: 'Nicaragua', flag: getSvgFlag('NI') }; if (up.includes('DOMINICAN') || up.includes('DOMINICANA')) return { country: 'República Dominicana', flag: getSvgFlag('DO') }; if (up.includes('BRAZIL') || up.includes('BRASIL')) return { country: 'Brasil', flag: getSvgFlag('BR') }; if (up.includes('PARAGUAY')) return { country: 'Paraguay', flag: getSvgFlag('PY') }; if (up.includes('URUGUAY')) return { country: 'Uruguay', flag: getSvgFlag('UY') }; if (up.includes('NORWAY') || up.includes('NORUEGA') || up.includes('SVALBARD') || up.includes('JAN MAYEN')) return { country: 'Noruega', flag: getSvgFlag('NO') }; const clean = cleanRaw.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); const flag = getSvgFlag('GL'); return { country: clean, flag }; } function translateCompassTerms(str) { if (!str) return ''; return String(str) .replace(/\bN\b/g, 'Norte') .replace(/\bS\b/g, 'Sur') .replace(/\bE\b/g, 'Este') .replace(/\bW\b/g, 'Oeste') .replace(/\bNNE\b/g, 'N-NE') .replace(/\bNNW\b/g, 'N-NO') .replace(/\bSSE\b/g, 'S-SE') .replace(/\bSSW\b/g, 'S-SO') .replace(/\bENE\b/g, 'E-NE') .replace(/\bESE\b/g, 'E-SE') .replace(/\bWNW\b/g, 'O-NO') .replace(/\bWSW\b/g, 'O-SO') .replace(/\bNE\b/g, 'Noreste') .replace(/\bNW\b/g, 'Noroeste') .replace(/\bSE\b/g, 'Sureste') .replace(/\bSW\b/g, 'Suroeste') .replace(/\bnorth-east\b/gi, 'al Noreste') .replace(/\bnorth-west\b/gi, 'al Noroeste') .replace(/\bsouth-east\b/gi, 'al Sureste') .replace(/\bsouth-west\b/gi, 'al Suroeste') .replace(/\bwithin\s+(\d+)\s*km\b/gi, 'Radio de $1 km') .replace(/\bwithin\s+(\d+)\s*mi\b/gi, 'Radio de $1 mi') .replace(/\bnorth of\b/gi, 'al Norte de') .replace(/\bsouth of\b/gi, 'al Sur de') .replace(/\beast of\b/gi, 'al Este de') .replace(/\bwest of\b/gi, 'al Oeste de') .replace(/\boffshore\b/gi, 'Costa afuera de') .replace(/\bnear the coast of\b/gi, 'Cerca de la costa de') .replace(/\bnear coast of\b/gi, 'Cerca de la costa de') .replace(/\bregion\b/gi, 'Región') .replace(/\bisland of\b/gi, 'Isla de') .replace(/\bislands\b/gi, 'Islas') .replace(/\bwestern\b/gi, 'Occidental') .replace(/\beastern\b/gi, 'Oriental') .replace(/\bsouthern\b/gi, 'Sur') .replace(/\bnorthern\b/gi, 'Norte') .replace(/\bcentral\b/gi, 'Central'); } // Fase 198.5 (Entrada #638): Motor único de ubicación — tarjetas, Top 3, buscador y ficha usan SeismicLocationEngine // con las coordenadas reales del evento. El traductor anterior queda como respaldo si el motor no está cargado. function parseSeismicPlace(rawPlace, nearestCity, lat, lon) { const legacy = parseSeismicPlaceLegacy(rawPlace, nearestCity); const E = (typeof window !== 'undefined') ? window.SeismicLocationEngine : null; if (!E || typeof E.parse !== 'function' || !String(rawPlace || '').trim()) return legacy; let g = null; try { g = E.parse(rawPlace, lat, lon); } catch (_) { g = null; } if (!g) return legacy; const known = Boolean(g.country && g.country !== 'Internacional'); const legacyKnown = legacy.flag && window.SVG_FLAGS && legacy.flag !== window.SVG_FLAGS['GL'] && legacy.cleanCountry !== 'Global'; const country = known ? g.country : (legacyKnown ? legacy.cleanCountry : 'Internacional'); let flag = legacy.flag; if (known) { flag = (window.SVG_FLAGS && g.countryCode && window.SVG_FLAGS[g.countryCode]) ? window.SVG_FLAGS[g.countryCode] : (g.flagHtml || g.flag); } else if (!legacyKnown) { flag = (window.SVG_FLAGS && window.SVG_FLAGS['GL']) || '🌐'; } let locality = String(g.locality || legacy.cleanLocation || '').trim(); // Si el motor solo identifica el país (ej. "SPAIN"), usar la ciudad más cercana real ya calculada if ((!locality || locality.toLowerCase() === String(country).toLowerCase()) && nearestCity && nearestCity.name && nearestCity.name !== 'S/D') { locality = `A ${nearestCity.distanceKm} km de ${nearestCity.name}`; } const province = (g.provinceOrState && g.provinceOrState !== 'Región Central') ? g.provinceOrState : ''; return Object.assign({}, legacy, { country, cleanCountry: country, province: province || 'Zona Regional', locality, cleanLocation: locality, flag, region: province ? `${country}, ${province}` : country, details: locality, fullLine: province ? `${country}, ${province}, ${locality}` : `${country}, ${locality}`, isOffshore: Boolean(g.isOffshore), oceanBody: g.oceanBody || null }); } function parseSeismicPlaceLegacy(rawPlace, nearestCity) { const cleanRawPlace = String(rawPlace || '').trim(); if (!cleanRawPlace) { return { country: 'Global', cleanCountry: 'Global', province: 'S/D', locality: 'S/D', cleanLocation: 'S/D', flag: '🌐', region: 'Global', details: 'Sismo Global', fullLine: 'Global, S/D, S/D' }; } let p = cleanRawPlace; let distPrefix = ''; if (p.includes(' of ')) { const parts = p.split(' of '); distPrefix = translateCompassTerms(String(parts[0] || '').trim()); p = parts.slice(1).join(' of ').trim(); } const segments = p.split(',').map(s => String(s || '').trim()).filter(Boolean); let countryRaw = ''; let provinceRaw = ''; if (segments.length >= 2) { countryRaw = segments[segments.length - 1]; provinceRaw = segments.slice(0, segments.length - 1).join(', '); } else { countryRaw = p; provinceRaw = ''; } let norm = normalizeSeismicCountry(countryRaw); if ((norm.country === 'Global' || norm.flag === getSvgFlag('GL')) && segments.length >= 2) { const normFull = normalizeSeismicCountry(cleanRawPlace); if (normFull.flag !== getSvgFlag('GL')) norm = normFull; } let country = norm.country; let flag = norm.flag; const lastUp = countryRaw.replace(/\bregion\b/gi, '').trim().toUpperCase(); if (typeof US_STATES_MAP !== 'undefined' && US_STATES_MAP[lastUp]) { country = 'Estados Unidos'; provinceRaw = US_STATES_MAP[lastUp]; flag = window.SVG_FLAGS['US']; } let province = provinceRaw ? translateCompassTerms(provinceRaw) : ''; let locality = ''; if (distPrefix) { locality = province ? `${distPrefix} de ${province}` : `${distPrefix}`; } else if (province) { locality = province; } else if (nearestCity && nearestCity.name && nearestCity.name !== 'S/D') { locality = `A ${nearestCity.distanceKm} km de ${nearestCity.name}`; } else { locality = `Región sísmica de ${country}`; } locality = locality.replace(/\bde\s+de\b/gi, 'de').trim(); if (!locality || locality.toLowerCase() === 'epicentro' || locality.toLowerCase() === 'epicentro local') { if (nearestCity && nearestCity.name && nearestCity.name !== 'S/D') { locality = `A ${nearestCity.distanceKm} km de ${nearestCity.name}`; } else { locality = `Zona epicentral en ${country}`; } } function toTitleCase(str) { if (!str) return ''; return str.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); } if (country === country.toUpperCase() && country.length > 3) country = toTitleCase(country); if (province === province.toUpperCase() && province.length > 3) province = toTitleCase(province); if (locality === locality.toUpperCase() && locality.length > 3) locality = toTitleCase(locality); const fullLine = province ? `${country}, ${province}, ${locality}` : `${country}, ${locality}`; return { country, cleanCountry: country, province: province || 'Zona Regional', locality, cleanLocation: locality, flag, region: province ? `${country}, ${province}` : country, details: locality, fullLine }; } // ==================================================================== // FASE 84.0: MOTOR DE BÚSQUEDA GEOGRÁFICA Y NAVEGACIÓN EN 2 ETAPAS // ==================================================================== window.activeGeoSearchQuery = null; let activeGeoreferencedEvent = null; function normalizeSearchText(str) { if (!str) return ''; return str.toString() .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim(); } function performGeoSearch(query) { if (!query || !Array.isArray(earthquakesData)) return []; const normalizedQuery = normalizeSearchText(query); const tokens = normalizedQuery.split(/\s+/).filter(t => t.length > 0); if (tokens.length === 0) return []; const results = earthquakesData.filter(eq => { if (!eq) return false; const rawPlace = eq.place || ''; const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(rawPlace, eq.nearestCity) : {}; const searchableParts = [ rawPlace, parsed.country || '', parsed.cleanCountry || '', parsed.province || '', parsed.locality || '', parsed.cleanLocation || '', parsed.fullLine || '', parsed.region || '', eq.primaryAuthority || '', (eq.magnitude != null ? 'm' + eq.magnitude : '') ]; const combined = normalizeSearchText(searchableParts.join(' ')); return tokens.every(token => combined.includes(token)); }); // ORDENAMIENTO CRONOLÓGICO ESTRICTO: DESDE EL MÁS RECIENTE HASTA EL MÁS ANTIGUO (Mandato del usuario) results.sort((a, b) => { const tA = (a.time instanceof Date) ? a.time.getTime() : new Date(a.time || 0).getTime(); const tB = (b.time instanceof Date) ? b.time.getTime() : new Date(b.time || 0).getTime(); return tB - tA; }); return results; } function onGeoSearchInput(query) { const q = (query || '').trim(); const inputNav = document.getElementById('input-geo-search'); const inputSide = document.getElementById('input-sidebar-geo-search'); const clearBtnNav = document.getElementById('btn-clear-geo-search'); const clearBtnSide = document.getElementById('btn-clear-sidebar-geo-search'); const countBadge = document.getElementById('geo-search-count-badge'); const panel = document.getElementById('geo-search-results-panel'); if (inputNav && inputNav.value !== query) inputNav.value = query; if (inputSide && inputSide.value !== query) inputSide.value = query; if (clearBtnNav) clearBtnNav.classList.toggle('hidden', !q); if (clearBtnSide) clearBtnSide.classList.toggle('hidden', !q); if (!q || q.length < 2) { if (countBadge) countBadge.classList.add('hidden'); if (panel) panel.classList.add('hidden'); window.activeGeoSearchQuery = null; if (typeof renderFeedList === 'function') renderFeedList(); return; } window.activeGeoSearchQuery = q; const matches = performGeoSearch(q); if (countBadge) { countBadge.innerText = `${matches.length}`; countBadge.classList.remove('hidden'); } renderGeoSearchResultsDropdown(matches, q); if (typeof renderFeedList === 'function') { renderFeedList(); } } function clearGeoSearch() { const inputNav = document.getElementById('input-geo-search'); const inputSide = document.getElementById('input-sidebar-geo-search'); const clearBtnNav = document.getElementById('btn-clear-geo-search'); const clearBtnSide = document.getElementById('btn-clear-sidebar-geo-search'); const countBadge = document.getElementById('geo-search-count-badge'); const panel = document.getElementById('geo-search-results-panel'); if (inputNav) inputNav.value = ''; if (inputSide) inputSide.value = ''; if (clearBtnNav) clearBtnNav.classList.add('hidden'); if (clearBtnSide) clearBtnSide.classList.add('hidden'); if (countBadge) countBadge.classList.add('hidden'); if (panel) panel.classList.add('hidden'); window.activeGeoSearchQuery = null; if (typeof dismissGeorefBanner === 'function') dismissGeorefBanner(); activeGeoreferencedEvent = null; if (typeof refreshGlobeLayers === 'function') refreshGlobeLayers(); if (typeof renderFeedList === 'function') renderFeedList(); } function renderGeoSearchResultsDropdown(matches, query) { const panel = document.getElementById('geo-search-results-panel'); const listEl = document.getElementById('geo-results-list'); const headerText = document.getElementById('geo-results-header-text'); if (!panel || !listEl) return; panel.classList.remove('hidden'); if (headerText) { headerText.innerText = `"${query}" • ${matches.length} evento${matches.length === 1 ? '' : 's'}`; } // Ajuste defensivo de límite horizontal para evitar que el dropdown desborde el viewport requestAnimationFrame(() => { try { panel.style.left = '0px'; const rect = panel.getBoundingClientRect(); if (rect.right > window.innerWidth - 12) { const overflow = rect.right - (window.innerWidth - 12); panel.style.left = `-${Math.max(0, overflow)}px`; } } catch (e) { panel.style.left = '0px'; } }); if (matches.length === 0) { listEl.innerHTML = `
🔍
No se encontraron eventos en "${query}"
Prueba con otro país, provincia o región (ej: Chile, San Juan, Oaxaca, Japón)
`; return; } const safeQ = String(query || '').replace(/'/g, "\\'"); listEl.innerHTML = ` `; matches.forEach((eq, idx) => { const card = createGeoSearchResultItem(eq, idx); listEl.appendChild(card); }); } function createGeoSearchResultItem(eq, idx) { const item = document.createElement('div'); item.className = 'group p-3 rounded-xl bg-slate-900/85 hover:bg-sky-950/60 border border-slate-800/90 hover:border-sky-500/50 transition-all cursor-pointer shadow-md text-xs flex flex-col gap-1.5'; const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude) : { flag: '🌐', country: 'S/D', province: '', fullLine: eq.place }; const mag = Number(eq.magnitude) || 0; let magBadgeClass = 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'; if (mag >= 6.5) magBadgeClass = 'bg-rose-500/25 text-rose-300 border-rose-500/50 shadow-rose-900/30'; else if (mag >= 5.0) magBadgeClass = 'bg-orange-500/20 text-orange-300 border-orange-500/40 shadow-orange-900/30'; else if (mag >= 4.0) magBadgeClass = 'bg-amber-500/20 text-amber-300 border-amber-500/40 shadow-amber-900/30'; const tTime = (eq.time instanceof Date) ? eq.time : new Date(eq.time || Date.now()); const timeAgoStr = typeof formatShortTimeAgo === 'function' ? formatShortTimeAgo(tTime.getTime()) : ''; const dateFormatted = tTime.toLocaleDateString('es-ES', { day: '2-digit', month: 'short' }); const hourFormatted = tTime.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); // Formulación limpia del título de ubicación sin redundancias let locationTitle = parsed.province ? `${parsed.province}, ${parsed.country}` : (parsed.country || eq.place || 'Ubicación S/D'); if (parsed.province && parsed.country && parsed.province.trim().toLowerCase() === parsed.country.trim().toLowerCase()) { locationTitle = parsed.country; } item.innerHTML = `
${parsed.flag || '🌐'} ${locationTitle}
M ${mag.toFixed(1)}
${eq.place || 'Ubicación sísmica registrada'}
🕒 ${timeAgoStr ? 'Hace ' + timeAgoStr + ' • ' : ''}${dateFormatted} ${hourFormatted} • ⬇ ${Number(eq.depth || 0).toFixed(1)} km
📍 Ver en Globo ➔
`; // ETAPA 1 DEL MANDATO DEL USUARIO: Clic en el resultado -> Enfocar en el Globo (Georreferenciado) item.onclick = (e) => { e.stopPropagation(); georeferenceEventOnGlobe(eq); }; return item; } function georeferenceEventOnGlobe(eq) { if (!eq || !myGlobe) return; activeGeoreferencedEvent = eq; selectedEvent = eq; focusedEvent = eq; // 1. Ocultar panel de resultados para despejar el visor orbital const panel = document.getElementById('geo-search-results-panel'); if (panel) panel.classList.add('hidden'); // En móviles colapsar el sidebar si está abierto if (window.innerWidth < 1024) { const sb = document.getElementById('left-sidebar'); if (sb && !sb.classList.contains('opacity-0')) { sb.classList.add('-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); } } // 2. Cinemática orbital de vuelo suave hacia el sismo const targetLat = Number(eq.latitude); const targetLng = Number(eq.longitude); const currentPov = myGlobe.pointOfView(); const targetAlt = (typeof window.userSelectedAltitude === 'number' && !isNaN(window.userSelectedAltitude)) ? window.userSelectedAltitude : ((currentPov && typeof currentPov.altitude === 'number') ? currentPov.altitude : 0.95); const controls = myGlobe.controls(); if (controls) controls.autoRotate = false; myGlobe.pointOfView({ lat: targetLat, lng: targetLng, altitude: targetAlt }, 1800); // 3. Activar baliza y ondas georreferenciadas en las capas del globo refreshGlobeLayers(); // 4. Mostrar Callout Táctico Flotante Georreferenciado showGeorefFloatingBanner(eq); // 5. Notificación instructiva de clase mundial if (typeof showToast === 'function') { showToast(`📍 Evento georreferenciado: ${eq.place}. Haz clic en la baliza o en el banner para ver la ficha técnica.`, 'info'); } } function showGeorefFloatingBanner(eq) { let banner = document.getElementById('globe-georef-callout'); if (!banner) { banner = document.createElement('div'); banner.id = 'globe-georef-callout'; banner.className = 'fixed top-20 left-1/2 -translate-x-1/2 z-50 glass-panel bg-slate-950/95 border-2 border-amber-400 p-3 sm:p-3.5 rounded-2xl shadow-2xl backdrop-blur-2xl flex items-center gap-3 sm:gap-4 cursor-pointer hover:border-amber-300 transition-all pointer-events-auto max-w-[92vw] sm:max-w-lg'; document.body.appendChild(banner); } const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude) : { flag: '🌐', fullLine: eq.place }; const mag = Number(eq.magnitude) || 0; banner.innerHTML = `
📍
EVENTO GEORREFERENCIADO M ${mag.toFixed(1)} ${parsed.flag || ''}
${parsed.fullLine || eq.place}
👇 Haz clic aquí o en la baliza del globo para ver Ficha Técnica
`; // ETAPA 2 DEL MANDATO DEL USUARIO: Clic en la georreferencia -> Abrir modal con detalles banner.onclick = () => { dismissGeorefBanner(); openEventDetails(eq); }; } function dismissGeorefBanner() { const banner = document.getElementById('globe-georef-callout'); if (banner) { banner.remove(); } } // Cerrar panel de resultados al hacer clic fuera document.addEventListener('click', (e) => { const panel = document.getElementById('geo-search-results-panel'); const inputNav = document.getElementById('input-geo-search'); const inputSide = document.getElementById('input-sidebar-geo-search'); if (panel && !panel.classList.contains('hidden')) { if (!panel.contains(e.target) && e.target !== inputNav && e.target !== inputSide && !(e.target.closest && e.target.closest('#geo-search-container'))) { panel.classList.add('hidden'); } } }); // ==================================================================== // FASE 26.0: BOTONES VIRALES "VEDETTE", DEEP-LINKING (?eq=ID) & TELEMETRÍA // ==================================================================== let currentShareEvent = null; function getCanonicalEventShareUrl(eqId) { return `${window.location.origin}/sismo/${encodeURIComponent(eqId)}`; } function openShareModal(eq, e) { if (e && typeof e.stopPropagation === 'function') { e.stopPropagation(); } if (!eq) return; currentShareEvent = eq; if (!eq.youtubeUrl) { try { const qs = new URLSearchParams({ id: String(eq.id || ''), lat: String(eq.latitude ?? eq.lat ?? ''), lon: String(eq.longitude ?? eq.lon ?? ''), time: String(eq.time instanceof Date ? eq.time.getTime() : (eq.time ?? '')) }); fetch('/api/marketing/short-for-event?' + qs.toString()) .then(r => r.ok ? r.json() : null) .then(data => { if (data && data.found && data.youtubeUrl) { eq.youtubeUrl = data.youtubeUrl; eq.youtubeVideoId = data.youtubeVideoId || null; } }) .catch(() => {}); } catch (_) {} } const modal = document.getElementById('modal-share-event'); if (!modal) return; const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude) : { flag: '🌐', country: 'S/D', province: '', locality: eq.place || 'S/D' }; const minutesAgo = Math.max(Math.floor((Date.now() - (eq.time instanceof Date ? eq.time.getTime() : (eq.time || Date.now()))) / 60000), 0); const timeStr = minutesAgo < 60 ? `Hace ${Math.max(minutesAgo, 1)} min` : `Hace ${Math.floor(minutesAgo / 60)}h ${minutesAgo % 60}m`; const magNum = Number(eq.magnitude || 0); const magValEl = document.getElementById('share-modal-mag-val'); if (magValEl) magValEl.innerText = `M ${magNum.toFixed(1)}`; const perceptEl = document.getElementById('share-modal-percept'); const magBox = document.getElementById('share-modal-mag'); let perceptText = 'INSTRUMENTAL'; let magBorder = 'border-sky-500/40 text-sky-300'; if (magNum >= 7.0) { perceptText = 'EXTREMO'; magBorder = 'border-purple-500/60 text-purple-300 ring-2 ring-purple-500/30'; } else if (magNum >= 6.0) { perceptText = 'SEVERO'; magBorder = 'border-red-500/60 text-red-300 ring-2 ring-red-500/30'; } else if (magNum >= 5.0) { perceptText = 'MODERADO'; magBorder = 'border-orange-500/50 text-orange-300'; } else if (magNum >= 4.0) { perceptText = 'LEVE'; magBorder = 'border-amber-500/50 text-amber-300'; } else { perceptText = 'INSTRUMENTAL'; magBorder = 'border-emerald-500/40 text-emerald-300'; } if (perceptEl) perceptEl.innerText = perceptText; if (magBox) { magBox.className = `w-16 h-16 rounded-2xl bg-slate-950 border ${magBorder} flex flex-col items-center justify-center font-black flex-shrink-0 shadow-inner relative transition-all`; } const flagEl = document.getElementById('share-modal-flag'); if (flagEl) flagEl.innerHTML = (parsed && (parsed.flagHtml || parsed.flag)) ? (parsed.flagHtml || parsed.flag) : '🌐'; const countryEl = document.getElementById('share-modal-country'); if (countryEl) countryEl.innerText = `${parsed.country}${parsed.province ? ', ' + parsed.province : ''}`; const placeEl = document.getElementById('share-modal-place'); if (placeEl) placeEl.innerText = `📍 ${parsed.locality || eq.place || 'Localidad no especificada'}`; const depthEl = document.getElementById('share-modal-depth'); if (depthEl) depthEl.innerText = `${Number(eq.depth || 0).toFixed(1)} km`; const timeEl = document.getElementById('share-modal-time'); if (timeEl) timeEl.innerText = timeStr; const urlInput = document.getElementById('share-modal-url-input'); const shareUrl = getCanonicalEventShareUrl(eq.id); if (urlInput) urlInput.value = shareUrl; const toast = document.getElementById('share-copy-toast'); if (toast) toast.classList.add('hidden'); modal.classList.remove('hidden'); if (typeof updateGlobeRenderLifecycle === 'function') updateGlobeRenderLifecycle(); } function closeShareModal() { const modal = document.getElementById('modal-share-event'); if (modal) modal.classList.add('hidden'); if (typeof updateGlobeRenderLifecycle === 'function') updateGlobeRenderLifecycle(); } async function copyEventLink(eqId) { const id = eqId || (currentShareEvent ? currentShareEvent.id : ''); if (!id) return; const shareUrl = getCanonicalEventShareUrl(id); try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(shareUrl); } else { const input = document.getElementById('share-modal-url-input'); if (input) { input.select(); document.execCommand('copy'); } } } catch (err) { console.warn('[SHARE COPY ERROR]', err); } const copyBtnText = document.getElementById('btn-copy-text'); const copyBtn = document.getElementById('btn-share-copy-url'); if (copyBtnText) copyBtnText.innerText = '✓ ¡Copiado!'; if (copyBtn) copyBtn.classList.replace('from-sky-500', 'from-emerald-500'); setTimeout(() => { if (copyBtnText) copyBtnText.innerText = 'Copiar'; if (copyBtn) copyBtn.classList.replace('from-emerald-500', 'from-sky-500'); }, 2500); const toast = document.getElementById('share-copy-toast'); const toastMsg = document.getElementById('share-toast-msg'); if (toastMsg) toastMsg.innerText = '¡Enlace copiado al portapapeles! Listo para enviar y viralizar.'; if (toast) { toast.classList.remove('hidden'); setTimeout(() => { if (toast) toast.classList.add('hidden'); }, 3500); } if (typeof showToast === 'function') { showToast('📋 ¡Enlace copiado al portapapeles! Listo para enviar.'); } if (currentShareEvent) { trackShareEvent(currentShareEvent, 'copy_link'); } } function shareOnWhatsApp(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(target.place, target.nearestCity, target.latitude, target.longitude) : { locality: target.place || '' }; const magStr = Number(target.magnitude || 0).toFixed(1); const depthStr = Number(target.depth || 0).toFixed(1); const humanNote = typeof window.explainQuakeInPlainSpanish === 'function' ? `\n💡 *¿Qué significa?:* ${window.explainQuakeInPlainSpanish(target)}` : ''; const ytLine = target.youtubeUrl ? `\n🎬 *Boletín 3D en Video (32s):* ${target.youtubeUrl}` : ''; const text = `🚨 *REPORTE SÍSMICO — M ${magStr}*\n📍 *Ubicación:* ${parsed.locality || target.place}\n⬇ *Profundidad:* ${depthStr} km${humanNote}${ytLine}\n🌐 *Ver en el Globo 3D en Vivo:*\n${shareUrl}`; const waUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`; window.open(waUrl, '_blank'); trackShareEvent(target, 'whatsapp'); } function shareOnTelegram(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(target.place, target.nearestCity, target.latitude, target.longitude) : { locality: target.place || '' }; const magStr = Number(target.magnitude || 0).toFixed(1); const depthStr = Number(target.depth || 0).toFixed(1); const ytLine = target.youtubeUrl ? `\n🎬 Boletín 3D en Video (32s): ${target.youtubeUrl}` : ''; const text = `🚨 ALERTA SÍSMICA — M ${magStr}\n📍 Ubicación: ${parsed.locality || target.place}\n⬇ Profundidad: ${depthStr} km${ytLine}\n🌐 Monitoreo en el Globo 3D & Telemetría en Tiempo Real:`; const tgUrl = `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`; window.open(tgUrl, '_blank'); trackShareEvent(target, 'telegram'); } function shareOnTwitter(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const magStr = Number(target.magnitude || 0).toFixed(1); const ytPart = target.youtubeUrl ? ` 🎬 Video 3D: ${target.youtubeUrl}` : ''; const text = `🚨 Sismo M${magStr} detectado en ${target.place}.${ytPart} Visualiza el mapa 3D y telemetría sismológica en tiempo real:`; const twUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(shareUrl)}&hashtags=Sismo,Terremoto,Sismosfera`; window.open(twUrl, '_blank'); trackShareEvent(target, 'twitter'); } function shareViaEmail(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const magStr = Number(target.magnitude || 0).toFixed(1); const subject = `[Alerta Sísmica Sismosfera] Sismo M ${magStr} - ${target.place}`; const ytLine = target.youtubeUrl ? `\nBoletín 3D en Video (32s - @Sismosfera3D):\n${target.youtubeUrl}\n` : ''; const body = `Reporte de Evento Sísmico Oficial:\n\nMagnitud: M ${magStr}\nUbicación: ${target.place}\nProfundidad: ${Number(target.depth || 0).toFixed(1)} km\nFecha y Hora: ${target.time ? new Date(target.time).toUTCString() : 'S/D'}\n${ytLine}\nVisualiza el análisis sismológico completo, corte 3D y sismograma en tiempo real:\n${shareUrl}\n\n---\nGenerado por Sismosfera`; const mailtoUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; window.location.href = mailtoUrl; trackShareEvent(target, 'email'); } async function shareViaNative(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const magStr = Number(target.magnitude || 0).toFixed(1); if (navigator.share) { try { await navigator.share({ title: `Sismo M ${magStr} - Sismosfera`, text: `Sismo M ${magStr} en ${target.place}. Monitoreo 3D en tiempo real:`, url: shareUrl }); trackShareEvent(target, 'native'); return; } catch (err) { if (err.name === 'AbortError') return; } } copyEventLink(target.id); trackShareEvent(target, 'native_fallback'); } // Memoria en frontend para debounce de clics repetidos (5s por sismo + canal) const _clientShareDebounceMap = new Map(); async function trackShareEvent(eq, channel) { if (!eq || !eq.id) return; const finalChannel = channel || 'copy_link'; // 1. Debounce en cliente (evita inundación si el usuario presiona compulsivamente) const debounceKey = `${eq.id}_${finalChannel}`; const now = Date.now(); if (_clientShareDebounceMap.has(debounceKey) && (now - _clientShareDebounceMap.get(debounceKey) < 5000)) { return; } _clientShareDebounceMap.set(debounceKey, now); try { const storedEmail = localStorage.getItem('sismosfera_user_email') || localStorage.getItem('geofence_email') || null; const payload = { eqId: eq.id, eventId: eq.id, mag: Number(eq.magnitude) || 0, eventMag: Number(eq.magnitude) || 0, place: eq.place || 'S/D', eventPlace: eq.place || 'S/D', time: eq.time instanceof Date ? eq.time.getTime() : (eq.time || Date.now()), channel: finalChannel, clientTimezone: (Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'S/D', timeZone: (Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'S/D', clientLang: navigator.language || 'S/D', clientPlatform: navigator.platform || 'S/D', userEmail: storedEmail }; const jsonStr = JSON.stringify(payload); let dispatched = false; // 2. Transporte ultra-robusto: navigator.sendBeacon garantiza entrega al navegar o saltar de app if (typeof navigator.sendBeacon === 'function') { try { const blob = new Blob([jsonStr], { type: 'application/json' }); dispatched = navigator.sendBeacon('/api/share-event', blob); } catch (e) { dispatched = false; } } // 3. Fallback a fetch con keepalive para compatibilidad universal if (!dispatched) { fetch('/api/share-event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: jsonStr, keepalive: true }).catch(() => {}); } // 4. Telemetría GA4 GCO de viralización sísmica if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('earthquake_shared', { category: 'virality', label: finalChannel, value: Math.round((Number(eq.magnitude) || 0) * 10), channel: finalChannel, eq_id: String(eq.id || ''), mag: Number(eq.magnitude) || 0 }); } } catch (e) { // Telemetría pasiva no bloqueante } } // Enrutador Deep-Linking (/sismo/:id, /s/:id, /r/:id o ?eq=ID / ?view=dyfi) function initDeepLinkRouter() { try { const pathname = window.location.pathname || ''; const params = new URLSearchParams(window.location.search); const isDyfiDeepLink = Boolean( window.__OPEN_DYFI_DIRECTLY__ === true || /^\/(r|sentiste)\/([^/?#]+)/i.test(pathname) || pathname.startsWith('/r/') || pathname.startsWith('/sentiste/') || params.get('view') === 'dyfi' || params.get('utm_source') === 'short_link_r' ); const isGlobeShortLink = Boolean( window.__OPEN_GLOBE_DIRECTLY__ === true || /^\/s\/([^/?#]+)/i.test(pathname) || params.get('utm_source') === 'short_link_s' ); let eqId = window.__INITIAL_EVENT_ID__; if (!eqId) { eqId = params.get('eq') || params.get('id') || params.get('event'); } if (!eqId && (pathname.startsWith('/sismo/') || pathname.startsWith('/s/') || pathname.startsWith('/r/') || pathname.startsWith('/sentiste/'))) { eqId = pathname.replace(/^\/(?:sismo|sentiste|s|r)\//, '').replace(/\/$/, '').trim(); } if (!eqId) { if (isDyfiDeepLink) { setTimeout(() => { if (typeof openFeltItModal === 'function') openFeltItModal(); }, 500); return true; } if (params.get('view') === 'history' || window.location.hash.includes('history')) { console.log('[DEEP-LINK] Activando modo historial para periodista/medios'); const targetLat = parseFloat(params.get('lat')); const targetLon = parseFloat(params.get('lon')); setTimeout(() => { if (typeof openMajorHistoryModal === 'function') { openMajorHistoryModal(); } if (!isNaN(targetLat) && !isNaN(targetLon) && typeof myGlobe !== 'undefined' && myGlobe) { myGlobe.pointOfView({ lat: targetLat, lng: targetLon, altitude: 1.5 }, 2000); } }, 800); return true; } return false; } const cleanId = String(eqId).trim(); if (!cleanId) return false; console.log(`[DEEP-LINK] Detectado enlace canónico para sismo ID: ${cleanId} (DYFI=${isDyfiDeepLink})`); const urlParams = new URLSearchParams(window.location.search); const isCinematicMode = urlParams.get('cinematic') === '1'; let matched = (earthquakesData || []).find(e => e.id === cleanId || String(e.id).toLowerCase() === cleanId.toLowerCase()); if (!matched && (window.__SSR_EVENT_DATA__ || window.__INITIAL_EVENT_DATA__)) { const rawEq = window.__SSR_EVENT_DATA__ || window.__INITIAL_EVENT_DATA__; matched = { id: String(rawEq.id || cleanId), magnitude: Number(rawEq.mag || rawEq.magnitude || 0), place: rawEq.place || rawEq.name || 'Ubicación S/D', time: new Date(rawEq.time || Date.now()), latitude: Number(rawEq.lat || (rawEq.coordinates && rawEq.coordinates[1]) || 0), longitude: Number(rawEq.lon || (rawEq.coordinates && rawEq.coordinates[0]) || 0), depth: Number(rawEq.depth || 10), consensusScore: (rawEq.sources ? rawEq.sources.split(',').length : 2), agencies: rawEq.sources ? rawEq.sources.split(',') : ['USGS', 'EMSC'], url: rawEq.url || '', felt: rawEq.felt || null, cdi: rawEq.cdi || null, mmi: rawEq.mmi || rawEq.maxMmi || null, alert: rawEq.alert || null, status: 'reviewed', tsunami: rawEq.tsunami || 0 }; if (Array.isArray(earthquakesData)) { earthquakesData.unshift(matched); } } if (!matched && urlParams.get('lat') !== null && urlParams.get('lon') !== null) { matched = { id: String(cleanId), magnitude: Number(urlParams.get('mag') || 0), place: urlParams.get('place') || 'Ubicación S/D', time: new Date(), latitude: Number(urlParams.get('lat')), longitude: Number(urlParams.get('lon')), depth: Number(urlParams.get('depth') || 10), consensusScore: 2, agencies: ['USGS', 'EMSC'], url: '', status: 'reviewed', tsunami: 0 }; if (Array.isArray(earthquakesData)) { earthquakesData.unshift(matched); } } if (matched) { console.log(`[DEEP-LINK] Sismo encontrado en catálogo activo: ${matched.place}`); selectedEvent = matched; if (isDyfiDeepLink && typeof window.openDyfiOneTapWidget === 'function') { const ssrCardNow = document.getElementById('seismic-event-ssr'); if (ssrCardNow) ssrCardNow.style.display = 'none'; window.openDyfiOneTapWidget(matched); } setTimeout(() => { if (isCinematicMode && typeof setSyncEngineState === 'function') { setSyncEngineState(false); } syncGlobeToEvent(matched, 1800, true); const ssrCard = document.getElementById('seismic-event-ssr'); const isMobileViewport = window.innerWidth <= 768; if (isDyfiDeepLink) { if (ssrCard) ssrCard.style.display = 'none'; if (typeof window.openDyfiOneTapWidget === 'function') { window.openDyfiOneTapWidget(matched); } else { openFeltItModal(matched); } } else if (isGlobeShortLink || !isCinematicMode) { if (isGlobeShortLink || !isMobileViewport || !ssrCard) { openEventDetails(matched); } } }, isDyfiDeepLink ? 60 : 350); return true; } // Búsqueda en catálogo de Grandes Terremotos Históricos Mundiales const histMatched = (window.MAJOR_HISTORIC_CATALOG || []).find(h => h.id === cleanId || String(h.id).toLowerCase() === cleanId.toLowerCase()); if (histMatched) { console.log(`[DEEP-LINK] Evento encontrado en catálogo histórico: ${histMatched.name}`); setTimeout(() => { if (typeof flyGlobeToHistoric === 'function') { flyGlobeToHistoric(histMatched.id); } }, 800); return true; } console.log(`[DEEP-LINK] Sismo no presente en feed reciente. Consultando API de detalle...`); fetch(`/api/usgs-detail?id=${encodeURIComponent(cleanId)}`) .then(r => r.json()) .then(feature => { if (!feature || !feature.properties) return; const props = feature.properties; const coords = (feature.geometry && feature.geometry.coordinates) || [0, 0, 10]; const syntheticEq = { id: feature.id || cleanId, magnitude: Number(props.mag || 0), place: props.place || 'S/D', time: new Date(props.time || Date.now()), latitude: coords[1], longitude: coords[0], depth: coords[2] || 10, consensusScore: (props.sources ? props.sources.split(',').length : 1), agencies: props.sources ? props.sources.split(',') : ['USGS'], url: props.url || '', felt: props.felt || null, cdi: props.cdi || null, mmi: props.mmi || null, alert: props.alert || null, status: props.status || 'reviewed', tsunami: props.tsunami || 0 }; if (typeof computeEventPerceptibility === 'function') { syntheticEq.perceptibility = computeEventPerceptibility(syntheticEq); } earthquakesData.unshift(syntheticEq); selectedEvent = syntheticEq; renderFeedList(); setTimeout(() => { if (isCinematicMode && typeof setSyncEngineState === 'function') { setSyncEngineState(false); } syncGlobeToEvent(syntheticEq, 1800, true); const ssrCard = document.getElementById('seismic-event-ssr'); const quickStrip = document.getElementById('ssr-dyfi-quick-strip'); const isMobileViewport = window.innerWidth <= 768; if (isDyfiDeepLink) { if (ssrCard) ssrCard.style.display = 'none'; if (typeof window.openDyfiOneTapWidget === 'function') { window.openDyfiOneTapWidget(syntheticEq); } else { openFeltItModal(syntheticEq); } } else if (!isCinematicMode) { if (!isMobileViewport || !ssrCard) { openEventDetails(syntheticEq); } } }, 500); }) .catch(err => { console.warn('[DEEP-LINK FETCH ERROR]', err); if (isDyfiDeepLink) openFeltItModal(); }); return true; } catch (err) { console.warn('[DEEP-LINK ROUTER ERROR]', err); return false; } } function shareOnFacebook(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(target.place, target.nearestCity, target.latitude, target.longitude) : { locality: target.place || '' }; const magStr = Number(target.magnitude || 0).toFixed(1); const quote = `🚨 Alerta Sísmica M ${magStr} en ${parsed.locality || target.place}. Monitoreo 3D en tiempo real:`; const fbUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}"e=${encodeURIComponent(quote)}`; window.open(fbUrl, '_blank', 'noopener,noreferrer,width=620,height=580'); trackShareEvent(target, 'facebook'); } async function shareOnInstagram(eq) { const target = eq || currentShareEvent; if (!target) return; const shareUrl = getCanonicalEventShareUrl(target.id); const parsed = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(target.place, target.nearestCity, target.latitude, target.longitude) : { locality: target.place || '' }; const magStr = Number(target.magnitude || 0).toFixed(1); const text = `🚨 ALERTA SÍSMICA — M ${magStr} • ${parsed.locality || target.place}\n⬇ Profundidad: ${Number(target.depth || 0).toFixed(1)} km\n🌐 Monitoreo en vivo: ${shareUrl}`; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(`${text}\n${shareUrl}`); } } catch (e) {} const toast = document.getElementById('share-copy-toast'); const toastMsg = document.getElementById('share-toast-msg'); if (toastMsg) toastMsg.innerText = '📋 ¡Ficha y enlace copiados! Pégalo en tu Historia o Chat de Instagram.'; if (toast) { toast.classList.remove('hidden'); setTimeout(() => { if (toast) toast.classList.add('hidden'); }, 5000); } if (typeof showToast === 'function') { showToast('📸 ¡Enlace copiado! Abriendo Instagram...'); } const isMobile = /iphone|ipad|ipod|android/i.test(navigator.userAgent || ''); if (isMobile) { window.location.href = 'instagram://app'; setTimeout(() => { window.open('https://www.instagram.com/', '_blank'); }, 800); } else { window.open('https://www.instagram.com/', '_blank'); } trackShareEvent(target, 'instagram'); } // FASE 206.0 (ITEM 3): Precarga Especulativa Inteligente en Eventos Destacados del Feed (0 ms Latencia) const _prefetchedEventIds = new Set(); function prefetchEventResources(eq) { if (!eq || !eq.id || _prefetchedEventIds.has(String(eq.id))) return; _prefetchedEventIds.add(String(eq.id)); try { const reqId = String(eq.id); if (window.SismosferaShortsCache && !window.SismosferaShortsCache.get(reqId)) { const qs = new URLSearchParams({ id: reqId, lat: String(eq.latitude ?? eq.lat ?? ''), lon: String(eq.longitude ?? eq.lon ?? ''), time: String(eq.time instanceof Date ? eq.time.getTime() : (eq.time ?? '')) }); fetch('/api/marketing/short-for-event?' + qs.toString()) .then(r => r.ok ? r.json() : null) .then(data => { if (data && data.found) { window.SismosferaShortsCache.set(reqId, data); const vid = data.youtubeVideoId || (data.youtubeUrl ? (data.youtubeUrl.match(/([a-zA-Z0-9_-]{11})/) || [])[1] : null); if (vid) { const img = new Image(); img.src = `https://i.ytimg.com/vi/${encodeURIComponent(vid)}/hqdefault.jpg`; } } }) .catch(() => {}); } } catch (_) {} } // Exposición global window.openShareModal = openShareModal; window.closeShareModal = closeShareModal; window.copyEventLink = copyEventLink; window.shareOnWhatsApp = shareOnWhatsApp; window.shareOnTelegram = shareOnTelegram; window.shareOnFacebook = shareOnFacebook; window.shareOnInstagram = shareOnInstagram; window.shareOnTwitter = shareOnTwitter; window.shareViaEmail = shareViaEmail; window.shareViaNative = shareViaNative; window.trackShareEvent = trackShareEvent; window.initDeepLinkRouter = initDeepLinkRouter; // ==================================================================== // RENDERIZADO DE TARJETAS SÍSMICAS (DISEÑO MAESTRO: PAÍS, PROVINCIA, LOCALIDAD) // ==================================================================== function formatElapsedHoursMins(rawMins, includePrefix = true) { const mins = Math.max(1, Math.floor(Number(rawMins) || 0)); const isEn = typeof currentLanguage !== 'undefined' && currentLanguage === 'en'; let core = ''; if (mins <= 59) { core = `${mins}m`; } else if (mins < 1440) { const h = Math.floor(mins / 60); const m = mins % 60; core = `${h}h ${m}m`; } else { const d = Math.floor(mins / 1440); const h = Math.floor((mins % 1440) / 60); core = h > 0 ? `${d}d ${h}h` : `${d}d`; } if (!includePrefix) return core; return isEn ? `${core} ago` : `Hace ${core}`; } window.formatElapsedHoursMins = formatElapsedHoursMins; function createFeedCardElement(eq, idx, isNewRealtime = false, cachedMostPowerful = null) { const item = document.createElement('div'); const t = I18N_TRANSLATIONS[currentLanguage] || I18N_TRANSLATIONS['es']; const minutesAgo = Math.max(Math.floor((Date.now() - eq.time.getTime()) / 60000), 0); const timeStr = formatElapsedHoursMins(minutesAgo, true); const parsed = parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude); const triTemp = typeof getEventTriTemporalAnalysis === 'function' ? getEventTriTemporalAnalysis(eq.time, eq.longitude, eq.latitude) : null; const isPerceptible = eq.magnitude >= 3.8; const mostPowerfulRecent = cachedMostPowerful !== null ? cachedMostPowerful : getMostPowerfulRecentEvent(earthquakesData); const isMostPowerful = mostPowerfulRecent && mostPowerfulRecent.id === eq.id; const isNew = isNewRealtime || (minutesAgo <= 5); // Colorimetría y Temas de Magnitud Gradiente let magTheme = { box: 'bg-emerald-950/40 border-emerald-500/40 text-emerald-300 shadow-emerald-500/10', tag: 'text-emerald-400/80' }; if (eq.magnitude >= 6.5) { magTheme = { box: 'bg-gradient-to-br from-rose-600/50 to-purple-900/60 border-rose-400/70 text-white shadow-rose-600/30 ring-1 ring-rose-400/40', tag: 'text-rose-200' }; } else if (eq.magnitude >= 5.5) { magTheme = { box: 'bg-gradient-to-br from-orange-500/30 to-rose-900/50 border-orange-500/50 text-orange-200 shadow-orange-500/20', tag: 'text-orange-300/80' }; } else if (eq.magnitude >= 4.5) { magTheme = { box: 'bg-gradient-to-br from-amber-500/25 to-amber-900/40 border-amber-500/50 text-amber-300 shadow-amber-500/15', tag: 'text-amber-400/80' }; } else if (eq.magnitude >= 3.0) { magTheme = { box: 'bg-gradient-to-br from-sky-500/25 to-blue-900/40 border-sky-500/40 text-sky-300 shadow-sky-500/10', tag: 'text-sky-400/80' }; } let depthCategory = 'Somero'; if (eq.depth >= 300) depthCategory = 'Profundo'; else if (eq.depth >= 70) depthCategory = 'Intermedio'; const mostRecentEvent = earthquakesData.length > 0 ? earthquakesData[0] : null; const isMostRecent = Boolean(mostRecentEvent && mostRecentEvent.id === eq.id); let topBadge = ''; if (!isMostRecent) { if (isMostPowerful) { topBadge = `👑 MAYOR`; } else if (isNew && !eq.isDelayedReport) { topBadge = `✨ NUEVO`; } else if (isPerceptible) { topBadge = `⚠️ SENTIDO`; } } const gapMatch = checkEventInSeismicGap(eq.latitude, eq.longitude); let gapBadge = ''; if (gapMatch) { eq.inSeismicGap = gapMatch; gapBadge = '⚡ BRECHA'; } let volcanicBadge = ''; if (eq.isVolcanic && eq.volcanoNearby) { volcanicBadge = '🌋 VOLCÁNICO'; } let cryoBadge = ''; const cryoMatch = checkCryoseismicEvent(eq.latitude, eq.longitude, eq.depth); if (cryoMatch) { eq.isCryo = true; eq.cryoInfo = cryoMatch; cryoBadge = '🧊 GLACIAR'; } let clusterBadge = ''; if (eq._clusterInfo) { if (eq._clusterInfo.isSwarm) { clusterBadge = `🌋 ENJAMBRE (${eq._clusterInfo.size})`; } else if (eq._clusterInfo.isAftershock) { clusterBadge = `⚡ RÉPLICA`; } else if (eq._clusterInfo.isMainshock) { clusterBadge = `💥 PRINCIPAL`; } } const isAutopilotTarget = activeAutopilotEvent && activeAutopilotEvent.id === eq.id; let cardBorderTheme = 'border-slate-800/80'; let topBannerHtml = ''; if (eq.isDelayedReport) { const delayM = eq.delayMinutes || Math.max(1, Math.round((Date.now() - (eq.time?.getTime() || Date.now())) / 60000)); const delayFormatted = formatElapsedHoursMins(delayM, false); cardBorderTheme = 'ring-2 ring-amber-500/90 border-amber-400/90 bg-gradient-to-b from-amber-950/40 via-slate-900/90 to-slate-950 shadow-2xl shadow-amber-500/25 scale-[1.015]'; topBannerHtml = `
⚡ INFORMADO EN ESTE MOMENTO (ARRIBO RECIENTE)
🌍 MOSTRANDO EN GLOBO
🕒 Sucedió hace ${delayFormatted} (Reporte diferido por agencia) RETRASO DE RED
`; } else if (isMostRecent) { cardBorderTheme = 'ring-2 ring-rose-500/90 border-rose-400/90 bg-gradient-to-b from-rose-950/40 via-slate-900/90 to-slate-950 shadow-2xl shadow-rose-500/25 scale-[1.015]'; topBannerHtml = `
⚡ EVENTO MÁS RECIENTE
🌍 MOSTRANDO EN GLOBO
`; } else if (isAutopilotTarget) { cardBorderTheme = 'ring-1 ring-sky-400 border-sky-400/80 bg-sky-950/40 shadow-xl shadow-sky-500/25 scale-[1.01]'; } const slideClass = isNewRealtime ? 'feed-card-slide-down' : (idx < 4 ? 'feed-card-slide' : ''); const isRevCard = (eq.status && String(eq.status).toLowerCase() === 'reviewed') || (eq.reviewLabel && String(eq.reviewLabel).toLowerCase().includes('revisado')); const reviewBadge = isRevCard ? `✓ SISMÓLOGO` : ''; let scaleBadge = ''; if (eq.shindo) { scaleBadge = `🇯🇵 Shindo ${eq.shindo}`; } else if (eq.mmi && Number(eq.mmi) >= 2.5) { scaleBadge = `MMI ${Number(eq.mmi).toFixed(1)}`; } const forensic = eq.forensicSource || (typeof classifyForensicSource === 'function' ? classifyForensicSource(eq) : null); const forensicBadge = (forensic && forensic.isNonTectonic) ? forensic.badgeHtml : ''; if (!eq.youtubeUrl && Array.isArray(window._publishedShortsList)) { const eqTimeMs = eq.time instanceof Date ? eq.time.getTime() : Number(eq.time || 0); const mShort = window._publishedShortsList.find(s => { if (!s || !s.youtubeUrl) return false; if (String(s.eventId) === String(eq.id)) return true; if (eqTimeMs > 0 && s.timeMs > 0 && Math.abs(eqTimeMs - s.timeMs) <= 5 * 60 * 1000 && Number.isFinite(eq.latitude) && Number.isFinite(eq.longitude) && Number.isFinite(s.lat) && Number.isFinite(s.lon)) { return haversineDistance(eq.latitude, eq.longitude, s.lat, s.lon) <= 90; } return false; }); if (mShort) { eq.youtubeUrl = mShort.youtubeUrl; eq.youtubeVideoId = mShort.youtubeVideoId || null; } } const ytShortBadge = eq.youtubeUrl ? `🎬3D` : ''; const activeBadges = [ytShortBadge, forensicBadge, topBadge, reviewBadge, scaleBadge, gapBadge, volcanicBadge, clusterBadge, cryoBadge].filter(Boolean); const badgesRowHtml = activeBadges.length > 0 ? `
${activeBadges.join('')}
` : ''; item.setAttribute('data-eq-id', eq.id); item.setAttribute('data-eq-time', eq.time ? eq.time.getTime() : 0); item.setAttribute('data-eq-effective-time', typeof window.getEffectiveFeedTime === 'function' ? window.getEffectiveFeedTime(eq) : (eq.time ? eq.time.getTime() : 0)); item.onpointerenter = () => { if (typeof prefetchEventSeismogram === 'function') prefetchEventSeismogram(eq); prefetchEventResources(eq); }; item.ontouchstart = () => { prefetchEventResources(eq); }; if (eq.isDelayedReport) { item.setAttribute('data-delayed-report', 'true'); item.setAttribute('data-delay-mins', eq.delayMinutes || 0); } item.className = `group relative p-3.5 rounded-2xl bg-gradient-to-b from-slate-900/90 to-slate-950/90 border hover:border-sky-500/50 hover:from-slate-850 hover:to-slate-900/90 cursor-pointer transition-all duration-200 shadow-md hover:shadow-xl hover:shadow-black/60 ${cardBorderTheme} ${slideClass}`; let timeBadgeHtml = ''; if (eq.isDelayedReport) { const delayM = eq.delayMinutes || Math.max(1, Math.round((Date.now() - (eq.time?.getTime() || Date.now())) / 60000)); const delayFormatted = formatElapsedHoursMins(delayM, false); timeBadgeHtml = ` ⚡ Hace ${delayFormatted} `; } else { timeBadgeHtml = ` 🕒 ${timeStr} `; } const agenciesBadgeLabel = typeof formatAgenciesBadgeString === 'function' ? formatAgenciesBadgeString(eq.validatingAgencies || [eq.source || 'USGS'], 2) : `${eq.consensusScore || 1} ag`; const agenciesFullTitle = Array.isArray(eq.validatingAgencies) && eq.validatingAgencies.length > 0 ? `Confirmado por ${eq.validatingAgencies.length} agencia(s): ${eq.validatingAgencies.join(', ')}${eq.reviewLabel ? ' • ' + eq.reviewLabel : ''}` : `Consenso Oficial de ${eq.consensusScore || 1} agencia(s)`; item.innerHTML = ` ${topBannerHtml}
${(eq.magType || 'MAG').toUpperCase().slice(0, 4)} ${eq.magnitude.toFixed(1)} ${depthCategory}
${parsed.flag || '🌐'} ${parsed.cleanCountry || parsed.country}
${timeBadgeHtml}
📍 ${parsed.cleanLocation || parsed.locality}
${triTemp ? ` ${triTemp.periodEmoji} ${triTemp.epicenterTimeShort} (${triTemp.periodTag} local) ` : ''} ${badgesRowHtml}
${typeof window.getCardHumanMicroLine === 'function' ? window.getCardHumanMicroLine(eq) : ''}
⬇ ${eq.depth.toFixed(1)} km ${agenciesBadgeLabel}
`; const cardShareBtn = item.querySelector('.btn-feed-card-share'); if (cardShareBtn) { cardShareBtn.onclick = (e) => { e.stopPropagation(); if (typeof openShareModal === 'function') { openShareModal(eq, e); } }; } item.onclick = () => { if (typeof window.activeGeoSearchQuery === 'string' && window.activeGeoSearchQuery.trim().length >= 2) { if (typeof georeferenceEventOnGlobe === 'function') { georeferenceEventOnGlobe(eq); openEventDetails(eq); return; } } syncGlobeToEvent(eq, 2200, true); const list = getVisibleFilteredEarthquakes(); const foundIdx = list.findIndex(e => e.id === eq.id); if (foundIdx !== -1) { syncCurrentIndex = (foundIdx + 1) % list.length; } // Fase 200.2: En móvil (< 768px), 1 toque abre directamente la Ficha Técnica completa sin ventanas intermedias const previewBar = document.getElementById('mobile-split-preview-bar'); if (previewBar) previewBar.classList.add('hidden'); openEventDetails(eq); }; // Fase 125.0: Accesibilidad y Feedback Táctil Instantáneo (<16ms) item.setAttribute('role', 'button'); item.setAttribute('tabindex', '0'); item.setAttribute('aria-label', `${eq.place}, Magnitud ${eq.magnitude.toFixed(1)}`); item.addEventListener('pointerdown', function() { item.classList.add('is-tactile-pressed'); }, { passive: true }); const clearCardTactile = function() { item.classList.remove('is-tactile-pressed'); }; item.addEventListener('pointerup', clearCardTactile, { passive: true }); item.addEventListener('pointerleave', clearCardTactile, { passive: true }); item.addEventListener('pointercancel', clearCardTactile, { passive: true }); item.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (typeof item.onclick === 'function') item.onclick(); } }); return item; } // ==================================================================== // INSERCIÓN CRONOLÓGICA ESTRICTA (LO MÁS RECIENTE SIEMPRE ARRIBA) // ==================================================================== function insertFeedCardChronologically(eq, isNewRealtime = false, forceTop = false) { const container = document.getElementById('seismic-feed-list'); if (!container) return; // No insertar si ya existe una tarjeta con este ID en el DOM if (container.querySelector(`[data-eq-id="${eq.id}"]`)) return; // Respetar filtro de magnitud activo if (liveFeedFilter === '3.0' && eq.magnitude < 3.0) return; if (liveFeedFilter === '4.5' && eq.magnitude < 4.5) return; if (liveFeedFilter === '6.0' && eq.magnitude < 6.0) return; if (liveFeedFilter === 'perceptible' && eq.magnitude < 3.8 && (!eq.perceptibility || !eq.perceptibility.feltInCity)) return; if (liveFeedFilter === 'video3d' && !eq.youtubeUrl && !eq.youtubeVideoId) return; if (liveFeedFilter === 'quiet') return; // Respetar filtro regional de país activo if (activeCountryFilter && activeCountryFilter !== 'all' && !isEarthquakeInCountry(eq, activeCountryFilter)) return; const eqEffectiveTime = (typeof window.getEffectiveFeedTime === 'function') ? window.getEffectiveFeedTime(eq) : ((eq.time instanceof Date) ? eq.time.getTime() : new Date(eq.time || 0).getTime()); const existingCards = Array.from(container.querySelectorAll('[data-eq-id]')); // Buscar la primera tarjeta en el DOM cuyo timestamp efectivo sea MENOR (más antigua) const olderReferenceCard = existingCards.find(cardEl => { const cTime = Number(cardEl.getAttribute('data-eq-effective-time') || cardEl.getAttribute('data-eq-time') || 0); return cTime < eqEffectiveTime; }); // Solo aplica animación de slide-down si va al tope absoluto (es forzado al tope o el más nuevo) const isGoingToTop = forceTop || !olderReferenceCard || olderReferenceCard === container.firstChild; if (isGoingToTop) { // Remover el marco y banner de "MÁS RECIENTE" / "INFORMADO AHORA" únicamente de la tarjeta previa en el tope (O(1)) const prevTop = container.querySelector('.feed-most-recent-banner')?.closest('[data-eq-id]') || container.firstElementChild; if (prevTop) { prevTop.querySelectorAll('.feed-most-recent-banner').forEach(b => b.remove()); prevTop.classList.remove( 'ring-2', 'ring-rose-500/90', 'border-rose-400/90', 'bg-gradient-to-b', 'from-rose-950/40', 'via-slate-900/90', 'to-slate-950', 'shadow-2xl', 'shadow-rose-500/25', 'scale-[1.015]', 'ring-amber-500/90', 'border-amber-400/90', 'from-amber-950/40', 'shadow-amber-500/25' ); prevTop.classList.add('border-slate-800/80'); } } const card = createFeedCardElement(eq, 0, isNewRealtime && isGoingToTop); const prevScrollTop = container.scrollTop; const userIsNearTop = prevScrollTop < 80; if (isGoingToTop || !olderReferenceCard) { if (container.firstChild) { container.insertBefore(card, container.firstChild); } else { container.appendChild(card); } } else { container.insertBefore(card, olderReferenceCard); } // Eliminar mensaje vacío inicial si existe const emptyMsg = container.querySelector('.text-center'); if (emptyMsg) emptyMsg.remove(); // Si fue al tope y el usuario no estaba cerca del tope, compensar scroll if (isGoingToTop && !userIsNearTop) { const cardHeight = card.offsetHeight + 8; container.scrollTop = prevScrollTop + cardHeight; } // Actualizar contador const feedCountEl = document.getElementById('feed-count'); if (feedCountEl) { const visibleCards = container.querySelectorAll('[data-eq-id]').length; const dockSismosBadge = document.getElementById('dock-sismos-count'); if (dockSismosBadge) dockSismosBadge.textContent = String(visibleCards); const t = I18N_TRANSLATIONS[currentLanguage] || I18N_TRANSLATIONS['es']; if (liveFeedFilter === 'all') { feedCountEl.innerText = `${visibleCards}`; feedCountEl.title = `${visibleCards} ${t.events_suffix}`; } else { feedCountEl.innerText = `${visibleCards}/${earthquakesData.length}`; feedCountEl.title = `${visibleCards} de ${earthquakesData.length} ${t.events_suffix}`; } } // Limitar DOM a 200 tarjetas máximo para rendimiento const allCards = container.querySelectorAll('[data-eq-id]'); if (allCards.length > 200) { for (let i = 200; i < allCards.length; i++) { allCards[i].remove(); } } } // Alias para compatibilidad con llamadas existentes const prependNewFeedCard = insertFeedCardChronologically; // Actualizador dinámico de tiempos relativos ("hace X min") para evitar textos desactualizados function updateFeedRelativeTimes() { const container = document.getElementById('seismic-feed-list'); if (!container) return; const t = I18N_TRANSLATIONS[currentLanguage] || I18N_TRANSLATIONS['es']; const now = Date.now(); container.querySelectorAll('[data-eq-id]').forEach(card => { const isDelayed = card.getAttribute('data-delayed-report') === 'true'; const timeVal = Number(card.getAttribute('data-eq-time')); if (!timeVal) return; const minsAgo = Math.max(0, Math.floor((now - timeVal) / 60000)); const badge = card.querySelector('.feed-card-time-badge'); const elapsedFormatted = formatElapsedHoursMins(minsAgo, false); if (isDelayed) { const delayText = `⚡ Hace ${elapsedFormatted}`; if (badge) { badge.setAttribute('title', `Arribo: Ahora • Hace ${elapsedFormatted} (Reporte diferido por agencia)`); if (badge.innerText.trim() !== delayText.trim()) { badge.innerHTML = `⚡ Hace ${elapsedFormatted}`; } } const bannerDesc = card.querySelector('.feed-delayed-report-banner .feed-delayed-time-text'); if (bannerDesc) { bannerDesc.setAttribute('title', `Sucedió hace ${elapsedFormatted} (Reporte diferido por agencia)`); bannerDesc.innerHTML = `🕒 Sucedió hace ${elapsedFormatted} (Reporte diferido por agencia)`; } } else { const timeStr = formatElapsedHoursMins(minsAgo, true); const expectedBadgeText = `🕒 ${timeStr}`; if (badge && badge.innerText.trim() !== expectedBadgeText.trim()) { badge.innerHTML = `🕒 ${timeStr}`; } } }); // Mantener actualizado el podio Top 3 cada minuto try { if (typeof renderTopRankings === 'function') renderTopRankings(); } catch (eRtr) {} } function updateFeedCardAgencyConfirmation(eq) { const card = document.querySelector(`#seismic-feed-list [data-eq-id="${eq.id}"]`); if (!card) return; const isConfirmed = eq.consensusScore > 1; const agencySpan = card.querySelector('.feed-card-agency-consensus') || card.querySelector('.text-emerald-400.font-medium'); if (agencySpan) { const agenciesBadgeLabel = typeof formatAgenciesBadgeString === 'function' ? formatAgenciesBadgeString(eq.validatingAgencies || [eq.source || 'USGS'], 2) : `${eq.consensusScore || 1} ag`; agencySpan.innerHTML = `${agenciesBadgeLabel}`; agencySpan.setAttribute('title', `Confirmado por ${eq.validatingAgencies.length} agencia(s): ${eq.validatingAgencies.join(', ')}${eq.reviewLabel ? ' • ' + eq.reviewLabel : ''}`); } card.classList.add('feed-card-confirm-flash'); setTimeout(() => { card.classList.remove('feed-card-confirm-flash'); }, 2000); } function getVisibleFilteredEarthquakes() { if (liveFeedFilter === 'quiet') return []; // Si hay una búsqueda geográfica activa (Fase 84.0), devolver los resultados de la búsqueda ordenados del más reciente al más antiguo if (typeof window.activeGeoSearchQuery === 'string' && window.activeGeoSearchQuery.trim().length >= 2 && typeof performGeoSearch === 'function') { return performGeoSearch(window.activeGeoSearchQuery); } let filtered = [...earthquakesData]; if (liveFeedFilter === '3.0') { filtered = filtered.filter(e => e.magnitude >= 3.0); } else if (liveFeedFilter === '4.5') { filtered = filtered.filter(e => e.magnitude >= 4.5); } else if (liveFeedFilter === '6.0') { filtered = filtered.filter(e => e.magnitude >= 6.0); } else if (liveFeedFilter === 'perceptible') { filtered = filtered.filter(e => e.magnitude >= 3.8 || (e.perceptibility && e.perceptibility.feltInCity)); } else if (liveFeedFilter === 'video3d') { const shortsArr = Array.isArray(window._publishedShortsList) ? window._publishedShortsList : []; const byIdSet = new Map(); shortsArr.forEach(s => { if (s && s.youtubeUrl) { if (s.eventId) byIdSet.set(String(s.eventId), s); if (s.parentEventId) byIdSet.set(String(s.parentEventId), s); } }); filtered.forEach(e => { if (!e.youtubeUrl && byIdSet.has(String(e.id))) { const m = byIdSet.get(String(e.id)); e.youtubeUrl = m.youtubeUrl; e.youtubeVideoId = m.youtubeVideoId; e.youtubeFormat = m.format; e.youtubeTitle = m.title; } }); const matchedIds = new Set(); filtered = filtered.filter(e => { const hasVid = Boolean(e.youtubeUrl || e.youtubeVideoId); if (hasVid) matchedIds.add(String(e.id)); return hasVid; }); // Incorporar sismos reales con video publicado que ya salieron de la ventana de 24h en memoria shortsArr.forEach(s => { const cid = String(s.parentEventId || s.eventId || '').trim(); if (!cid || matchedIds.has(cid) || !s.youtubeVideoId) return; matchedIds.add(cid); const magNum = Number(s.magnitude || s.mag || 4.5); const placeClean = s.place || (s.title ? s.title.replace(/^.*?M\s*[0-9.]+\s+(?:en|in|—|-)\s+/i, '').split(/[•|#]/)[0].trim() : 'Región Sísmica'); const evTime = new Date(s.timestamp || s.timeMs || s.publishedAt || Date.now()); const archiveEq = { id: cid, magnitude: magNum > 0 ? magNum : 4.5, place: placeClean || 'Región Sísmica', latitude: Number.isFinite(Number(s.lat)) ? Number(s.lat) : 0, longitude: Number.isFinite(Number(s.lon)) ? Number(s.lon) : 0, depth: Number(s.depth || 10), time: evTime, url: `https://sismosfera.com/sismo/${encodeURIComponent(cid)}`, tsunami: 0, status: 'reviewed', source: 'USGS/EMSC', validatingAgencies: ['USGS', 'EMSC'], consensusScore: 2, youtubeUrl: s.youtubeUrl, youtubeVideoId: s.youtubeVideoId, youtubeFormat: s.format || '9:16_SHORT', youtubeTitle: s.title || '' }; if (!earthquakesData.some(x => String(x.id) === cid)) { earthquakesData.push(archiveEq); } filtered.push(archiveEq); }); } // Respetar filtro regional de país activo window._sismoMajorRegionalFallbackActive = false; if (activeCountryFilter && activeCountryFilter !== 'all') { filtered = filtered.filter(e => isEarthquakeInCountry(e, activeCountryFilter)); if (filtered.length === 0 && (liveFeedFilter === '4.5' || liveFeedFilter === '6.0')) { const countryAll = earthquakesData.filter(e => isEarthquakeInCountry(e, activeCountryFilter)); const m35 = countryAll.filter(e => Number(e.magnitude) >= 3.5); filtered = m35.length > 0 ? m35 : [...countryAll].sort((a, b) => (Number(b.magnitude) || 0) - (Number(a.magnitude) || 0)).slice(0, 10); if (filtered.length > 0) window._sismoMajorRegionalFallbackActive = true; } } // Ordenar estrictamente de arriba hacia abajo: lo más nuevo arriba filtered.sort((a, b) => (b.time?.getTime() || 0) - (a.time?.getTime() || 0)); return filtered; } // ==================================================================== // MONETIZACIÓN BENTO NATIVA CERO CLS & TELEMETRÍA GA4/MCM (FASE 112.0) // ==================================================================== let persistentBentoFeedAdNode = null; let bentoFeedAdObserver = null; let bentoFeedAdVisibleStartTime = 0; let bentoFeedAd1sTracked = false; let bentoFeedAd45sTracked = false; // Centinela Universal de Telemetría Publicitaria GA4 / GTM window.trackAdEvent = function(action, params) { try { window.dataLayer = window.dataLayer || []; window.dataLayer.push(Object.assign({ event: 'ad_monetization_telemetry', ad_action: action, platform: 'sismosfera', timestamp: new Date().toISOString() }, params || {})); } catch (e) { // Silencioso ante navegadores con bloqueador } }; // Motor Híbrido Universal de Entrega (AdSense / Google Ad Manager MCM / Prebid Header Bidding - FASE 113.0) window.SismosferaAdDeliveryEngine = { version: '113.0', activeProvider: 'adsense', networkCode: '1493292282409725', mcmStatus: 'ready_for_onboarding', headerBiddingEnabled: true, headerBiddingTimeoutMs: 800, bidders: ['rubicon', 'appnexus', 'ix', 'openx', 'criteo'], slots: { feed: { id: 'persistent-bento-feed-ad-card', status: 'active', format: 'rectangle,horizontal', sizes: [[300, 250], [336, 280]] }, detail: { id: 'detail-bento-ad-card', status: 'active', format: 'rectangle,horizontal', sizes: [[300, 250], [336, 280]] }, countryHub: { id: 'hub-country-bento-billboard', status: 'active', format: 'horizontal', sizes: [[728, 90], [970, 90], [320, 100]] } }, enableGoogleAdManager: function(gamNetworkCode) { this.activeProvider = 'gam_adx'; this.gamNetworkCode = gamNetworkCode; if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('gam_mcm_switched', { gamNetworkCode: gamNetworkCode }); } if (window.googletag && window.googletag.cmd) { window.googletag.cmd.push(() => { try { window.googletag.pubads().enableSingleRequest(); window.googletag.pubads().collapseEmptyDivs(); window.googletag.enableServices(); } catch (eGpt) { console.warn('[GPT] Initialization note:', eGpt); } }); } }, // VECTOR 4: Header Bidding Asíncrono con timeout estricto de 800ms initHeaderBidding: function(slotKey, callback) { const timeout = this.headerBiddingTimeoutMs || 800; let done = false; const finish = (result) => { if (done) return; done = true; if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('prebid_auction_complete', { slot: slotKey, status: result.status, provider: this.activeProvider }); } if (typeof callback === 'function') callback(result); }; const timer = setTimeout(() => { finish({ status: 'timeout', provider: this.activeProvider }); }, timeout); try { if (window.pbjs && typeof window.pbjs.requestBids === 'function') { window.pbjs.que = window.pbjs.que || []; window.pbjs.que.push(() => { window.pbjs.requestBids({ timeout: timeout - 50, bidsBackHandler: function(bids) { clearTimeout(timer); if (window.googletag && typeof window.pbjs.setTargetingForGPTAsync === 'function') { window.pbjs.setTargetingForGPTAsync(); } finish({ status: 'success', bids: bids }); } }); }); } else { clearTimeout(timer); finish({ status: 'direct_fallback', provider: this.activeProvider }); } } catch (errHb) { clearTimeout(timer); finish({ status: 'error', error: errHb.message, provider: this.activeProvider }); } }, // VECTOR 1: Gentle AdBlock Notice (Polite Bento Invitation, cero bloqueo, cero popups) renderGentleAdBlockNotice: function(containerEl, slotKey) { if (!containerEl) return; const isTh = window.location.hostname.includes('terremotoshoy'); const siteName = isTh ? 'Terremotos Hoy' : 'Sismosfera 3D'; containerEl.innerHTML = `
🛡️ Red Ciudadana de Alerta Acceso Libre
Monitoreo Sísmico Abierto e Independiente

${siteName} mantiene servidores dedicados 24/7 para alertar a la población ante emergencias geofísicas. Si utilizas un bloqueador de anuncios, considera incluirnos en tu lista de permitidos o apoyarnos compartiendo la plataforma.

`; if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('adblock_gentle_notice_rendered', { slot: slotKey }); } }, // Verificación proactiva de renderizado de AdSense checkAdBlockStatus: function(adContainer, slotKey) { if (!adContainer) return; setTimeout(() => { try { const ins = adContainer.querySelector('ins.adsbygoogle'); const hasBlockedStatus = ins && (ins.getAttribute('data-ad-status') === 'unfilled' || ins.children.length === 0 || ins.offsetHeight === 0); const scriptBlocked = typeof window.adsbygoogle === 'undefined' || !window.adsbygoogle.loaded; if (hasBlockedStatus || scriptBlocked) { this.renderGentleAdBlockNotice(adContainer, slotKey); } } catch (e) {} }, 2200); }, getMetrics: function() { return { viewableFeed: Boolean(bentoFeedAdVisibleStartTime > 0), feedDurationMs: bentoFeedAdVisibleStartTime ? (Date.now() - bentoFeedAdVisibleStartTime) : 0, feedMrcQualified: bentoFeedAd1sTracked, feedActiveView45s: bentoFeedAd45sTracked }; }, syncFromBackend: async function() { try { const res = await fetch('/api/monetization/config'); if (!res.ok) return; const cfg = await res.json(); if (!cfg || !cfg.success) return; if (cfg.provider === 'gam_adx') { this.enableGoogleAdManager(cfg.gamNetworkCode || '1493292282409725'); } else if (cfg.provider === 'prebid') { this.headerBiddingEnabled = true; if (cfg.auctionTimeout) this.headerBiddingTimeoutMs = cfg.auctionTimeout; if (cfg.bidders) { this.bidders = Object.keys(cfg.bidders).filter(k => cfg.bidders[k] && cfg.bidders[k].enabled); } this.activeProvider = 'prebid'; } else { this.activeProvider = 'adsense'; } if (cfg.bentoCardsEnabled !== undefined) { this.bentoCardsEnabled = cfg.bentoCardsEnabled; } } catch (e) { console.debug('[AD ENGINE] Sincronización backend fallback local'); } } }; try { window.SismosferaAdDeliveryEngine.syncFromBackend(); } catch(e) {} function getOrCreatePersistentFeedAdNode() { if (persistentBentoFeedAdNode) { return persistentBentoFeedAdNode; } const adWrapper = document.createElement('div'); adWrapper.id = 'persistent-bento-feed-ad-card'; adWrapper.className = 'bento-feed-ad-card p-2.5 rounded-2xl bg-slate-900/75 border border-slate-800/80 my-2 text-center transition-all duration-200 overflow-hidden shadow-sm relative'; adWrapper.style.minHeight = '76px'; adWrapper.style.display = 'flex'; adWrapper.style.flexDirection = 'column'; adWrapper.style.justifyContent = 'center'; adWrapper.style.alignItems = 'center'; adWrapper.style.contain = 'layout paint'; adWrapper.innerHTML = `
CANAL 3D Boletines Sísmicos 3D (@Sismosfera3D)
Simulaciones hipocentrales y documentales 3D en YouTube
`; // Carga asíncrona segura protegida setTimeout(() => { try { (window.adsbygoogle = window.adsbygoogle || []).push({}); if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('ad_slot_impression', { slot_name: 'feed_in_feed', provider: 'adsense' }); } } catch (e) { // Silencioso ante bloqueadores de anuncios o carga previa } }, 60); // Smart Active View Observer (> 45s de visibilidad real en pantalla para optimización de eCPM) if ('IntersectionObserver' in window) { try { let timer1s = null; let timer45s = null; bentoFeedAdObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting && entry.intersectionRatio >= 0.6) { if (!bentoFeedAdVisibleStartTime) bentoFeedAdVisibleStartTime = Date.now(); if (!bentoFeedAd1sTracked && !timer1s) { timer1s = setTimeout(() => { bentoFeedAd1sTracked = true; if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('ad_slot_viewable_mrc', { slot_name: 'feed_in_feed', duration: '1s' }); } }, 1000); } if (!bentoFeedAd45sTracked && !timer45s) { timer45s = setTimeout(() => { bentoFeedAd45sTracked = true; if (typeof window.trackAdEvent === 'function') { window.trackAdEvent('ad_slot_active_view_45s', { slot_name: 'feed_in_feed', duration: '45s' }); } }, 45000); } } else { bentoFeedAdVisibleStartTime = 0; if (timer1s) { clearTimeout(timer1s); timer1s = null; } if (timer45s) { clearTimeout(timer45s); timer45s = null; } } }); }, { threshold: [0.6] }); bentoFeedAdObserver.observe(adWrapper); } catch (errObs) { console.warn('[ADS] Error iniciando observador de visibilidad:', errObs); } } // VECTOR 1: Verificación de AdBlock para activación de Polite Notice if (window.SismosferaAdDeliveryEngine && typeof window.SismosferaAdDeliveryEngine.checkAdBlockStatus === 'function') { window.SismosferaAdDeliveryEngine.checkAdBlockStatus(adWrapper, 'feed_in_feed'); } persistentBentoFeedAdNode = adWrapper; return persistentBentoFeedAdNode; } function createBentoFeedAdCard() { return getOrCreatePersistentFeedAdNode(); } function renderFeedList() { const container = document.getElementById('seismic-feed-list'); if (!container) return; // Actualizar tarjeta de inteligencia sísmica regional y energía acumulada try { updateRegionalIntelligenceCard(); } catch (errIntel) { console.warn('[INTEL] Error actualizando tarjeta regional:', errIntel); } // Calcular clusters espacio-temporales DBSCAN sobre el catálogo activo (protegido defensivamente) try { computeDbscanClusters(earthquakesData); } catch (errClusters) { console.warn('[CLUSTERS] Error calculando DBSCAN:', errClusters); } // Detectar sismos de origen volcano-tectónico somero (Herramienta 3.6 - protegido defensivamente) try { detectVolcanicEarthquakes(earthquakesData); } catch (errVolcano) { console.warn('[VOLCANO] Error detectando sismos volcánicos:', errVolcano); } // FASE 108: Preservación de nodo DOM publicitario para evitar destrucción y recargas redundantes if (persistentBentoFeedAdNode && persistentBentoFeedAdNode.parentNode === container) { container.removeChild(persistentBentoFeedAdNode); } if (window._cachedMktV3BentoCard && window._cachedMktV3BentoCard.parentNode === container) { container.removeChild(window._cachedMktV3BentoCard); } // Fase 198.9: Preservar bloques SSR de Hub de País (#hub-direct-answer-summary con H1, #kit-72h, etc.) al final del feed if (!window._preservedHubSeoNodes) { const hubIds = ['hub-direct-answer-summary', 'hub-country-shorts-gallery', 'kit-72h', 'hub-youtube-16x9-showcase', 'hub-country-bento-billboard']; const foundNodes = hubIds.map(id => container.querySelector('#' + id)).filter(Boolean); if (foundNodes.length > 0) window._preservedHubSeoNodes = foundNodes; } if (Array.isArray(window._preservedHubSeoNodes)) { window._preservedHubSeoNodes.forEach(n => { if (n && n.parentNode === container) container.removeChild(n); }); } container.innerHTML = ''; const t = I18N_TRANSLATIONS[currentLanguage] || I18N_TRANSLATIONS['es']; if (liveFeedFilter === 'quiet') { const feedCountEl = document.getElementById('feed-count'); if (feedCountEl) { feedCountEl.innerText = `${ASEISMIC_CRATONS.length}`; feedCountEl.title = `${ASEISMIC_CRATONS.length} Zonas Seguras`; } ASEISMIC_CRATONS.forEach((craton) => { const item = document.createElement('div'); item.className = 'group p-3 rounded-xl bg-slate-900/80 border border-emerald-500/40 hover:bg-slate-800/90 hover:border-emerald-400 cursor-pointer transition-all duration-200 shadow-sm hover:shadow-emerald-500/20 feed-card-slide'; item.innerHTML = `
CALMA ${craton.name}
ASEÍSMICO
${craton.region}
${craton.description}
${craton.status} ${craton.age}
`; item.onclick = () => { focusEarthquake({ latitude: craton.latitude, longitude: craton.longitude, magnitude: 0, place: `${craton.name} (Región Asísmica)` }, 2400); showToast(`🛡️ Enfocando Zona Libre de Sismos: ${craton.name} (${craton.region})`); }; container.appendChild(item); }); container.scrollTop = 0; return; } const filtered = getVisibleFilteredEarthquakes(); const feedCountEl = document.getElementById('feed-count'); if (feedCountEl) { if (liveFeedFilter === 'all') { feedCountEl.innerText = `${filtered.length}`; feedCountEl.title = `${filtered.length} ${t.events_suffix}`; } else { feedCountEl.innerText = `${filtered.length}/${earthquakesData.length}`; feedCountEl.title = `${filtered.length} de ${earthquakesData.length} ${t.events_suffix}`; } } const dockSismosBadge = document.getElementById('dock-sismos-count'); if (dockSismosBadge) dockSismosBadge.textContent = String(filtered.length); if (typeof window.updateMobileHumanPulseBar === 'function') window.updateMobileHumanPulseBar(); if (filtered.length === 0) { const countryInfo = (activeCountryFilter && activeCountryFilter !== 'all') ? REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter] : null; const msg = countryInfo ? `Sin eventos sismológicos recientes en ${countryInfo.flag} ${countryInfo.name}` : 'Sin eventos sismológicos para este filtro'; container.innerHTML = `
${countryInfo ? countryInfo.flag : '🛡️'}
${msg}
Tranquilidad sísmica en la región seleccionada
`; return; } const mostPowerfulRecent = getMostPowerfulRecentEvent(earthquakesData); const frag = document.createDocumentFragment(); if (activeCountryFilter && activeCountryFilter !== 'all') { const cInfo = REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter] || { name: activeCountryFilter.toUpperCase(), code: activeCountryFilter }; const slugMap = { cl: 'chile', mx: 'mexico', pe: 'peru', ar: 'argentina', co: 'colombia', ec: 'ecuador', gt: 'guatemala', cr: 'costa-rica', sv: 'el-salvador', ni: 'nicaragua', pa: 'panama', pr: 'puerto-rico', do: 'republica-dominicana', es: 'espana', us: 'estados-unidos', jp: 'japon', it: 'italia', tr: 'turquia', id: 'indonesia', tw: 'taiwan', nz: 'nueva-zelanda', ph: 'filipinas', gr: 'grecia' }; const hubSlug = slugMap[String(activeCountryFilter).toLowerCase()] || ''; const hubBtnHtml = hubSlug ? `📊 Hub ↗` : ''; const subNote = window._sismoMajorRegionalFallbackActive ? ` • SISMOS RELEVANTES (${filtered.length})` : ` (${filtered.length})`; const bannerDiv = document.createElement('div'); bannerDiv.id = 'sismo-country-focus-banner'; bannerDiv.className = 'mb-1.5 px-2.5 py-1.5 rounded-xl bg-sky-950/80 border border-sky-500/40 flex items-center justify-between gap-2 text-[11px] text-sky-100'; bannerDiv.innerHTML = `📍 ${cInfo.name}${subNote}
${hubBtnHtml}
`; frag.appendChild(bannerDiv); } const limit = window._feedVisibleLimit || 60; const displayItems = filtered.slice(0, limit); // Fase 198.5 (Entrada #638): Agrupación de enjambres (DBSCAN real) — nunca agrupa M4.5+ ni secuencias principal+réplicas const SWARM_MIN_GROUP = 4; const isGroupableSwarm = (q) => Boolean(q && q._clusterInfo && q._clusterInfo.isSwarm && Number(q.magnitude) < 4.5); const swarmMembersById = new Map(); displayItems.forEach(q => { if (!isGroupableSwarm(q)) return; const cid = q._clusterInfo.clusterId; if (!swarmMembersById.has(cid)) swarmMembersById.set(cid, []); const bucket = swarmMembersById.get(cid); // Evita que un encadenamiento transitivo DBSCAN agrupe ciudades lejanas (> 65 km del evento más reciente del grupo) if (bucket.length === 0 || haversineDistance(bucket[0].latitude, bucket[0].longitude, q.latitude, q.longitude) <= 65) { bucket.push(q); } }); const renderedSwarms = new Set(); let visibleRowIdx = 0; displayItems.forEach((eq, idx) => { const cid = isGroupableSwarm(eq) ? eq._clusterInfo.clusterId : null; const members = cid ? swarmMembersById.get(cid) : null; if (members && members.length >= SWARM_MIN_GROUP) { if (renderedSwarms.has(cid)) return; renderedSwarms.add(cid); frag.appendChild(createSwarmGroupElement(cid, members, mostPowerfulRecent)); } else { const item = createFeedCardElement(eq, idx, false, mostPowerfulRecent); frag.appendChild(item); } // Bento Ad Slot Cero CLS intercalado tras la 5ª fila visible (In-Feed Native) if (visibleRowIdx === 4) { const adCard = createBentoFeedAdCard(); if (adCard) frag.appendChild(adCard); } visibleRowIdx++; }); if (filtered.length > displayItems.length) { const remaining = filtered.length - displayItems.length; const loadMoreBtn = document.createElement('button'); loadMoreBtn.className = 'w-full py-2.5 px-3 mt-2 mb-4 text-xs font-semibold text-sky-400 bg-slate-900/90 hover:bg-slate-800 border border-sky-500/30 hover:border-sky-400 rounded-xl transition-all shadow-md flex items-center justify-center gap-1.5 cursor-pointer'; loadMoreBtn.innerHTML = `⬇️Cargar más sismos (+${Math.min(remaining, 50)}) · Mostrando ${displayItems.length} de ${filtered.length}`; loadMoreBtn.onclick = () => { window._feedVisibleLimit = (window._feedVisibleLimit || 60) + 50; renderFeedList(); }; frag.appendChild(loadMoreBtn); } if (Array.isArray(window._preservedHubSeoNodes) && activeCountryFilter && activeCountryFilter !== 'all') { window._preservedHubSeoNodes.forEach(n => { if (n) frag.appendChild(n); }); } container.appendChild(frag); if (typeof window.mountMktV3BentoAtFeedPos3 === 'function') { try { window.mountMktV3BentoAtFeedPos3(); } catch (_) {} } container.scrollTop = 0; } // Fase 198.5 (Entrada #638): Fila agrupada de enjambre sísmico con despliegue de los sismos individuales (datos reales) function createSwarmGroupElement(clusterId, members, mostPowerfulRecent) { const wrap = document.createElement('div'); wrap.className = 'swarm-group-wrap'; wrap.setAttribute('data-swarm-id', clusterId); const sorted = [...members].sort((a, b) => (b.time ? b.time.getTime() : 0) - (a.time ? a.time.getTime() : 0)); const newest = sorted[0]; const times = sorted.map(q => (q.time ? q.time.getTime() : 0)).filter(Boolean); const spanH = times.length > 1 ? (Math.max(...times) - Math.min(...times)) / 3600000 : 0; const spanLabel = spanH < 1 ? `${Math.max(1, Math.round(spanH * 60))} min` : (spanH < 48 ? `${Math.round(spanH)} h` : `${Math.round(spanH / 24)} días`); const maxMag = Math.max(...sorted.map(q => Number(q.magnitude) || 0)); const geo = parseSeismicPlace(newest.place, newest.nearestCity, newest.latitude, newest.longitude); const minutesAgo = Math.max(Math.floor((Date.now() - (newest.time ? newest.time.getTime() : Date.now())) / 60000), 0); const lastAgo = typeof formatElapsedHoursMins === 'function' ? formatElapsedHoursMins(minutesAgo, true) : `${minutesAgo} min`; const header = document.createElement('button'); header.type = 'button'; header.className = 'swarm-group-header w-full text-left p-3 rounded-2xl bg-gradient-to-r from-purple-950/70 via-slate-900/90 to-slate-950/90 border border-purple-500/45 hover:border-purple-400 flex items-center gap-3 cursor-pointer transition-all'; header.setAttribute('aria-expanded', 'false'); header.innerHTML = `
🐝 ${sorted.length}
${geo.flag || '🌐'} Enjambre sísmico • ${geo.cleanCountry || geo.country}
${sorted.length} sismos en ${spanLabel} • Mayor M${maxMag.toFixed(1)}
📍 ${geo.cleanLocation || geo.locality} • el último ${String(lastAgo).toLowerCase()}
Ver ▾ `; const list = document.createElement('div'); list.className = 'swarm-group-members hidden space-y-2 mt-2 pl-2 border-l-2 border-purple-500/40'; let built = false; header.onclick = () => { const willOpen = list.classList.contains('hidden'); if (willOpen && !built) { sorted.forEach((q, i) => list.appendChild(createFeedCardElement(q, i + 10, false, mostPowerfulRecent))); built = true; } list.classList.toggle('hidden', !willOpen); header.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); const tg = header.querySelector('.swarm-group-toggle'); if (tg) tg.textContent = willOpen ? 'Ocultar ▴' : 'Ver ▾'; if (willOpen) { // Sin mover el globo: syncGlobeToEvent colapsa el panel en móvil y el usuario perdería la lista que acaba de abrir try { if (typeof window.trackPhase148Tool === 'function') window.trackPhase148Tool('swarm_group_expand'); } catch (_) {} } }; wrap.appendChild(header); wrap.appendChild(list); return wrap; } function formatShortTimeAgo(timestamp) { if (!timestamp) return 'S/D'; const diffMs = Date.now() - Number(timestamp); if (diffMs < 0) return 'Ahora'; const mins = Math.floor(diffMs / 60000); if (mins < 1) return '< 1m'; if (mins < 60) return `${mins}m`; const hours = Math.floor(mins / 60); const remMins = mins % 60; if (hours < 24) return `${hours}h ${remMins}m`; const days = Math.floor(hours / 24); const remHours = hours % 24; return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`; } function renderTopRankings() { const container = document.getElementById('top-rankings-container'); if (!container) return; const now = Date.now(); const ONE_DAY_MS = 24 * 60 * 60 * 1000; // Filtrar sismos reales de la ventana móvil de las últimas 24 horas (estricto) const events24h = (earthquakesData || []).filter(eq => { if (!eq) return false; const mag = (typeof eq.magnitude === 'number' && !isNaN(eq.magnitude)) ? eq.magnitude : parseFloat(eq.magnitude); if (isNaN(mag)) return false; const t = (eq.time instanceof Date) ? eq.time.getTime() : new Date(eq.time || 0).getTime(); return (now - t) <= ONE_DAY_MS && (now - t) >= -60000; }); // REGLA DE ORO 1: CERO fallback indiscriminado a catálogos de 7 o 30 días const dataset = events24h; const sorted24h = [...dataset].sort((a, b) => { const magA = (typeof a.magnitude === 'number' && !isNaN(a.magnitude)) ? a.magnitude : (parseFloat(a.magnitude) || 0); const magB = (typeof b.magnitude === 'number' && !isNaN(b.magnitude)) ? b.magnitude : (parseFloat(b.magnitude) || 0); if (Math.abs(magB - magA) > 0.001) return magB - magA; // En empate de magnitud, priorizar mayor consenso multi-agencia y alfabeto latino legible const agA = Array.isArray(a.validatingAgencies) ? a.validatingAgencies.length : 1; const agB = Array.isArray(b.validatingAgencies) ? b.validatingAgencies.length : 1; if (agB !== agA) return agB - agA; const tA = (a.time instanceof Date) ? a.time.getTime() : new Date(a.time || 0).getTime(); const tB = (b.time instanceof Date) ? b.time.getTime() : new Date(b.time || 0).getTime(); return tB - tA; }); const top3 = []; for (const cand of sorted24h) { if (top3.length >= 3) break; const cTime = (cand.time instanceof Date) ? cand.time.getTime() : new Date(cand.time || 0).getTime(); const cLat = Number(cand.latitude ?? cand.lat ?? 0); const cLng = Number(cand.longitude ?? cand.lon ?? cand.lng ?? 0); const isDup = top3.some(picked => { const pTime = (picked.time instanceof Date) ? picked.time.getTime() : new Date(picked.time || 0).getTime(); const pLat = Number(picked.latitude ?? picked.lat ?? 0); const pLng = Number(picked.longitude ?? picked.lon ?? picked.lng ?? 0); return Math.abs(cTime - pTime) < 20 * 60 * 1000 && Math.abs(cLat - pLat) < 2.5 && Math.abs(cLng - pLng) < 2.5; }); if (!isDup) top3.push(cand); } window.currentTop3Earthquakes = top3; container.innerHTML = ''; if (top3.length === 0) { container.innerHTML = `
S/D en las últimas 24 horas
`; return; } const podiumConfig = [ { rank: '#1', medal: '🥇', border: 'border-amber-400/80', badgeBg: 'bg-amber-400/20 text-amber-300 border-amber-400/40', gradient: 'bg-gradient-to-b from-amber-500/15 via-slate-900/90 to-slate-950/95 shadow-lg shadow-amber-500/10' }, { rank: '#2', medal: '🥈', border: 'border-slate-300/70', badgeBg: 'bg-slate-300/20 text-slate-200 border-slate-300/40', gradient: 'bg-gradient-to-b from-slate-400/15 via-slate-900/90 to-slate-950/95 shadow-lg shadow-slate-400/10' }, { rank: '#3', medal: '🥉', border: 'border-amber-700/70', badgeBg: 'bg-amber-700/20 text-amber-400 border-amber-700/40', gradient: 'bg-gradient-to-b from-amber-700/15 via-slate-900/90 to-slate-950/95 shadow-lg shadow-amber-700/10' } ]; top3.forEach((eq, idx) => { const pod = podiumConfig[idx] || podiumConfig[2]; const geo = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude) : { flag: '🌐', locality: eq.place, country: '' }; const timeAgo = formatShortTimeAgo(eq.time); const depthVal = (typeof eq.depth === 'number' && eq.depth >= 0) ? `${Math.round(eq.depth)}km` : 'S/D'; const isShallow = typeof eq.depth === 'number' && eq.depth >= 0 && eq.depth < 70; const depthTag = typeof eq.depth === 'number' && eq.depth >= 0 ? (isShallow ? `SUP` : `PROF`) : ''; let magColor = 'text-sky-300'; if (eq.magnitude >= 7.0) magColor = 'text-rose-400'; else if (eq.magnitude >= 6.0) magColor = 'text-amber-400'; const agenciesCompact = (eq.validatingAgencies && eq.validatingAgencies.length) ? eq.validatingAgencies.slice(0, 2).join('·') : 'USGS'; const agenciesFull = (eq.validatingAgencies && eq.validatingAgencies.length) ? eq.validatingAgencies.join(' • ') : 'USGS'; const safePlace = typeof sanitizeSeismicText === 'function' ? sanitizeSeismicText(eq.place) : eq.place; const countryName = String(geo.cleanCountry || geo.country || 'Global').trim(); const rawLocality = String(geo.cleanLocation || geo.locality || eq.place || '').trim(); const compactLocality = rawLocality .replace(/\s*km\s+(?:al\s+)?Noreste\s+de\s+/gi, 'km NE ') .replace(/\s*km\s+(?:al\s+)?Noroeste\s+de\s+/gi, 'km NO ') .replace(/\s*km\s+(?:al\s+)?Sureste\s+de\s+/gi, 'km SE ') .replace(/\s*km\s+(?:al\s+)?Suroeste\s+de\s+/gi, 'km SO ') .replace(/\s*km\s+(?:al\s+)?Norte\s+de\s+/gi, 'km N ') .replace(/\s*km\s+(?:al\s+)?Sur\s+de\s+/gi, 'km S ') .replace(/\s*km\s+(?:al\s+)?Este\s+de\s+/gi, 'km E ') .replace(/\s*km\s+(?:al\s+)?Oeste\s+de\s+/gi, 'km O ') .replace(/\s*km\s+(?:al\s+)?([NSEO]{1,2})-([NSEO]{1,2})\s+de\s+/gi, 'km $1$2 ') .replace(/\bVillage\b/gi, '') .trim(); const card = document.createElement('div'); card.setAttribute('data-eq-id', eq.id); card.className = `p-2 rounded-xl border ${pod.border} ${pod.gradient} cursor-pointer hover:scale-[1.02] hover:border-sky-400/80 transition-all duration-200 flex flex-col justify-between group backdrop-blur-md min-h-[104px]`; card.title = `${safePlace} - Magnitud ${eq.magnitude.toFixed(1)} (${agenciesFull})\nClic: Centrar globo 3D y abrir ficha técnica detallada`; card.innerHTML = `
${pod.medal} ${pod.rank}
${timeAgo}
M ${eq.magnitude.toFixed(1)} ${depthVal}
${geo.flag || '🌐'} ${countryName}

${compactLocality}

${depthTag} ${eq.tsunami === 1 ? '🌊' : ''}
${agenciesCompact}
`; card.onclick = () => { if (timelapseActive && typeof toggleTimelapsePlay === 'function') toggleTimelapsePlay(); if (typeof syncTimelapseToEvent === 'function') syncTimelapseToEvent(eq); selectTopRankingEvent(eq, card, idx); }; container.appendChild(card); }); } window.renderTopRankings = renderTopRankings; // ==================================================================== // FASE 426 / 427 / 428 / 439: DESPLIEGUE DE DETALLE POR ENCIMA DEL TOP 3 & ENCUADRE GLOBO 3D // ==================================================================== let activeTop3SelectedEvent = null; function selectTopRankingEvent(eq, cardElement, rankIndex) { if (!eq) return; activeTop3SelectedEvent = eq; // 1. Resaltar tarjeta seleccionada en el Top 3 y atenuar las demás const container = document.getElementById('top-rankings-container'); if (container) { container.querySelectorAll('[data-eq-id]').forEach(c => { c.classList.remove('ring-2', 'ring-sky-400', 'scale-[1.03]', 'shadow-sky-500/40', 'opacity-100'); c.classList.add('opacity-70'); }); } if (cardElement) { cardElement.classList.remove('opacity-70'); cardElement.classList.add('ring-2', 'ring-sky-400', 'scale-[1.03]', 'shadow-sky-500/40', 'opacity-100'); } // Determinar medalla y rango let podLabel = '🏆 TOP 24H'; let podBadge = 'bg-sky-950/80 text-sky-300 border-sky-500/40'; const actualRank = (typeof rankIndex === 'number') ? rankIndex : (window.currentTop3Earthquakes ? window.currentTop3Earthquakes.findIndex(t => String(t.id) === String(eq.id)) : -1); if (actualRank === 0) { podLabel = '🥇 #1 MÁS FUERTE (24H)'; podBadge = 'bg-amber-950/80 text-amber-300 border-amber-500/50'; } else if (actualRank === 1) { podLabel = '🥈 #2 MÁS FUERTE (24H)'; podBadge = 'bg-slate-900/90 text-slate-200 border-slate-400/50'; } else if (actualRank === 2) { podLabel = '🥉 #3 MÁS FUERTE (24H)'; podBadge = 'bg-amber-950/70 text-amber-400 border-amber-700/50'; } // 2. Renderizar y desplegar la tarjeta de detalle POR ENCIMA del Top 3 const detailPanel = document.getElementById('top3-selected-detail'); if (detailPanel) { const magVal = (typeof eq.magnitude === 'number') ? eq.magnitude : (parseFloat(eq.magnitude) || 0); const pPlace = typeof parseSeismicPlace === 'function' ? parseSeismicPlace(eq.place, eq.nearestCity, eq.latitude, eq.longitude) : { flag: '🌐', locality: eq.place, country: '' }; const safePlace = typeof sanitizeSeismicText === 'function' ? sanitizeSeismicText(eq.place) : eq.place; const countryTitle = String(pPlace.cleanCountry || pPlace.country || '').trim(); const localityTitle = String(pPlace.cleanLocation || pPlace.locality || safePlace).trim(); const timeAgo = typeof formatShortTimeAgo === 'function' ? formatShortTimeAgo(eq.time) : 'Reciente'; const depthVal = (typeof eq.depth === 'number' && eq.depth >= 0) ? `${Math.round(eq.depth)} km` : 'S/D'; const isShallow = typeof eq.depth === 'number' && eq.depth >= 0 && eq.depth < 70; const depthTag = typeof eq.depth === 'number' && eq.depth >= 0 ? (isShallow ? `🟢 Superficial (< 70 km)` : (eq.depth < 300 ? `🔵 Intermedio (70-300 km)` : `🟣 Muy Profundo (> 300 km)`)) : 'S/D'; const agencies = (eq.validatingAgencies && eq.validatingAgencies.length) ? eq.validatingAgencies.slice(0, 3).join(' • ') : 'USGS'; const latVal = (typeof eq.latitude === 'number') ? eq.latitude : (parseFloat(eq.lat) || 0); const lngVal = (typeof eq.longitude === 'number') ? eq.longitude : (parseFloat(eq.lon || eq.lng) || 0); const coordsText = `${latVal >= 0 ? latVal.toFixed(2) + '°N' : Math.abs(latVal).toFixed(2) + '°S'}, ${lngVal >= 0 ? lngVal.toFixed(2) + '°E' : Math.abs(lngVal).toFixed(2) + '°W'}`; let magBadgeColor = 'from-sky-500/30 to-blue-900/50 text-sky-300 border-sky-400/60 shadow-sky-500/20'; if (magVal >= 7.0) magBadgeColor = 'from-rose-600/60 to-purple-900/70 text-rose-100 border-rose-400/80 shadow-rose-600/40'; else if (magVal >= 6.0) magBadgeColor = 'from-orange-500/40 to-rose-900/60 text-orange-100 border-orange-400/80 shadow-orange-500/30'; else if (magVal >= 5.0) magBadgeColor = 'from-amber-500/35 to-amber-900/50 text-amber-200 border-amber-400/70 shadow-amber-500/25'; // Formato de hora exacta UTC const eventDate = eq.time ? new Date(eq.time) : null; const utcTimeStr = eventDate && !isNaN(eventDate.getTime()) ? `${String(eventDate.getUTCHours()).padStart(2,'0')}:${String(eventDate.getUTCMinutes()).padStart(2,'0')} UTC` : ''; detailPanel.innerHTML = `
${podLabel} VIVO
MAG ${magVal.toFixed(1)}
${pPlace.flag || '🌐'} ${countryTitle ? `${countryTitle}•` : ''}

${localityTitle}

🕒 ${timeAgo} ${utcTimeStr ? `(${utcTimeStr})` : ''} • 📏 ${depthVal}
Tipo de Foco
${depthTag}
Coordenadas Reales 📍 ${coordsText}
${eq.tsunami === 1 ? '🌊 Alerta Tsunami' : '🟢 Sin tsunami'} • ${agencies}
${(eq.felt && eq.felt > 0) ? `⚡ ${eq.felt} reportes DYFI` : 'A la espera DYFI'}
AD
Sismosfera • Red Geocientífica 24/7
Espacio patrocinado • Monitoreo sísmico en tiempo real
Sismosfera Ads
`; detailPanel.classList.remove('hidden'); // Carga asíncrona segura del anuncio discreto en el desplegable (Fase 428) setTimeout(() => { try { const adIns = detailPanel.querySelector('.adsbygoogle'); if (adIns && !adIns.hasAttribute('data-adsbygoogle-status')) { (window.adsbygoogle = window.adsbygoogle || []).push({}); } } catch (e) { // Silencioso ante adblockers } }, 120); } // 3. Ubicar el Globo 3D de forma tal que se vea perfectamente dónde fue positionGlobeForEvent(eq); } function positionGlobeForEvent(eq) { if (!myGlobe || !eq) return; const rawLat = (eq.latitude != null) ? eq.latitude : ((eq.lat != null) ? eq.lat : 0); const rawLng = (eq.longitude != null) ? eq.longitude : ((eq.lon != null) ? eq.lon : ((eq.lng != null) ? eq.lng : 0)); const lat = isFinite(Number(rawLat)) ? Math.max(-89.9, Math.min(89.9, Number(rawLat))) : 0; const lng = isFinite(Number(rawLng)) ? Math.max(-180, Math.min(180, Number(rawLng))) : 0; // En pantallas móviles / estrechas, colapsar sidebar para maximizar área visible del globo if (window.innerWidth < 1024) { const sb = document.getElementById('left-sidebar'); const bs = document.getElementById('btn-show-sidebar'); if (sb && !sb.classList.contains('opacity-0')) { sb.classList.add('-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); if (bs) bs.classList.remove('hidden'); } } // Detener autorotación const controls = myGlobe.controls(); if (controls) controls.autoRotate = false; // Cálculo de altitud y desplazamiento para que el epicentro quede en la zona libre visible del globo const isMobile = window.innerWidth < 1024; // En móvil el widget y el detalle ocupan la zona inferior; desplazamos el objetivo hacia el sur (latOffset negativo) // para que el epicentro en la esfera suba hacia el tercio superior/medio de la pantalla visible const latOffset = isMobile ? -9.5 : -3.5; const targetLat = Math.max(-85, Math.min(85, lat + latOffset)); const targetAlt = isMobile ? 1.50 : 1.65; myGlobe.pointOfView({ lat: targetLat, lng: lng, altitude: targetAlt }, 1400); // Seleccionar evento y refrescar baliza parpadeante y punto incandescente en tiempo real focusedEvent = eq; selectedEvent = eq; eq._selected = true; if (Array.isArray(earthquakesData)) { earthquakesData.forEach(q => { q._selected = (String(q.id) === String(eq.id)); }); } refreshGlobeLayers(); if (typeof highlightSidebarCard === 'function') highlightSidebarCard(eq.id, true); } function closeTop3Detail() { const detailPanel = document.getElementById('top3-selected-detail'); if (detailPanel) detailPanel.classList.add('hidden'); const container = document.getElementById('top-rankings-container'); if (container) { container.querySelectorAll('[data-eq-id]').forEach(c => { c.classList.remove('ring-2', 'ring-sky-400', 'scale-[1.03]', 'shadow-sky-500/40', 'opacity-70', 'opacity-75'); c.classList.add('opacity-100'); }); } activeTop3SelectedEvent = null; } function recenterTop3Event() { if (activeTop3SelectedEvent) { positionGlobeForEvent(activeTop3SelectedEvent); } } function openFullDetailFromTop3() { if (activeTop3SelectedEvent) { openEventDetails(activeTop3SelectedEvent); } } window.selectTopRankingEvent = selectTopRankingEvent; window.positionGlobeForEvent = positionGlobeForEvent; window.closeTop3Detail = closeTop3Detail; window.recenterTop3Event = recenterTop3Event; window.openFullDetailFromTop3 = openFullDetailFromTop3; function renderD3EnergyChart() { const chartContainer = document.getElementById('d3-energy-chart'); if (!chartContainer) return; chartContainer.innerHTML = ''; const buckets = [ { range: '< 4.0', total: 0, count: 0 }, { range: '4.0-4.9', total: 0, count: 0 }, { range: '5.0-5.9', total: 0, count: 0 }, { range: '6.0-6.9', total: 0, count: 0 }, { range: '>= 7.0', total: 0, count: 0 } ]; earthquakesData.forEach(eq => { const m = eq.magnitude; if (m < 4.0) { buckets[0].total += eq.energyJoules; buckets[0].count++; } else if (m < 5.0) { buckets[1].total += eq.energyJoules; buckets[1].count++; } else if (m < 6.0) { buckets[2].total += eq.energyJoules; buckets[2].count++; } else if (m < 7.0) { buckets[3].total += eq.energyJoules; buckets[3].count++; } else { buckets[4].total += eq.energyJoules; buckets[4].count++; } }); const width = 340; const height = 130; const margin = { top: 10, right: 10, bottom: 22, left: 50 }; const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom; const svg = d3.select(chartContainer) .append('svg') .attr('width', width) .attr('height', height); const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`); const x = d3.scaleBand() .domain(buckets.map(d => d.range)) .range([0, innerW]) .padding(0.3); const maxVal = d3.max(buckets, d => Math.max(d.total, 1)) || 1000; const y = d3.scaleLog() .domain([1, maxVal * 2]) .range([innerH, 0]); g.append('g') .attr('transform', `translate(0,${innerH})`) .call(d3.axisBottom(x).tickSize(0)) .selectAll('text') .attr('fill', '#94a3b8') .attr('font-size', '9px'); g.select('.domain').attr('stroke', '#334155'); const yAxis = d3.axisLeft(y).ticks(3, '~e').tickSize(-innerW); const yG = g.append('g').call(yAxis); yG.selectAll('text').attr('fill', '#64748b').attr('font-size', '8px'); yG.selectAll('line').attr('stroke', '#1e293b').attr('stroke-dasharray', '2,2'); yG.select('.domain').remove(); g.selectAll('.bar') .data(buckets) .enter() .append('rect') .attr('x', d => x(d.range)) .attr('y', d => y(Math.max(d.total, 1))) .attr('width', x.bandwidth()) .attr('height', d => innerH - y(Math.max(d.total, 1))) .attr('fill', '#ef4444') .attr('rx', 3); } // ==================================================================== // MOTOR DE SINCRONIZACIÓN DINÁMICA GLOBO 3D & LISTADO SÍSMICO EN VIVO // ==================================================================== let syncEngineActive = true; // Activo por defecto: el globo sigue los eventos del listado autopilotActive = true; let syncEngineTimer = null; let syncCurrentIndex = 0; let syncManualPauseTimer = null; let isUserManuallyInteracting = false; let realtimeArrivalQueue = []; let isProcessingArrivalQueue = false; // Resaltar tarjeta en el feed lateral, insertar badge de foco 3D y scroll suave function highlightSidebarCard(targetId, pulseBadge = true) { const feedContainer = document.getElementById('seismic-feed-list'); if (!feedContainer) return; const cards = feedContainer.querySelectorAll('[data-eq-id]'); cards.forEach(card => { const id = card.getAttribute('data-eq-id'); const isTarget = (String(id) === String(targetId)); // Remover badge anterior si existía const oldBadge = card.querySelector('.feed-sync-focus-badge'); if (oldBadge) { const p = oldBadge.parentElement; oldBadge.remove(); if (p && p.classList.contains('feed-card-badges-row') && p.children.length === 0) { p.classList.add('hidden'); } } if (isTarget) { card.classList.add( 'ring-2', 'ring-sky-400', 'border-sky-400', 'bg-sky-950/60', 'shadow-2xl', 'shadow-sky-500/30', 'scale-[1.015]' ); card.classList.remove('border-slate-800/80'); if (pulseBadge) { // Solo inyectar badge 'EN FOCO 3D' si la tarjeta no es la más reciente (que ya tiene el banner de globo) const hasMostRecentBanner = card.querySelector('.feed-most-recent-banner'); if (!hasMostRecentBanner) { const badgesContainer = card.querySelector('.feed-card-badges-row'); if (badgesContainer) { badgesContainer.classList.remove('hidden'); const badge = document.createElement('span'); badge.className = 'feed-sync-focus-badge px-1.5 py-0.5 rounded text-[8px] font-mono font-bold bg-sky-500/25 text-sky-300 border border-sky-400/50 animate-pulse flex items-center gap-0.5 shadow-sm'; badge.innerHTML = `📡EN FOCO 3D`; badgesContainer.insertBefore(badge, badgesContainer.firstChild); } } } // Auto-scroll suave para mantener la tarjeta activa en el campo visual del usuario card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } else { card.classList.remove( 'ring-2', 'ring-sky-400', 'border-sky-400', 'bg-sky-950/60', 'shadow-2xl', 'shadow-sky-500/30', 'scale-[1.015]', 'ring-1', 'border-sky-400/80', 'bg-sky-950/40', 'ring-rose-500', 'bg-slate-800/95' ); card.classList.add('border-slate-800/80'); } }); } function removeSidebarHighlights() { const feedContainer = document.getElementById('seismic-feed-list'); if (!feedContainer) return; feedContainer.querySelectorAll('[data-eq-id]').forEach(card => { const oldBadge = card.querySelector('.feed-sync-focus-badge'); if (oldBadge) { const p = oldBadge.parentElement; oldBadge.remove(); if (p && p.classList.contains('feed-card-badges-row') && p.children.length === 0) { p.classList.add('hidden'); } } card.classList.remove( 'ring-2', 'ring-sky-400', 'border-sky-400', 'bg-sky-950/60', 'shadow-2xl', 'shadow-sky-500/30', 'scale-[1.015]', 'ring-1', 'border-sky-400/80', 'bg-sky-950/40', 'ring-rose-500', 'bg-slate-800/95' ); card.classList.add('border-slate-800/80'); }); } // Sincronizar cámara del globo y feed con un evento específico function syncGlobeToEvent(eq, customDuration = 2200, showHud = true) { if (!eq || !myGlobe) return; focusedEvent = eq; activeAutopilotEvent = eq; // 1. Mover cámara con vuelo cinemático orbital o de aproximación suave focusEarthquake(eq, customDuration); // 2. Resaltar tarjeta correspondiente en el listado lateral highlightSidebarCard(eq.id, true); // 3. Actualizar HUD de telemetría superior if (showHud) { const isMostRecent = Boolean(earthquakesData.length > 0 && String(earthquakesData[0].id) === String(eq.id)); updateAutopilotHud(eq, isMostRecent); } // 4. Refrescar balizas luminosas en el globo 3D refreshGlobeLayers(); } // Centrar simultáneamente el globo 3D y destacar el evento más reciente en el listado function syncGlobeToMostRecentEvent(force = true) { if (!earthquakesData || earthquakesData.length === 0 || !myGlobe) return; // Enfoque DOM-First: Identificar el sismo que efectivamente encabeza la vista del usuario en el listado const feedContainer = document.getElementById('seismic-feed-list'); let mostRecent = null; if (feedContainer) { const firstCard = feedContainer.querySelector('[data-eq-id]'); if (firstCard) { const firstId = firstCard.getAttribute('data-eq-id'); mostRecent = earthquakesData.find(e => String(e.id) === String(firstId)); } } if (!mostRecent) { mostRecent = earthquakesData[0]; } if (!mostRecent) return; focusedEvent = mostRecent; activeAutopilotEvent = mostRecent; // 1. Vuelo cinemático hacia el epicentro del sismo más reciente focusEarthquake(mostRecent, 2200); // 2. Resaltar tarjeta en el listado lateral highlightSidebarCard(mostRecent.id, true); // 3. Mostrar el HUD superior con título de evento más reciente updateAutopilotHud(mostRecent, true); // 4. Refrescar capas y ondas expansivas concéntricas en el globo refreshGlobeLayers(); } // Avance secuencial en el ciclo de sincronización (garantiza mantener el sismo más reciente en pantalla) function stepGlobeSync(manualForward = false) { if (isUserManuallyInteracting) return; syncGlobeToMostRecentEvent(true); } // Activar o pausar la sincronización dinámica global function setSyncEngineState(active) { syncEngineActive = active; autopilotActive = active; // Sincronizar UI del switch en el sidebar const chkAutoCenter = document.getElementById('chk-auto-center'); if (chkAutoCenter && chkAutoCenter.checked !== active) { chkAutoCenter.checked = active; } const txtAutoCenter = document.getElementById('txt-auto-center'); if (txtAutoCenter) { if (active) { txtAutoCenter.innerHTML = ` Sincronizar Globo `; } else { txtAutoCenter.innerHTML = ` Globo Quieto `; } } // Sincronizar UI del botón en la cabecera const dot = document.getElementById('autopilot-dot'); const text = document.getElementById('autopilot-text'); if (dot && text) { if (active) { dot.className = 'w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping'; text.className = 'text-emerald-400 font-mono font-bold'; text.innerText = 'ON'; } else { dot.className = 'w-2.5 h-2.5 rounded-full bg-slate-500'; text.className = 'text-slate-400 font-mono font-bold'; text.innerText = 'OFF'; } } const hud = document.getElementById('autopilot-hud'); if (syncEngineTimer) { clearInterval(syncEngineTimer); syncEngineTimer = null; } if (active) { // Al activar, centrar de inmediato en el más reciente y permanecer quieto esperando nuevos sismos syncGlobeToMostRecentEvent(true); } else { if (hud) hud.classList.add('hidden'); removeSidebarHighlights(); refreshGlobeLayers(); } } function toggleAutopilot() { setSyncEngineState(!syncEngineActive); } const flyNextAutopilot = syncGlobeToMostRecentEvent; // Pausa temporal cuando el usuario interactúa manualmente con el globo 3D function pauseAutoSyncTemporary(ms = 12000) { isUserManuallyInteracting = true; if (syncManualPauseTimer) clearTimeout(syncManualPauseTimer); syncManualPauseTimer = setTimeout(() => { isUserManuallyInteracting = false; }, ms); } function updateAutopilotHud(target, isMostRecent = false) { // Retirado por directiva de diseño (Fase 54.0) para mantener la vista orbital 100% limpia return; const hud = document.getElementById('autopilot-hud'); if (!hud) return; hud.classList.remove('hidden'); const t = I18N_TRANSLATIONS[currentLanguage] || I18N_TRANSLATIONS['es']; const hudTitle = document.getElementById('autopilot-hud-title'); if (hudTitle) { hudTitle.innerHTML = isMostRecent ? `🔴 ÚLTIMO SISMO • EN VIVO` : `🧭 SISMO ENFOCADO`; } const magEl = document.getElementById('autopilot-hud-mag'); if (magEl) { magEl.innerText = `M ${target.magnitude.toFixed(1)}`; let bg = 'bg-emerald-600/70 border-emerald-400'; if (target.magnitude >= 7.0) bg = 'bg-red-600/80 border-red-400'; else if (target.magnitude >= 6.0) bg = 'bg-orange-600/80 border-orange-400'; else if (target.magnitude >= 4.5) bg = 'bg-amber-600/80 border-amber-400'; magEl.className = `px-1.5 py-0.5 rounded font-black text-white font-mono text-[11px] sm:text-xs border flex-shrink-0 ${bg}`; } const placeEl = document.getElementById('autopilot-hud-place'); if (placeEl) { const parsed = parseSeismicPlace(target.place, target.nearestCity, target.latitude, target.longitude); placeEl.innerText = `${parsed.country}, ${parsed.province}, ${parsed.locality}`; } const depthEl = document.getElementById('autopilot-hud-depth'); if (depthEl) { depthEl.innerText = `${t.depth_label}: ${target.depth.toFixed(1)} km`; } const btnDet = document.getElementById('btn-autopilot-hud-details'); if (btnDet) { btnDet.onclick = () => openEventDetails(target); } } // ==================================================================== // ==================================================================== // CAPAS DINÁMICAS DEL GLOBO 3D (SISMOS + MI UBICACIÓN GPS) // ==================================================================== function refreshGlobeLayers() { if (!myGlobe) return; // 1. Identificación Robusta del Sismo Enfocado / Seleccionado const targetEvent = focusedEvent || selectedEvent || (Array.isArray(earthquakesData) && earthquakesData.length > 0 ? earthquakesData[0] : null); // 2. Capa de Epicentros Parpadeantes (RingsData — Ondas Concéntricas Dinámicas a 60 FPS) const allRings = []; // A. Sismo Enfocado / Seleccionado: Baliza de pulso rápido cian neón de alta visibilidad if (targetEvent) { const tLat = Number(targetEvent.latitude != null ? targetEvent.latitude : targetEvent.lat); const tLng = Number(targetEvent.longitude != null ? targetEvent.longitude : (targetEvent.lon != null ? targetEvent.lon : targetEvent.lng)); allRings.push({ latitude: tLat, longitude: tLng, ringMaxDegrees: 4.8, propagationSpeed: 1.5, repeatPeriod: 1100, ringColor: (t) => { const alpha = Math.max(0, Math.sin((1 - t) * Math.PI) * 0.85 * (1 - t * 0.35)); return `rgba(56, 189, 248, ${alpha})`; } }); } // B. Catálogo Activo de Epicentros Parpadeantes (ondas concéntricas proporcionales a su energía) const activeQuakes = Array.isArray(earthquakesData) ? earthquakesData : []; activeQuakes.forEach(eq => { if (targetEvent && String(eq.id) === String(targetEvent.id)) return; const mag = Number(eq.magnitude) || 4.0; const ringDeg = Math.max(1.4, Math.min(3.6, 0.9 + mag * 0.35)); const repPeriod = Math.max(1200, 2400 - Math.round(mag * 150)); allRings.push({ latitude: Number(eq.latitude != null ? eq.latitude : eq.lat), longitude: Number(eq.longitude != null ? eq.longitude : (eq.lon != null ? eq.lon : eq.lng)), magnitude: mag, ringMaxDegrees: ringDeg, propagationSpeed: 1.1, repeatPeriod: repPeriod, ringColor: (t) => { const alpha = Math.max(0, Math.sin((1 - t) * Math.PI) * 0.55 * (1 - t * 0.5)); if (mag >= 7.0) return `rgba(239, 68, 68, ${alpha})`; if (mag >= 6.0) return `rgba(249, 115, 22, ${alpha * 0.95})`; if (mag >= 5.0) return `rgba(234, 179, 8, ${alpha * 0.90})`; if (mag >= 4.0) return `rgba(132, 204, 22, ${alpha * 0.85})`; return `rgba(16, 185, 129, ${alpha * 0.80})`; } }); }); // C. Pulso de Micro-Radar de Ubicación del Usuario if (userLocation) { allRings.push({ latitude: userLocation.lat, longitude: userLocation.lng, ringMaxDegrees: 1.8, propagationSpeed: 1.0, repeatPeriod: 1800, ringColor: (t) => `rgba(14, 165, 233, ${Math.max(0, Math.sin((1 - t) * Math.PI) * 0.40 * (1 - t * 0.6))})` }); } myGlobe.ringsData(allRings); // 3. Capa de Epicentros Sísmicos Globales (PointsData con Ordenamiento de Capas para Sismo Enfocado) if (targetEvent) { earthquakesData.forEach(q => { q._selected = (String(q.id) === String(targetEvent.id)); }); } // El sismo enfocado se ubica al final del array para ser dibujado en la capa superior absoluta let renderPoints = [...earthquakesData]; if (targetEvent) { const others = earthquakesData.filter(e => String(e.id) !== String(targetEvent.id)); renderPoints = [...others, targetEvent]; } myGlobe.pointsData(renderPoints); // 3. Objetos 3D de Infraestructura Crítica (GPS Usuario, Boyas DART, Sismógrafos FDSN) const infrastructureObjects = []; if (userLocation) { infrastructureObjects.push({ latitude: userLocation.lat, longitude: userLocation.lng, isUser: true, accuracy: userLocation.accuracy }); } // Boyas DART de NOAA destacadas en el Globo 3D const showDart = document.getElementById('chk-dart-buoys') ? document.getElementById('chk-dart-buoys').checked : true; if (showDart && Array.isArray(dartStationsData)) { dartStationsData.forEach(b => { infrastructureObjects.push({ latitude: Number(b.lat), longitude: Number(b.lon), id: b.id, name: b.name, owner: b.owner, program: b.program, isBuoy: true }); }); } // Sismógrafos de la Red Sísmica Global (FDSN / GSN) destacados en el Globo 3D const showSeismic = document.getElementById('chk-seismographs') ? document.getElementById('chk-seismographs').checked : true; if (showSeismic && Array.isArray(seismicStationsData)) { seismicStationsData.forEach(s => { infrastructureObjects.push({ latitude: Number(s.lat), longitude: Number(s.lon), code: s.code, net: s.net, sta: s.sta, name: s.name, elevation: s.elevation, networkName: s.networkName, isStation: true }); }); } // Fase 124.1: Memoización por firma — las balizas 3D solo se recrean si cambió su conjunto const objectsSig = infrastructureObjects.map(o => `${o.isUser ? 'U' : o.isBuoy ? 'B' : o.isStation ? 'S' : 'O'}|${o.id || o.code || ''}|${o.latitude}|${o.longitude}|${o.accuracy || ''}`).join('§'); if (objectsSig !== window._lastGlobeObjectsSig) { window._lastGlobeObjectsSig = objectsSig; activeQuantumBeacons = []; myGlobe.objectsData(infrastructureObjects); } // 3. Etiquetas compactas (ciudades, infraestructura y usuario) updateGlobeLabels(); // 4. Mapa de Calor Sísmico (Malla Hexagonal 3D - Fase 400.0) if (heatmapActive) { renderSeismicHeatmapContours(heatmapRange); } else { activeSeismicContourFeatures = []; activeSeismicContourPaths = []; updateVectorLayers(); if (myGlobe) myGlobe.hexBinPointsData([]); } } // ==================================================================== // MOTOR CIENTÍFICO DE CURVAS DE NIVEL SÍSMICAS 3D (FASE 22.0) // ==================================================================== let activeSeismicContourPaths = []; let activeSeismicContourFeatures = activeSeismicContourPaths; // Alias para compatibilidad const seismicContoursCache = {}; const SEISMIC_TIER_CONFIG = [ { tier: 1, name: 'Sismicidad de Base / Tasa de Fondo', threshold: 0.06, color: 'rgba(99, 102, 241, 0.85)', // Índigo Neón capColor: 'rgba(99, 102, 241, 0.85)', sideColor: 'rgba(99, 102, 241, 0.40)', strokeColor: 'rgba(99, 102, 241, 0.85)', strokeWidth: 1.6, altitude: 0.005, dash: 0.15, animateTime: 6000 }, { tier: 2, name: 'Incidencia Leve / Fracturas Activas', threshold: 0.16, color: 'rgba(6, 182, 212, 0.90)', // Cian Turquesa capColor: 'rgba(6, 182, 212, 0.90)', sideColor: 'rgba(6, 182, 212, 0.40)', strokeColor: 'rgba(6, 182, 212, 0.90)', strokeWidth: 2.0, altitude: 0.009, dash: 0.12, animateTime: 5000 }, { tier: 3, name: 'Incidencia Moderada / Fallas Regionales', threshold: 0.32, color: 'rgba(16, 185, 129, 0.95)', // Esmeralda / Menta capColor: 'rgba(16, 185, 129, 0.95)', sideColor: 'rgba(16, 185, 129, 0.45)', strokeColor: 'rgba(16, 185, 129, 0.95)', strokeWidth: 2.4, altitude: 0.015, dash: 0.10, animateTime: 4000 }, { tier: 4, name: 'Incidencia Alta / Cúmulos Tectónicos', threshold: 0.52, color: 'rgba(245, 158, 11, 1.0)', // Ámbar Dorado capColor: 'rgba(245, 158, 11, 1.0)', sideColor: 'rgba(245, 158, 11, 0.50)', strokeColor: 'rgba(245, 158, 11, 1.0)', strokeWidth: 2.8, altitude: 0.022, dash: 0.08, animateTime: 3500 }, { tier: 5, name: 'Concentración Crítica / Falla Principal', threshold: 0.72, color: 'rgba(249, 115, 22, 1.0)', // Naranja Radiante capColor: 'rgba(249, 115, 22, 1.0)', sideColor: 'rgba(249, 115, 22, 0.55)', strokeColor: 'rgba(249, 115, 22, 1.0)', strokeWidth: 3.2, altitude: 0.030, dash: 0.06, animateTime: 3000 }, { tier: 6, name: 'Máxima Severidad / Enjambre & Subducción', threshold: 0.88, color: 'rgba(239, 68, 68, 1.0)', // Carmesí Neón capColor: 'rgba(239, 68, 68, 1.0)', sideColor: 'rgba(239, 68, 68, 0.60)', strokeColor: 'rgba(239, 68, 68, 1.0)', strokeWidth: 3.8, altitude: 0.040, dash: 0.05, animateTime: 2500 } ]; // FASE 140.0 (A4): Web Worker dedicado (Blob URL) para cálculo de matriz Gaussiana KDE 2D (0ms bloqueo en hilo principal) let _kdeHeatmapWorker = null; let _kdeWorkerSeq = 0; const _kdeWorkerPending = new Map(); function getKdeHeatmapWorker() { if (_kdeHeatmapWorker) return _kdeHeatmapWorker; if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined') return null; try { const workerCode = ` self.onmessage = function(e) { const { jobId, points } = e.data || {}; const N_LON = 360; const N_LAT = 180; const grid = new Float32Array(N_LON * N_LAT); const sigma = 2.4; const sigmaSq2 = 2 * sigma * sigma; const radius = Math.ceil(3 * sigma); let maxDensity = 0; if (Array.isArray(points)) { for (let i = 0; i < points.length; i++) { const pt = points[i]; const lat = Number(pt[0]); const lon = Number(pt[1]); const mag = Number(pt[2]); if (isNaN(lat) || isNaN(lon)) continue; const weight = Math.max(0.25, Math.pow(10, 0.32 * ((isNaN(mag) ? 2.5 : mag) - 2.5))); const cx = Math.floor(((lon + 180) / 360) * N_LON) % N_LON; const cy = Math.floor(((90 - lat) / 180) * N_LAT); const cosLat = Math.cos((lat * Math.PI) / 180); const radiusX = Math.min(Math.ceil(radius / Math.max(0.18, cosLat)), Math.floor(N_LON / 4)); for (let dy = -radius; dy <= radius; dy++) { const y = cy + dy; if (y < 0 || y >= N_LAT) continue; for (let dx = -radiusX; dx <= radiusX; dx++) { let x = (cx + dx) % N_LON; if (x < 0) x += N_LON; const dxDeg = dx * cosLat; const dSq = dxDeg * dxDeg + dy * dy; if (dSq <= radius * radius) { const idx = y * N_LON + x; const val = (grid[idx] += weight * Math.exp(-dSq / sigmaSq2)); if (val > maxDensity) maxDensity = val; } } } } } if (maxDensity > 0) { const gamma = 2.0; const logDenom = Math.log10(1 + gamma * maxDensity); for (let i = 0; i < grid.length; i++) { if (grid[i] > 0) { grid[i] = Math.log10(1 + gamma * grid[i]) / logDenom; } } } self.postMessage({ jobId, maxDensity, gridBuffer: grid.buffer }, [grid.buffer]); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); _kdeHeatmapWorker = new Worker(URL.createObjectURL(blob)); _kdeHeatmapWorker.onmessage = (ev) => { const { jobId, maxDensity, gridBuffer } = ev.data || {}; const resolver = _kdeWorkerPending.get(jobId); if (resolver) { _kdeWorkerPending.delete(jobId); resolver({ maxDensity, grid: gridBuffer ? new Float32Array(gridBuffer) : null }); } }; _kdeHeatmapWorker.onerror = () => { for (const [, r] of _kdeWorkerPending.entries()) r(null); _kdeWorkerPending.clear(); }; return _kdeHeatmapWorker; } catch (_) { return null; } } function computeKdeDensityGridAsync(earthquakeList) { return new Promise((resolve) => { if (!Array.isArray(earthquakeList) || earthquakeList.length === 0) return resolve(null); const worker = getKdeHeatmapWorker(); if (!worker) return resolve(null); const jobId = ++_kdeWorkerSeq; const compactPoints = []; for (let i = 0; i < earthquakeList.length; i++) { const eq = earthquakeList[i]; if (!eq) continue; compactPoints.push([Number(eq.latitude), Number(eq.longitude), Number(eq.magnitude ?? 2.5)]); } const timer = setTimeout(() => { if (_kdeWorkerPending.has(jobId)) { _kdeWorkerPending.delete(jobId); resolve(null); } }, 2500); _kdeWorkerPending.set(jobId, (res) => { clearTimeout(timer); resolve(res); }); worker.postMessage({ jobId, points: compactPoints }); }); } function generateSeismicContourPaths(earthquakeList, precomputedKde = null) { if (!earthquakeList || !Array.isArray(earthquakeList) || earthquakeList.length === 0) return []; const N_LON = 360; const N_LAT = 180; let grid = precomputedKde && precomputedKde.grid ? precomputedKde.grid : null; let maxDensity = precomputedKde && typeof precomputedKde.maxDensity === 'number' ? precomputedKde.maxDensity : 0; if (!grid) { grid = new Float32Array(N_LON * N_LAT); const sigma = 2.4; const sigmaSq2 = 2 * sigma * sigma; const radius = Math.ceil(3 * sigma); for (let i = 0; i < earthquakeList.length; i++) { const eq = earthquakeList[i]; const lat = Number(eq.latitude); const lon = Number(eq.longitude); if (isNaN(lat) || isNaN(lon)) continue; // 1. Tipado estricto y soporte para microsismos (incluso mag <= 0.0 sin falsy fallback): const mag = (typeof eq.magnitude === 'number' && !isNaN(eq.magnitude)) ? eq.magnitude : (parseFloat(eq.magnitude) || 2.5); // 2. Ponderación física Gutenberg-Richter calibrada: const weight = Math.max(0.25, Math.pow(10, 0.32 * (mag - 2.5))); const cx = Math.floor(((lon + 180) / 360) * N_LON) % N_LON; const cy = Math.floor(((90 - lat) / 180) * N_LAT); const cosLat = Math.cos((lat * Math.PI) / 180); // 3. Isotropía Geodésica Esférica: expansión longitudinal según latitud (cero distorsión polar) const radiusX = Math.min(Math.ceil(radius / Math.max(0.18, cosLat)), Math.floor(N_LON / 4)); for (let dy = -radius; dy <= radius; dy++) { const y = cy + dy; if (y < 0 || y >= N_LAT) continue; const dyDeg = dy; for (let dx = -radiusX; dx <= radiusX; dx++) { let x = (cx + dx) % N_LON; if (x < 0) x += N_LON; const dxDeg = dx * cosLat; const dSq = dxDeg * dxDeg + dyDeg * dyDeg; if (dSq <= radius * radius) { const idx = y * N_LON + x; const val = (grid[idx] += weight * Math.exp(-dSq / sigmaSq2)); if (val > maxDensity) maxDensity = val; } } } } if (maxDensity <= 0) return []; // 4. Normalización Logarítmica Sub-lineal Anti-Outliers (Data Engineering): const gamma = 2.0; const logDenom = Math.log10(1 + gamma * maxDensity); for (let i = 0; i < grid.length; i++) { if (grid[i] > 0) { grid[i] = Math.log10(1 + gamma * grid[i]) / logDenom; } } } else if (maxDensity <= 0) { return []; } const thresholds = SEISMIC_TIER_CONFIG.map(t => t.threshold); const generatedPaths = []; if (typeof d3 !== 'undefined' && typeof d3.contours === 'function') { try { const contours = d3.contours() .size([N_LON, N_LAT]) .thresholds(thresholds) (grid); contours.forEach((c, idx) => { if (!c || !c.coordinates || c.coordinates.length === 0) return; const cfg = SEISMIC_TIER_CONFIG[idx] || SEISMIC_TIER_CONFIG[SEISMIC_TIER_CONFIG.length - 1]; c.coordinates.forEach(polygon => { polygon.forEach(ring => { if (!ring || ring.length < 4) return; // Convertir coordenadas de pixel [x, y] a [lng, lat] geodésicas const geoPoints = ring.map(([x, y]) => { const lng = Number(((x / N_LON) * 360 - 180).toFixed(3)); const lat = Number((90 - (y / N_LAT) * 180).toFixed(3)); return [lng, lat]; }); // Segmentación antimeridiana para evitar polilíneas que atraviesen el globo terráqueo let currentSegment = []; for (let p = 0; p < geoPoints.length; p++) { const pt = geoPoints[p]; if (currentSegment.length > 0) { const prev = currentSegment[currentSegment.length - 1]; if (Math.abs(pt[0] - prev[0]) > 180) { if (currentSegment.length >= 3) { pushContourSegment(currentSegment, cfg); } currentSegment = []; } } currentSegment.push(pt); } if (currentSegment.length >= 3) { pushContourSegment(currentSegment, cfg); } }); }); }); } catch (err) { console.warn('[SEISMIC-CONTOURS] Fallback en d3.contours:', err); } } function pushContourSegment(coords, cfg) { generatedPaths.push({ isSeismicContour: true, name: `Curva de Nivel: ${cfg.name}`, tier: cfg.tier, threshold: cfg.threshold, altitude: cfg.altitude, color: cfg.strokeColor || cfg.color, stroke: cfg.strokeWidth, dash: cfg.dash, animateTime: cfg.animateTime, coords: coords, label: `
🔥 CURVA DE NIVEL SÍSMICA NIVEL ${cfg.tier}/6
${cfg.name}
Densidad de Energía Sísmica: ${Math.round((cfg.threshold || 0) * 100)}%
Relieve Topográfico 3D: +${Math.round((cfg.altitude || 0.005) * 6371)} km equiv.
` }); } return generatedPaths; } // Alias para compatibilidad con código existente function generateSeismicContourGeoJSON(earthquakeList) { return generateSeismicContourPaths(earthquakeList); } async function renderSeismicHeatmapContours(range = '24h') { heatmapRange = range; const legend = document.getElementById('heatmap-contour-legend'); const badge = document.getElementById('heatmap-legend-range-badge'); if (badge) badge.innerText = range === '24h' ? '24 HORAS' : (range === '7d' ? '7 DÍAS' : '30 DÍAS'); let eqDataset = heatmapCache[range]; if (!eqDataset || eqDataset.length === 0) { if (range === '24h' && earthquakesData && earthquakesData.length > 0) { eqDataset = earthquakesData; heatmapCache['24h'] = earthquakesData; } else { eqDataset = await loadHeatmapData(range); } } if (seismicContoursCache[range]) { activeSeismicContourPaths = seismicContoursCache[range]; } else { // Fase 140.0 (A4 — INP < 30ms): Calcular matriz Gaussiana KDE 2D en Blob Web Worker fuera del hilo principal const workerKde = await computeKdeDensityGridAsync(eqDataset); activeSeismicContourPaths = generateSeismicContourPaths(eqDataset, workerKde); seismicContoursCache[range] = activeSeismicContourPaths; } activeSeismicContourFeatures = activeSeismicContourPaths; if (legend && heatmapActive) { legend.classList.remove('hidden'); const tl = document.getElementById('timelapse-bar'); if (tl) tl.classList.add('timelapse-shifted-right'); } await new Promise(resolve => setTimeout(resolve, 0)); updateVectorLayers(); if (myGlobe) myGlobe.hexBinPointsData([]); } // ==================================================================== // CARGA DE DATOS PARA MAPA DE CALOR 3D (24H, 7D, 30D) // ==================================================================== async function loadHeatmapData(range = '24h') { heatmapRange = range; if (heatmapCache[range]) { if (myGlobe && heatmapActive) { renderSeismicHeatmapContours(range); } return heatmapCache[range]; } showToast(`Cargando sismos oficiales de los últimos ${range === '7d' ? '7 días' : '30 días'} (USGS / Global)...`); let rawFeatures = []; const url = range === '7d' ? 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson' : 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson'; try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); rawFeatures = json.features || []; } catch (err) { console.warn(`[HEATMAP] Error cargando ${range}:`, err.message); showToast(`⚠️ No se pudo descargar el feed de ${range}.`); return heatmapCache['24h'] || earthquakesData; } const parsed = rawFeatures.map(f => { const lat = f.geometry.coordinates[1]; const lon = f.geometry.coordinates[0]; const depth = Math.abs(f.geometry.coordinates[2] || 10); const mag = f.properties.mag || 0; const place = f.properties.place || 'Sismo Global'; const time = new Date(f.properties.time); const nearestCity = findNearestCity(lat, lon); const perceptibility = evaluatePerceptibility(mag, depth, nearestCity ? nearestCity.distanceKm : 9999); const environment = detectEnvironment(place, lat, lon); const continent = detectContinent(lat, lon, place); const plate = detectPlate(lat, lon); const energyJoules = Math.pow(10, 4.8 + 1.5 * mag); return { id: f.id, latitude: lat, longitude: lon, depth: Number(depth.toFixed(1)), magnitude: Number(mag.toFixed(1)), place, time, primaryAuthority: 'USGS', consensusScore: 1, validatingAgencies: ['USGS'], energyJoules, nearestCity, perceptibility, environment, continent, plate, agencyReports: [ { agency: 'USGS', reportedMag: Number(mag.toFixed(1)), reportedDepth: Number(depth.toFixed(1)), reportedTime: time, placeOriginal: place } ] }; }); heatmapCache[range] = parsed; if (myGlobe && heatmapActive) { renderSeismicHeatmapContours(range); } showToast(`🔥 Curvas de nivel sísmicas activas: ${parsed.length} sismos analizados.`); return parsed; } // ==================================================================== // EXPLORADOR Y CENTRO DE HISTORIAL MULTIDIMENSIONAL // ==================================================================== async function updateHistoryDataset(timeRange = '24h') { const tbody = document.getElementById('history-table-tbody'); if (tbody) { tbody.innerHTML = 'Cargando catálogo oficial de sismos...'; } let dataset = heatmapCache[timeRange]; if (!dataset) { dataset = await loadHeatmapData(timeRange); } historyDataset = dataset || earthquakesData; applyHistoryFilters(); } function applyHistoryFilters() { const query = (document.getElementById('filter-search')?.value || '').toLowerCase().trim(); const minMagVal = document.getElementById('filter-mag')?.value || 'all'; const continentVal = document.getElementById('filter-continent')?.value || 'all'; const plateVal = document.getElementById('filter-plate')?.value || 'all'; const envVal = document.getElementById('filter-env')?.value || 'all'; const popVal = document.getElementById('filter-pop')?.value || 'all'; const damageVal = document.getElementById('filter-damage')?.value || 'all'; const minMag = minMagVal === 'all' ? 0 : Number(minMagVal); const minPop = popVal === 'all' ? 0 : Number(popVal); currentHistoryList = (historyDataset || earthquakesData).filter(eq => { // 1. Texto / Lugar if (query) { const matchPlace = (eq.place || '').toLowerCase().includes(query); const matchCity = eq.nearestCity && eq.nearestCity.name.toLowerCase().includes(query); const matchCountry = eq.nearestCity && eq.nearestCity.country.toLowerCase().includes(query); if (!matchPlace && !matchCity && !matchCountry) return false; } // 2. Magnitud if (eq.magnitude < minMag) return false; // 3. Continente if (continentVal !== 'all' && eq.continent !== continentVal) return false; // 4. Placa Tectónica if (plateVal !== 'all' && !eq.plate.includes(plateVal)) return false; // 5. Entorno (Mar vs Tierra) if (envVal === 'sea' && !eq.environment.includes('Mar')) return false; if (envVal === 'land' && !eq.environment.includes('Tierra')) return false; // 6. Población Cercana if (minPop > 0) { const rawPop = eq.nearestCity?.pop ? parseInt(eq.nearestCity.pop.replace(/,/g, '')) : 0; if (rawPop < minPop) return false; } // 7. Daños / Perceptibilidad if (damageVal === 'perceptible' && (!eq.perceptibility || !eq.perceptibility.feltInCity)) return false; if (damageVal === 'severe' && eq.magnitude < 6.0) return false; if (damageVal === 'instrumental' && eq.magnitude >= 3.8) return false; return true; }); // Orden cronológico estricto: lo más nuevo arriba currentHistoryList.sort((a, b) => (b.time?.getTime() || 0) - (a.time?.getTime() || 0)); renderHistoryTable(); updateHistoryStats(); } function renderHistoryTable() { const tbody = document.getElementById('history-table-tbody'); if (!tbody) return; tbody.innerHTML = ''; if (currentHistoryList.length === 0) { tbody.innerHTML = 'No se encontraron sismos que coincidan con los filtros seleccionados.'; return; } // Renderizar los primeros 120 resultados ordenados para máxima fluidez currentHistoryList.slice(0, 120).forEach(eq => { let magColor = 'text-emerald-400 bg-emerald-500/10 border-emerald-500/30'; if (eq.magnitude >= 7.0) magColor = 'text-rose-400 bg-rose-500/10 border-rose-500/30'; else if (eq.magnitude >= 6.0) magColor = 'text-orange-400 bg-orange-500/10 border-orange-500/30'; else if (eq.magnitude >= 4.5) magColor = 'text-amber-400 bg-amber-500/10 border-amber-500/30'; const tr = document.createElement('tr'); tr.className = 'hover:bg-slate-800/40 transition-colors'; const cleanPlace = sanitizeSeismicText(eq.place); const cleanCity = eq.nearestCity ? sanitizeSeismicText(eq.nearestCity.name) : 'Zona Remota'; tr.innerHTML = ` M ${eq.magnitude.toFixed(1)} ${eq.time instanceof Date ? eq.time.toLocaleDateString() + ' ' + eq.time.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : 'Reciente'} ${cleanPlace} ${eq.depth.toFixed(1)} km ${eq.environment.includes('Mar') ? '🌊 Mar / Océano' : '🏔️ Tierra'} ${eq.plate}
${cleanCity}
${eq.nearestCity ? eq.nearestCity.pop + ' hab.' : ''}
${eq.perceptibility ? `${eq.perceptibility.status.split('/')[0]}` : '--'} `; tbody.appendChild(tr); }); // Conectar botones para centrar el globo en el epicentro tbody.querySelectorAll('.btn-goto-3d').forEach(btn => { btn.onclick = () => { const id = btn.getAttribute('data-id'); const target = currentHistoryList.find(x => x.id === id); if (target) { document.getElementById('modal-history').classList.add('hidden'); focusEarthquake(target); openEventDetails(target); } }; }); } function updateHistoryStats() { const countEl = document.getElementById('stat-history-count'); const maxMagEl = document.getElementById('stat-history-maxmag'); const envEl = document.getElementById('stat-history-env'); const energyEl = document.getElementById('stat-history-energy'); const badgeEl = document.getElementById('history-total-badge'); if (!countEl) return; const total = currentHistoryList.length; if (badgeEl) badgeEl.innerText = `${total} sismos`; countEl.innerText = total.toLocaleString(); if (total === 0) { maxMagEl.innerText = 'M 0.0'; envEl.innerText = '0% / 0%'; energyEl.innerText = '0 J'; return; } const maxMag = Math.max(...currentHistoryList.map(x => x.magnitude)); maxMagEl.innerText = `M ${maxMag.toFixed(1)}`; const seaCount = currentHistoryList.filter(x => x.environment && x.environment.includes('Mar')).length; const seaPct = Math.round((seaCount / total) * 100); const landPct = 100 - seaPct; envEl.innerText = `🌊 ${seaPct}% / 🏔️ ${landPct}%`; const sumEnergy = currentHistoryList.reduce((acc, x) => acc + (x.energyJoules || 0), 0); energyEl.innerText = `${sumEnergy.toExponential(2)} J`; } function getExportableDataset() { if (Array.isArray(currentHistoryList) && currentHistoryList.length > 0) { return currentHistoryList; } if (Array.isArray(historyDataset) && historyDataset.length > 0) { return historyDataset; } if (Array.isArray(earthquakesData) && earthquakesData.length > 0) { return earthquakesData; } return []; } function exportHistoryCSV() { const dataset = getExportableDataset(); if (!dataset || dataset.length === 0) { showToast('⚠️ No hay sismos disponibles en el catálogo para exportar.'); return; } const headers = [ "id", "event_time_utc", "magnitude", "mag_type", "latitude", "longitude", "depth_km", "place", "country", "nearest_city", "nearest_city_distance_km", "nearest_city_population", "tectonic_plate", "environment", "source_agency", "energy_joules", "intensity_mmi_epicentral", "tsunami_potential", "official_url" ]; const rows = dataset.map(eq => { const timeUtc = eq.time instanceof Date ? eq.time.toISOString() : (eq.time ? new Date(eq.time).toISOString() : 'S/D'); const cleanPlace = sanitizeSeismicText(eq.place || '').replace(/"/g, '""'); const mag = Number(eq.magnitude || 0).toFixed(2); const magType = eq.magType || 'M'; const lat = eq.latitude !== undefined && eq.latitude !== null ? eq.latitude : 'S/D'; const lon = eq.longitude !== undefined && eq.longitude !== null ? eq.longitude : 'S/D'; const depth = eq.depth !== undefined && eq.depth !== null ? eq.depth : 'S/D'; const country = eq.nearestCity && eq.nearestCity.country ? `"${eq.nearestCity.country.replace(/"/g, '""')}"` : '"S/D"'; const city = eq.nearestCity && eq.nearestCity.name ? `"${eq.nearestCity.name.replace(/"/g, '""')}"` : '"S/D"'; const distKm = eq.nearestCity && eq.nearestCity.distanceKm ? Number(eq.nearestCity.distanceKm).toFixed(1) : 'S/D'; const pop = eq.nearestCity && eq.nearestCity.pop ? `"${String(eq.nearestCity.pop).replace(/"/g, '""')}"` : '"S/D"'; const plate = eq.plate ? `"${eq.plate.replace(/"/g, '""')}"` : '"S/D"'; const env = eq.environment ? `"${eq.environment.replace(/"/g, '""')}"` : '"S/D"'; const agency = `"${(eq.provider || eq.agency || eq.source || 'USGS').replace(/"/g, '""')}"`; const energy = eq.energyJoules || Math.pow(10, 4.8 + 1.5 * eq.magnitude); const mmi = eq.perceptibility && eq.perceptibility.mmi ? eq.perceptibility.mmi : (eq.perceptibility && eq.perceptibility.status ? `"${eq.perceptibility.status.replace(/"/g, '""')}"` : '"S/D"'); const tsunami = eq.isTsunamiPotential ? 'SI' : 'NO'; const url = eq.url ? `"${eq.url.replace(/"/g, '""')}"` : '"S/D"'; return [ eq.id, timeUtc, mag, magType, lat, lon, depth, `"${cleanPlace}"`, country, city, distKm, pop, plate, env, agency, energy, mmi, tsunami, url ].join(','); }); const csvContent = '\uFEFF' + [headers.join(','), ...rows].join('\r\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Sismosfera_Secuencia_Temporal_${new Date().toISOString().slice(0,10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`📥 Secuencia exportada en CSV: ${dataset.length} sismos analizados.`); } function exportHistoryGeoJSON() { const dataset = getExportableDataset(); if (!dataset || dataset.length === 0) { showToast('⚠️ No hay sismos disponibles en el catálogo para exportar a GeoJSON.'); return; } const geojson = { type: "FeatureCollection", metadata: { title: "Sismosfera Global Seismic Sequence", generated: new Date().toISOString(), count: dataset.length, crs: "EPSG:4326 (WGS84)", source: "Sismosfera (https://terremoto.com) - Real Seismic Telemetry", filterApplied: { query: document.getElementById('filter-search')?.value || 'all', timeRange: document.getElementById('filter-time')?.value || '24h', minMag: document.getElementById('filter-mag')?.value || 'all', continent: document.getElementById('filter-continent')?.value || 'all', plate: document.getElementById('filter-plate')?.value || 'all', environment: document.getElementById('filter-env')?.value || 'all' } }, features: dataset.map(eq => { const timeIso = eq.time instanceof Date ? eq.time.toISOString() : (eq.time ? new Date(eq.time).toISOString() : null); return { type: "Feature", id: eq.id, geometry: { type: "Point", coordinates: [ Number(eq.longitude), Number(eq.latitude), -(Number(eq.depth) || 0) ] }, properties: { id: eq.id, mag: Number(eq.magnitude), magType: eq.magType || 'M', time: timeIso, depthKm: Number(eq.depth) || 0, place: sanitizeSeismicText(eq.place || ''), country: eq.nearestCity?.country || null, nearestCity: eq.nearestCity?.name || null, nearestCityDistanceKm: eq.nearestCity?.distanceKm ? Number(eq.nearestCity.distanceKm) : null, nearestCityPopulation: eq.nearestCity?.pop || null, tectonicPlate: eq.plate || null, environment: eq.environment || null, sourceAgency: eq.provider || eq.agency || eq.source || 'USGS', energyJoules: eq.energyJoules || Math.pow(10, 4.8 + 1.5 * eq.magnitude), intensityMmi: eq.perceptibility?.mmi || null, tsunamiPotential: Boolean(eq.isTsunamiPotential), officialUrl: eq.url || null } }; }) }; const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/geo+json;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Sismosfera_Secuencia_SIG_${new Date().toISOString().slice(0,10)}.geojson`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`🗺️ Capa GeoJSON (RFC 7946) exportada: ${dataset.length} sismos para QGIS / ArcGIS.`); } function exportHistoryKML() { const dataset = getExportableDataset(); if (!dataset || dataset.length === 0) { showToast('⚠️ No hay sismos disponibles en el catálogo para exportar a KML.'); return; } const escapeXml = (str) => String(str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const placemarks = dataset.map(eq => { const timeStr = eq.time instanceof Date ? eq.time.toISOString() : (eq.time ? new Date(eq.time).toISOString() : 'S/D'); const cleanPlace = escapeXml(sanitizeSeismicText(eq.place || 'Epicentro')); const mag = Number(eq.magnitude).toFixed(1); const depthMeters = -Math.abs((Number(eq.depth) || 0) * 1000); let styleId = 'mag-green'; if (eq.magnitude >= 7.0) styleId = 'mag-red'; else if (eq.magnitude >= 6.0) styleId = 'mag-orange'; else if (eq.magnitude >= 4.5) styleId = 'mag-yellow'; return ` M ${mag} - ${cleanPlace}

M ${mag} - ${cleanPlace}

Fecha (UTC): ${timeStr}

Profundidad: ${eq.depth} km

Placa: ${escapeXml(eq.plate || 'S/D')}

Entorno: ${escapeXml(eq.environment || 'S/D')}

Ciudad más cercana: ${eq.nearestCity ? escapeXml(eq.nearestCity.name) + ' (' + eq.nearestCity.distanceKm + ' km)' : 'S/D'}

Agencia: ${escapeXml(eq.provider || eq.agency || eq.source || 'USGS')}

${eq.url ? `

Ver reporte oficial

` : ''} ]]>
#${styleId} relativeToGround ${eq.longitude},${eq.latitude},${depthMeters}
`; }).join('\n'); const kmlContent = ` Sismosfera Global Seismic Sequence (${dataset.length} sismos) Secuencia sísmica generada por Sismosfera (terremoto.com). Datos reales de agencias sismológicas internacionales. ${placemarks} `; const blob = new Blob([kmlContent], { type: 'application/vnd.google-earth.kml+xml;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Sismosfera_Secuencia_GoogleEarth_${new Date().toISOString().slice(0,10)}.kml`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`🌍 Secuencia 3D KML exportada: ${dataset.length} sismos para Google Earth.`); } function initExportDropdown() { const btnToggle = document.getElementById('btn-export-dropdown-toggle'); const menu = document.getElementById('export-dropdown-menu'); const arrow = document.getElementById('export-dropdown-arrow'); if (!btnToggle || !menu) return; btnToggle.onclick = (e) => { e.stopPropagation(); const isHidden = menu.classList.contains('hidden'); if (isHidden) { menu.classList.remove('hidden'); if (arrow) arrow.style.transform = 'rotate(180deg)'; } else { menu.classList.add('hidden'); if (arrow) arrow.style.transform = 'rotate(0deg)'; } }; document.addEventListener('click', (e) => { if (!menu.contains(e.target) && !btnToggle.contains(e.target)) { menu.classList.add('hidden'); if (arrow) arrow.style.transform = 'rotate(0deg)'; } }); const btnCsv = document.getElementById('btn-export-csv'); if (btnCsv) { btnCsv.onclick = () => { menu.classList.add('hidden'); if (arrow) arrow.style.transform = 'rotate(0deg)'; exportHistoryCSV(); }; } const btnGeoJson = document.getElementById('btn-export-geojson'); if (btnGeoJson) { btnGeoJson.onclick = () => { menu.classList.add('hidden'); if (arrow) arrow.style.transform = 'rotate(0deg)'; exportHistoryGeoJSON(); }; } const btnKml = document.getElementById('btn-export-kml'); if (btnKml) { btnKml.onclick = () => { menu.classList.add('hidden'); if (arrow) arrow.style.transform = 'rotate(0deg)'; exportHistoryKML(); }; } } // ==================================================================== // NOTIFICACIÓN TOAST FLOTANTE // ==================================================================== let _lastToastTime = 0; let _toastQueue = 0; function showToast(msg, duration = 3000, priority = false) { if (!msg) return; // Directiva de diseño expresa del usuario: erradicar carteles flotantes de sismos, consensos y reportes automáticos const s = String(msg).toLowerCase(); if (s.includes('consenso') || s.includes('reporte en vivo') || s.includes('sismo') || s.includes('terremoto') || s.includes('alerta')) { return; } const container = document.getElementById('toast-container'); if (!container) return; // Limitar a 1 único mensaje discreto para acciones voluntarias del usuario (ej: Enlace copiado) const existing = container.querySelectorAll('div'); existing.forEach(el => el.remove()); const toast = document.createElement('div'); toast.className = 'glass-panel px-3.5 py-2 rounded-xl border border-sky-500/40 bg-slate-950/95 text-sky-200 text-xs font-semibold shadow-xl flex items-center space-x-2 transition-all duration-300 pointer-events-auto backdrop-blur-md'; toast.innerHTML = `${msg}`; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-6px)'; setTimeout(() => toast.remove(), 300); }, duration); } // ==================================================================== // GEOLOCALIZACIÓN GPS DE ALTA PRECISIÓN (MI UBICACIÓN) // ==================================================================== function locateUser(source = 'header') { if (!navigator.geolocation) { alert("Tu navegador o sistema no tiene soporte para la API de geolocalización GPS."); return; } const btnHeader = document.getElementById('btn-my-location'); const btnHeaderTxt = document.getElementById('btn-my-location-text'); const btnModal = document.getElementById('btn-modal-locate'); const btnModalTxt = document.getElementById('btn-modal-locate-text'); if (btnHeaderTxt) btnHeaderTxt.innerText = 'OBTENIENDO GPS...'; if (btnModalTxt) btnModalTxt.innerText = 'Obteniendo GPS...'; showToast("Conectando con satélites GPS y triangulación de alta precisión..."); navigator.geolocation.getCurrentPosition( (position) => { const { latitude, longitude, accuracy } = position.coords; userLocation = { lat: Number(latitude.toFixed(5)), lng: Number(longitude.toFixed(5)), accuracy: Math.round(accuracy || 10) }; centerLat = userLocation.lat; centerLon = userLocation.lng; // Actualizar texto en modal de geocercas const coordsText = document.getElementById('center-coords-text'); if (coordsText) { coordsText.innerText = `${centerLat}°, ${centerLon}° (GPS ±${userLocation.accuracy}m)`; } // Si Leaflet está inicializado, reposicionar mapa, círculo y marcador if (leafletMap) { leafletMap.setView([centerLat, centerLon], 8); if (leafletCircle) leafletCircle.setLatLng([centerLat, centerLon]); if (leafletMarker) leafletMarker.setLatLng([centerLat, centerLon]); leafletMap.invalidateSize(); } // Refrescar capas 3D y etiquetas en el globo refreshGlobeLayers(); updateGlobeLabels(); // Centrar cámara del globo en la posición exacta del usuario if (myGlobe) { const controls = myGlobe.controls(); if (controls) controls.autoRotate = false; // Detener rotación para inspección precisa myGlobe.pointOfView({ lat: userLocation.lat, lng: userLocation.lng, altitude: 0.8 }, 2200); } // Actualizar estado visual de los botones if (btnHeader) { btnHeader.className = 'glass-panel px-3.5 py-2 rounded-xl text-xs font-bold text-white bg-sky-600/40 border border-sky-400 transition-all flex items-center space-x-2 shadow-lg shadow-sky-500/20 cursor-pointer'; } if (btnHeaderTxt) btnHeaderTxt.innerText = 'MI UBICACIÓN (ACTIVA)'; if (btnModalTxt) { btnModalTxt.innerText = '✅ Ubicación GPS Fijada'; setTimeout(() => { if (btnModalTxt) btnModalTxt.innerText = 'Usar Mi Ubicación GPS Precisa'; }, 3000); } detectUserTimeZone(); updateTimeZoneUI(); renderFeedList(); showToast(`🎯 Ubicación fijada: ${userLocation.lat}°, ${userLocation.lng}° • Huso: ${userTimeZoneInfo.gmtLabel}`); }, (error) => { console.warn("[GEOLOCATION] Error al obtener posición GPS:", error); let errDetail = "No se pudo obtener la señal GPS."; if (error.code === error.PERMISSION_DENIED) { errDetail = "Permiso denegado. Habilita el acceso a la ubicación en tu navegador."; } else if (error.code === error.POSITION_UNAVAILABLE) { errDetail = "Señal de posición no disponible en este momento."; } else if (error.code === error.TIMEOUT) { errDetail = "Tiempo de espera agotado al consultar coordenadas GPS."; } alert(`⚠️ Aviso de Ubicación:\n${errDetail}`); if (btnHeaderTxt) btnHeaderTxt.innerText = 'MI UBICACIÓN'; if (btnModalTxt) btnModalTxt.innerText = 'Usar Mi Ubicación GPS Precisa'; }, { enableHighAccuracy: true, timeout: 12000, maximumAge: 0 } ); } // ==================================================================== // LEAFLET MAP PARA MODAL DE GEOCERCAS (ESRI WORLD DARK GRAY BASE) // ==================================================================== function initLeafletMap() { const mapContainer = document.getElementById('leaflet-modal-map'); if (!mapContainer) return; if (leafletMap) { leafletMap.invalidateSize(); return; } leafletMap = L.map('leaflet-modal-map', { zoomControl: true, attributionControl: true }).setView([centerLat, centerLon], 5); // Usar Esri World Dark Gray Base: libre de marcas de agua y de alta resolución L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri — Esri, DeLorme, NAVTEQ', maxZoom: 16 }).addTo(leafletMap); // Geocerca circular leafletCircle = L.circle([centerLat, centerLon], { color: '#ef4444', fillColor: '#ef4444', fillOpacity: 0.22, weight: 2, radius: radiusKm * 1000 }).addTo(leafletMap); // Marcador central interactivo leafletMarker = L.marker([centerLat, centerLon], { icon: L.divIcon({ className: 'custom-gps-marker', html: '
', iconSize: [14, 14], iconAnchor: [7, 7] }) }).addTo(leafletMap); leafletMap.on('click', (e) => { centerLat = Number(e.latlng.lat.toFixed(4)); centerLon = Number(e.latlng.lng.toFixed(4)); leafletCircle.setLatLng([centerLat, centerLon]); leafletMarker.setLatLng([centerLat, centerLon]); document.getElementById('center-coords-text').innerText = `${centerLat}°, ${centerLon}°`; }); setTimeout(() => { if (leafletMap) leafletMap.invalidateSize(); }, 150); } // ==================================================================== // MÓDULO DE CIENCIA CIUDADANA «¡LO SENTÍ!» (CROWDSOURCED EARLY WARNING) // ==================================================================== let citizenReports = []; let activeCitizenAlert = null; async function refreshFeltModalTally(eventId) { const totalEl = document.getElementById('felt-tally-total'); const leveEl = document.getElementById('felt-tally-leve'); const modEl = document.getElementById('felt-tally-mod'); const fuerteEl = document.getElementById('felt-tally-fuerte'); const sumEl = document.getElementById('felt-tally-summary'); if (!eventId) { if (sumEl) sumEl.textContent = 'A la espera de reportes ciudadanos en vivo'; return; } try { const res = await fetch(`/api/citizen-reports?eventId=${encodeURIComponent(eventId)}`, { signal: AbortSignal.timeout(4000) }); if (res.ok) { const data = await res.json(); const total = Number(data.count) || 0; const b = data.breakdown || { leve: 0, moderado: 0, fuerte: 0 }; if (totalEl) totalEl.textContent = `${total} ${total === 1 ? 'reporte' : 'reportes'}`; if (leveEl) leveEl.textContent = String(b.leve || 0); if (modEl) modEl.textContent = String(b.moderado || 0); if (fuerteEl) fuerteEl.textContent = String(b.fuerte || 0); if (sumEl) { sumEl.textContent = total > 0 ? `Intensidad predominante: ${data.predominantLabel || 'Moderado'}` : 'Sé la primera persona en reportar cómo se sintió'; } const modalFeedEl = document.getElementById('felt-modal-feed-list'); if (modalFeedEl) { const reports = Array.isArray(data.reports) ? data.reports : []; if (reports.length > 0) { modalFeedEl.innerHTML = reports.slice(0, 5).map(r => { const icon = (r.intensity === 'strong' || r.intensity === 'fuerte') ? '🔴' : ((r.intensity === 'weak' || r.intensity === 'leve') ? '🟢' : '🟡'); const isFeatured = Boolean(r.isFeatured || (r.sourceLabel && r.sourceLabel.includes('Verificado'))); const badge = isFeatured ? '⭐ Verificado' : (r.source === 'whatsapp_verified' ? 'WhatsApp' : 'Web'); const comment = r.comment ? ` · "${String(r.comment).slice(0, 70)}"` : ''; return `
${icon} ${r.name || 'Vecino'} en ${r.city || 'zona'}${comment} ${badge}
`; }).join(''); } else { modalFeedEl.innerHTML = `
Sé el primero en reportar cómo se sintió.
`; } } } } catch (_) {} } function openFeltItModal(eqArg) { if (typeof trackToolHit === 'function') trackToolHit('dyfi'); if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('felt_report_opened', { category: 'conversion', label: 'dyfi_modal' }); } const modal = document.getElementById('modal-felt-it'); if (!modal) return; ['mobile-explore-hub-modal', 'mobile-radar-selector-modal', 'layer-menu'].forEach(id => { const m = document.getElementById(id); if (m) m.classList.add('hidden'); }); const rawList = (typeof earthquakesData !== 'undefined' && Array.isArray(earthquakesData)) ? earthquakesData : []; const filteredList = (typeof getVisibleFilteredEarthquakes === 'function') ? getVisibleFilteredEarthquakes() : []; const list = (filteredList && filteredList.length > 0) ? filteredList : rawList; let ev = (eqArg && (eqArg.id || eqArg.place || eqArg.magnitude !== undefined)) ? eqArg : null; if (!ev) { const selInActiveList = selectedEvent && list.some(q => String(q.id) === String(selectedEvent.id)); if (selInActiveList && Number(selectedEvent.magnitude || 0) >= 2.5) { ev = selectedEvent; } else { const perceptible = list.filter(q => Number(q.magnitude || 0) >= 2.5); ev = perceptible[0] || (selInActiveList ? selectedEvent : null) || list[0] || window.__INITIAL_EVENT_DATA__ || null; } } if (ev && (ev.id || ev.place)) { selectedEvent = ev; } const magValEl = document.getElementById('felt-event-mag-val'); const magBadgeEl = document.getElementById('felt-event-mag-badge'); const placeEl = document.getElementById('felt-event-place'); const metaEl = document.getElementById('felt-event-meta'); const feedbackEl = document.getElementById('felt-vote-feedback'); if (feedbackEl) feedbackEl.classList.add('hidden'); if (ev) { const initMag = (window.__INITIAL_EVENT_DATA__ && String(window.__INITIAL_EVENT_DATA__.id) === String(ev.id)) ? Number(window.__INITIAL_EVENT_DATA__.mag || window.__INITIAL_EVENT_DATA__.magnitude || 0) : 0; const mag = Math.max(Number(ev.magnitude ?? ev.mag ?? 0), initMag); const depth = Math.round(Math.abs(Number(ev.depth ?? (window.__INITIAL_EVENT_DATA__ && window.__INITIAL_EVENT_DATA__.depth) ?? 10))); const rawPlace = ev.place || ev.name || 'Epicentro en monitoreo'; const lat = Number(ev.latitude ?? ev.lat ?? 0); const lon = Number(ev.longitude ?? ev.lng ?? 0); let formattedHtml = rawPlace; try { const eng = window.SeismicLocationEngine; if (eng && typeof eng.parse === 'function') { const g = eng.parse(rawPlace, lat, lon); const flag = typeof eng.flagSvg === 'function' ? eng.flagSvg(g.code, 18) : ''; const ctry = String(g.country || '').trim(); const loc = String(g.locality || '').trim(); const titleText = (loc && ctry && ctry !== 'Internacional' && !loc.includes(ctry)) ? `${ctry} · ${loc}` : (loc || ctry || rawPlace); formattedHtml = `${flag}${titleText}`; } } catch (_) {} const timeVal = ev.time ? new Date(ev.time) : null; const timeStr = (timeVal && !isNaN(timeVal.getTime())) ? timeVal.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'Reciente'; if (magValEl) magValEl.textContent = mag > 0 ? mag.toFixed(1) : 'S/D'; if (magBadgeEl) { magBadgeEl.className = 'felt-mag-box ' + (mag >= 6.0 ? 'felt-mag-box--fuerte' : (mag >= 4.5 ? 'felt-mag-box--mod' : 'felt-mag-box--leve')); } if (placeEl) { placeEl.className = ''; placeEl.style.cssText = 'font-size:15px;font-weight:900;color:#ffffff;line-height:1.25;letter-spacing:-0.01em;white-space:normal;'; placeEl.innerHTML = formattedHtml; } if (metaEl) { metaEl.innerHTML = `🌊 Profundidad: ${depth} km🕒 Hora local: ${timeStr}`; } refreshFeltModalTally(String(ev.id || ev.eventId || '')); // Selector rápido para cambiar de sismo si el usuario sintió otro evento reciente const cardEl = document.getElementById('felt-event-card'); if (cardEl && list.length > 1) { let selWrap = document.getElementById('felt-quake-selector-wrap'); if (!selWrap) { selWrap = document.createElement('div'); selWrap.id = 'felt-quake-selector-wrap'; selWrap.style.cssText = 'margin-top:-4px;'; cardEl.insertAdjacentElement('afterend', selWrap); } const candidates = list.filter(q => Number(q.magnitude || 0) >= 2.5).slice(0, 12); if (ev.id && !candidates.some(c => String(c.id) === String(ev.id))) candidates.unshift(ev); const opts = candidates.map(c => { const m = Number(c.magnitude || 0).toFixed(1); let labelLoc = c.place || 'Sismo'; try { if (window.SeismicLocationEngine && typeof window.SeismicLocationEngine.parse === 'function') { const g = window.SeismicLocationEngine.parse(c.place, c.latitude ?? c.lat, c.longitude ?? c.lng); labelLoc = (g.country && g.country !== 'Internacional') ? `${g.country} · ${g.locality || ''}` : (g.locality || c.place); } } catch (_) {} const selAttr = String(c.id) === String(ev.id) ? 'selected' : ''; return ``; }).join(''); selWrap.innerHTML = ` `; const selEl = document.getElementById('felt-quake-selector'); if (selEl) { selEl.onchange = function() { const found = list.find(q => String(q.id) === String(this.value)); if (found) openFeltItModal(found); }; } } } modal.classList.remove('hidden'); try { window.history.pushState({ sheet: 'felt' }, ''); } catch(_) {} updateFeltLocationDisplay(); } function updateFeltLocationDisplay() { const locText = document.getElementById('felt-location-text'); const cityText = document.getElementById('felt-city-text'); const curLat = userLocation ? userLocation.lat : centerLat; const curLng = userLocation ? userLocation.lng : centerLon; if (locText) { locText.innerText = `${curLat.toFixed(4)}°, ${curLng.toFixed(4)}° ${userLocation ? '(GPS activo)' : '(Centro de referencia)'}`; } const nearest = findNearestCity(curLat, curLng); if (cityText) { if (nearest) { cityText.innerText = `${nearest.name}, ${nearest.country} (a ${nearest.distanceKm} km)`; } else { cityText.innerText = 'Coordenadas globales'; } } } async function submitCitizenReport(intensity) { const repLat = userLocation ? userLocation.lat : centerLat; const repLng = userLocation ? userLocation.lng : centerLon; const nearest = findNearestCity(repLat, repLng); const cityName = nearest ? nearest.name : 'Zona marina/remota'; const now = Date.now(); const targetEventId = selectedEvent ? String(selectedEvent.id || selectedEvent.eventId || '') : (window.__INITIAL_EVENT_ID__ ? String(window.__INITIAL_EVENT_ID__) : ''); const report = { id: `felt_${now}_${typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : performance.now().toString(36)}`, eventId: targetEventId, timestamp: now, lat: repLat, lng: repLng, city: cityName, intensity: intensity, weight: intensity === 'strong' ? 3 : (intensity === 'moderate' ? 2 : 1) }; citizenReports.push(report); // Persistencia en backend Sismosfera y actualización en vivo del escrutinio en el pop-up try { await fetch('/api/citizen-reports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ eventId: targetEventId, intensity, lat: repLat, lng: repLng, city: cityName }) }); } catch (e) {} await refreshFeltModalTally(targetEventId); const feedbackEl = document.getElementById('felt-vote-feedback'); if (feedbackEl) { feedbackEl.classList.remove('hidden'); if (typeof window.syncKit72hProgressUI === 'function') window.syncKit72hProgressUI(); setTimeout(() => { try { feedbackEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (_) {} }, 80); } // Si la ficha del sismo está abierta, refrescar de inmediato el texto de testimonios try { if (typeof fetchAndRenderCitizenReports === 'function' && targetEventId) { fetchAndRenderCitizenReports(targetEventId); } } catch (e) {} // Feedback acústico táctico si el motor de audio está disponible if (typeof playEarthquakeAcoustic === 'function') { const simMag = intensity === 'strong' ? 5.8 : (intensity === 'moderate' ? 4.8 : 3.8); playEarthquakeAcoustic(simMag, 15); } const labelEs = intensity === 'strong' ? 'FUERTE' : (intensity === 'moderate' ? 'MODERADO' : 'LEVE'); showToast(`✅ ¡Voto registrado (${labelEs})! Tu reporte ya suma en el mapa vecinal.`); // FUNNEL INBOUND POST-DYFI: Ofrecer alertas inmediatas ante réplicas en la localidad if (!pushSubscriptionActive && typeof Notification !== 'undefined' && Notification.permission === 'default') { setTimeout(() => { const banner = document.getElementById('banner-quick-push'); if (banner) { banner.classList.remove('hidden'); const txt = banner.querySelector('.text-\\[11px\\]'); if (txt) txt.innerText = `¿Quieres recibir avisos inmediatos si hay réplicas en ${cityName}?`; } }, 1200); } evaluateCitizenCluster(report); } function evaluateCitizenCluster(latestReport) { const now = Date.now(); const WINDOW_MS = 5 * 60 * 1000; // 5 minutos const recentReports = citizenReports.filter(r => (now - r.timestamp) <= WINDOW_MS); const nearbyReports = recentReports.filter(r => { const d = haversineDistance(latestReport.lat, latestReport.lng, r.lat, r.lng); return d <= 180; }); if (nearbyReports.length >= 2 || latestReport.intensity === 'strong') { const clusterLat = nearbyReports.reduce((acc, r) => acc + r.lat, 0) / nearbyReports.length; const clusterLng = nearbyReports.reduce((acc, r) => acc + r.lng, 0) / nearbyReports.length; const totalCount = nearbyReports.length; activeCitizenAlert = { lat: clusterLat, lng: clusterLng, city: latestReport.city, count: totalCount, intensity: latestReport.intensity, timestamp: now }; const banner = document.getElementById('citizen-alert-banner'); const bannerTxt = document.getElementById('citizen-alert-banner-text'); if (banner && bannerTxt) { bannerTxt.innerHTML = `⚠️ ${totalCount} ${totalCount === 1 ? 'reporte' : 'reportes'} de sacudida recibido(s) en la zona de ${latestReport.city} hace instantes.`; banner.classList.remove('hidden'); } refreshGlobeLayers(); if (soundActive && typeof playEarthquakeAcoustic === 'function') { playEarthquakeAcoustic(6.0, 10); } } } // ==================================================================== // MÓDULO DE INTELIGENCIA SÍSMICA REGIONAL Y SELECTOR DE PAÍSES (FASE 67.0) // ==================================================================== function updateRegionalIntelligenceCard() { const card = document.getElementById('regional-intel-card'); const badge = document.getElementById('country-filter-active-badge'); if (!card) return; if (!activeCountryFilter || activeCountryFilter === 'all') { card.classList.add('hidden'); if (badge) badge.innerText = 'Global'; return; } const conf = REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter]; if (!conf) { card.classList.add('hidden'); if (badge) badge.innerText = 'Global'; return; } card.classList.remove('hidden'); const confCodeUpper = (conf.code || '').toUpperCase(); const svgFlag = (window.SVG_FLAGS && window.SVG_FLAGS[confCodeUpper]) ? window.SVG_FLAGS[confCodeUpper].replace('w-5 h-3.5', 'w-4 h-2.5') : ''; if (badge) badge.innerHTML = `${svgFlag} ${conf.name}`; const flagEl = document.getElementById('intel-country-flag'); const nameEl = document.getElementById('intel-country-name'); if (flagEl) flagEl.innerHTML = svgFlag || conf.flag; if (nameEl) nameEl.innerText = conf.name.toUpperCase(); // Filtrar los sismos del país en las últimas 24h (Regla de Oro #1: 100% verídico) const now = Date.now(); const oneDayMs = 24 * 60 * 60 * 1000; const recentRegionalEqs = (earthquakesData || []).filter(e => { if (!e) return false; const t = e.time instanceof Date ? e.time.getTime() : new Date(e.time || 0).getTime(); return (now - t <= oneDayMs) && isEarthquakeInCountry(e, activeCountryFilter); }); // Calcular energía acumulada total con Gutenberg-Richter (10^(4.8 + 1.5 * M) Joules) let totalJoules = 0; let maxMag = 0; let maxMagEvent = null; let totalDepth = 0; recentRegionalEqs.forEach(eq => { const m = Number(eq.magnitude) || 0; const eJoules = calculateGutenbergRichterEnergyJoules(m); totalJoules += eJoules; if (m > maxMag) { maxMag = m; maxMagEvent = eq; } totalDepth += (Number(eq.depth) || 0); }); const avgDepth = recentRegionalEqs.length > 0 ? (totalDepth / recentRegionalEqs.length).toFixed(1) : '0.0'; const { energyStr, tntStr } = formatSeismicEnergy(totalJoules); const energyValEl = document.getElementById('intel-energy-val'); const tntValEl = document.getElementById('intel-tnt-val'); const countValEl = document.getElementById('intel-count-val'); const depthValEl = document.getElementById('intel-depth-val'); const maxMagEl = document.getElementById('intel-max-mag'); const maxPlaceEl = document.getElementById('intel-max-place'); if (energyValEl) energyValEl.innerText = energyStr; if (tntValEl) tntValEl.innerText = tntStr; if (countValEl) countValEl.innerText = `${recentRegionalEqs.length} evento${recentRegionalEqs.length !== 1 ? 's' : ''}`; if (depthValEl) depthValEl.innerText = `Prof. media: ${avgDepth} km`; if (maxMagEvent) { if (maxMagEl) maxMagEl.innerText = `M ${maxMag.toFixed(1)}`; if (maxPlaceEl) { maxPlaceEl.innerText = sanitizeSeismicText(maxMagEvent.place || conf.name); maxPlaceEl.title = maxMagEvent.place || ''; } } else { if (maxMagEl) maxMagEl.innerText = 'Sin sismos'; if (maxPlaceEl) { maxPlaceEl.innerText = 'Tranquilidad sísmica 24h'; maxPlaceEl.title = ''; } } } window.applyCountryFocusFilter = function(labelOrSlug) { const clean = String(labelOrSlug || '').trim(); if (!clean) { selectCountryFilter('all', true); if (typeof window.updateMobileHumanPulseBar === 'function') window.updateMobileHumanPulseBar(); return; } const norm = clean.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); const map = { 'chile': 'cl', 'mexico': 'mx', 'argentina': 'ar', 'peru': 'pe', 'japon': 'jp', 'estados-unidos': 'us', 'ee.uu.': 'us', 'usa': 'us', 'california': 'ca', 'colombia': 'co', 'ecuador': 'ec', 'espana': 'es', 'italia': 'it', 'turquia': 'tr', 'indonesia': 'id', 'taiwan': 'tw', 'guatemala': 'gt', 'centroamerica': 'gt', 'costa-rica': 'cr', 'costa rica': 'cr', 'nueva-zelanda': 'nz', 'nueva zelanda': 'nz', 'filipinas': 'ph', 'grecia': 'gr' }; if (map[norm] && typeof selectCountryFilter === 'function') { window.activeGeoSearchQuery = null; selectCountryFilter(map[norm], true); } else { window.activeGeoSearchQuery = clean; if (typeof renderFeedList === 'function') renderFeedList(); } if (typeof window.updateMobileHumanPulseBar === 'function') window.updateMobileHumanPulseBar(); }; function selectCountryFilter(countryCode, animateCamera = true) { activeCountryFilter = countryCode || 'all'; // Actualizar UI de chips en la barra de scroll document.querySelectorAll('#country-chips-container .btn-country-chip').forEach(btn => { const c = btn.getAttribute('data-country'); if (c === activeCountryFilter) { btn.classList.add('active', 'bg-sky-500/20', 'text-sky-300', 'border-sky-500/40', 'font-bold', 'shadow-sm'); btn.classList.remove('bg-slate-900/80', 'text-slate-300', 'border-slate-800'); try { btn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } catch(eSc) {} } else { btn.classList.remove('active', 'bg-sky-500/20', 'text-sky-300', 'border-sky-500/40', 'font-bold', 'shadow-sm'); btn.classList.add('bg-slate-900/80', 'text-slate-300', 'border-slate-800'); } }); // Mover la cámara si corresponde if (animateCamera) { if (activeCountryFilter === 'all') { if (myGlobe && typeof myGlobe.pointOfView === 'function') { myGlobe.pointOfView({ lat: 15.0, lng: 0.0, altitude: 2.6 }, 1800); } if (typeof leafletMap !== 'undefined' && leafletMap && typeof leafletMap.setView === 'function') { leafletMap.setView([20.0, 0.0], 2); } } else { const conf = REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter]; if (conf) { if (myGlobe && typeof myGlobe.pointOfView === 'function') { myGlobe.pointOfView(conf.camera, 1800); } if (typeof leafletMap !== 'undefined' && leafletMap && typeof leafletMap.setView === 'function') { leafletMap.setView([conf.camera.lat, conf.camera.lng], 5); } } } } // Actualizar URL sin recargar para posibilitar deep-linking try { const url = new URL(window.location.href); if (activeCountryFilter === 'all') { url.searchParams.delete('country'); url.searchParams.delete('pais'); } else { url.searchParams.set('country', activeCountryFilter); } window.history.replaceState({}, '', url.toString()); } catch (eUrl) {} // Telemetría GA4 GCO if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('country_filter_selected', { country_code: activeCountryFilter, country_name: REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter]?.name || 'Global' }); } // Renderizar feed filtrado renderFeedList(); if (typeof window.updateMobileHumanPulseBar === 'function') window.updateMobileHumanPulseBar(); if (activeCountryFilter !== 'all') { const conf = REGIONAL_INTELLIGENCE_COUNTRIES[activeCountryFilter]; if (conf && typeof showToast === 'function') { showToast(`🎯 Región: ${conf.flag} ${conf.name}`); } } } function initCountryFilter() { // Event listeners para botones de país document.querySelectorAll('#country-chips-container .btn-country-chip').forEach(btn => { btn.onclick = () => { const code = btn.getAttribute('data-country') || 'all'; selectCountryFilter(code, true); }; }); // Botón cerrar/restablecer en tarjeta regional const btnReset = document.getElementById('btn-reset-country-filter'); if (btnReset) { btnReset.onclick = () => { selectCountryFilter('all', true); }; } // Desplazamiento horizontal con rueda del ratón en chips de países (Fase 102.0) const chipsContainer = document.getElementById('country-chips-container'); if (chipsContainer) { chipsContainer.addEventListener('wheel', (e) => { if (e.deltaY !== 0) { e.preventDefault(); chipsContainer.scrollLeft += e.deltaY; } }, { passive: false }); } // Detectar si la URL traía ?country=cl o ?pais=cl try { const params = new URLSearchParams(window.location.search); const urlCountry = (params.get('country') || params.get('pais') || '').toLowerCase(); if (urlCountry && REGIONAL_INTELLIGENCE_COUNTRIES[urlCountry]) { setTimeout(() => { selectCountryFilter(urlCountry, false); }, 800); } } catch(e) {} } // ==================================================================== // CONTROLES DE LA COLUMNA LATERAL DE SISMOS (ESCONDER / AMPLIAR / DISMINUIR) // ==================================================================== const SIDEBAR_WIDTH_CLASSES = ['w-72', 'w-96', 'w-[480px]', 'w-[600px]']; let currentSidebarWidthIndex = 1; // w-96 por defecto function initSidebarControls() { const sidebar = document.getElementById('left-sidebar'); const btnShow = document.getElementById('btn-show-sidebar'); const btnCollapse = document.getElementById('btn-sidebar-collapse'); const btnExpand = document.getElementById('btn-sidebar-expand'); const btnShrink = document.getElementById('btn-sidebar-shrink'); const zoomContainer = document.getElementById('globe-zoom-slider-container'); const timeMachineBar = document.getElementById('timelapse-bar'); const ctaAlert = document.getElementById('cta-container-alert'); const ctaFelt = document.getElementById('cta-container-felt'); if (!sidebar) return; const isMobile = () => window.innerWidth < 1024; function updateLayoutOffsets(sidebarOpen) { const isDesktop = window.innerWidth >= 1024; const hud = document.getElementById('autopilot-hud'); const timeMachine = document.getElementById('timelapse-bar'); const rightAside = document.querySelector('aside.fixed.bottom-3'); const heatmapLegend = document.getElementById('heatmap-contour-legend'); const isHeatmapActive = (typeof heatmapActive !== 'undefined' && heatmapActive) || (heatmapLegend && !heatmapLegend.classList.contains('hidden')); let hudShiftX = 0; let timeMachineLeft = '50%'; let timeMachineTransform = 'translate(-50%, 0)'; if (isDesktop) { const sbWidth = (sidebarOpen && sidebar) ? (sidebar.offsetWidth || 340) : 0; const rightWidth = (rightAside && rightAside.offsetWidth) ? rightAside.offsetWidth : 320; // 1. Posicionamiento del Heatmap Legend (Curvas de Nivel): NUNCA sobre el sidebar if (heatmapLegend) { if (sidebarOpen && sbWidth > 0) { heatmapLegend.style.left = (sbWidth + 20) + 'px'; } else { heatmapLegend.style.left = '24px'; } } // 2. Posicionamiento de la Barra de Tiempo (Time Machine): Cero colisión con la leyenda if (isHeatmapActive && heatmapLegend && !heatmapLegend.classList.contains('hidden')) { const legendLeft = parseInt(heatmapLegend.style.left) || (sbWidth + 20); const legendW = heatmapLegend.offsetWidth || 300; const legendRightBoundary = legendLeft + legendW + 20; // 20px de aire limpio timeMachineLeft = (legendRightBoundary + 10) + 'px'; timeMachineTransform = 'translate(0, 0)'; } else { const availableWidth = window.innerWidth - sbWidth - rightWidth; const centerBottom = Math.round(sbWidth + (availableWidth / 2)); timeMachineLeft = `${centerBottom}px`; timeMachineTransform = 'translate(-50%, 0)'; } } else { // Mobile / Tablet (< 1024px) timeMachineLeft = '50%'; timeMachineTransform = 'translate(-50%, 0)'; if (heatmapLegend) { heatmapLegend.style.left = '16px'; if (sidebarOpen) { heatmapLegend.classList.add('hidden'); } } } if (hud) { hud.style.left = hudShiftX > 0 ? `calc(50% + ${hudShiftX}px)` : '50%'; } if (timeMachine) { timeMachine.style.left = timeMachineLeft; timeMachine.style.transform = timeMachineTransform; } } window.updateLayoutOffsets = updateLayoutOffsets; function updateMobileLayout(sidebarOpen) { const isPhone = window.innerWidth < 768; const isTablet = window.innerWidth >= 768 && window.innerWidth < 1024; if (isPhone) { // En móviles (< 768px): Mantener despejado el 90%+ del visor 3D (gestos táctiles nativos) if (zoomContainer) zoomContainer.classList.add('hidden'); if (timeMachineBar) timeMachineBar.classList.add('hidden'); if (sidebarOpen) { if (ctaAlert) ctaAlert.classList.add('hidden'); if (ctaFelt) ctaFelt.classList.add('hidden'); } else { if (ctaAlert) ctaAlert.classList.remove('hidden'); if (ctaFelt) ctaFelt.classList.remove('hidden'); } } else if (isTablet) { // En tablets (768px - 1023px): Ocultar controles secundarios al abrir el feed para no pisar tarjetas if (zoomContainer) { if (sidebarOpen) zoomContainer.classList.add('hidden'); else zoomContainer.classList.remove('hidden'); } if (timeMachineBar) { if (sidebarOpen) timeMachineBar.classList.add('hidden'); else timeMachineBar.classList.remove('hidden'); } if (ctaAlert) ctaAlert.classList.remove('hidden'); if (ctaFelt) ctaFelt.classList.remove('hidden'); } else { // En desktop (>= 1024px): Cada panel tiene su cuadrante independiente con mucho aire if (zoomContainer) zoomContainer.classList.remove('hidden'); if (timeMachineBar) timeMachineBar.classList.remove('hidden'); if (ctaAlert) ctaAlert.classList.add('hidden'); // En desktop las alertas se gestionan desde el navbar superior if (ctaFelt) ctaFelt.classList.add('hidden'); // En desktop ¿Lo Sentiste? se gestiona desde el dock superior derecho } updateLayoutOffsets(sidebarOpen); } // Esconder columna if (btnCollapse) { btnCollapse.onclick = () => { sidebar.classList.add('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); if (btnShow) btnShow.classList.remove('hidden'); updateMobileLayout(false); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); }; } // Mostrar columna if (btnShow) { btnShow.onclick = () => { sidebar.classList.remove('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); btnShow.classList.add('hidden'); updateMobileLayout(true); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('sismos'); }; } // Fase 200.2 (Entrada #676): En móvil (< 768px), iniciar siempre con el Globo 3D 100% limpio y despejado (salvo ?list=1 o #sismos) if (isMobile()) { const wantsListOpen = /[?&]list=1/i.test(window.location.search || '') || /^#sismos/i.test(window.location.hash || ''); if (wantsListOpen) { sidebar.classList.remove('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none', 'mobile-sheet-expanded'); if (btnShow) btnShow.classList.add('hidden'); updateMobileLayout(true); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('sismos'); } else { sidebar.classList.add('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); if (btnShow) btnShow.classList.add('hidden'); updateMobileLayout(false); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); } } else { sidebar.classList.remove('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); updateMobileLayout(true); } window.addEventListener('resize', () => { const isCollapsed = sidebar.classList.contains('opacity-0'); updateMobileLayout(!isCollapsed); }); // Ampliar ancho if (btnExpand) { btnExpand.onclick = () => { if (currentSidebarWidthIndex < SIDEBAR_WIDTH_CLASSES.length - 1) { sidebar.classList.remove(SIDEBAR_WIDTH_CLASSES[currentSidebarWidthIndex]); currentSidebarWidthIndex++; sidebar.classList.add(SIDEBAR_WIDTH_CLASSES[currentSidebarWidthIndex]); } }; } // Disminuir ancho if (btnShrink) { btnShrink.onclick = () => { if (currentSidebarWidthIndex > 0) { sidebar.classList.remove(SIDEBAR_WIDTH_CLASSES[currentSidebarWidthIndex]); currentSidebarWidthIndex--; sidebar.classList.add(SIDEBAR_WIDTH_CLASSES[currentSidebarWidthIndex]); } }; } // Delegación táctil reactiva e inmediata para tarjetas del feed (SSR y dinámicas - Pilar 1) const feedListContainer = document.getElementById('seismic-feed-list'); if (feedListContainer && !feedListContainer._hasFeedDelegation) { feedListContainer._hasFeedDelegation = true; feedListContainer.addEventListener('click', (e) => { const card = e.target.closest('[data-eq-id]'); if (!card) return; // Fase 198.7: Si la tarjeta dinámica ya tiene su propio card.onclick adjunto, no duplicar ni abrir el modal encima de la Vista Dividida if (typeof card.onclick === 'function') return; const shareBtn = e.target.closest('.btn-feed-card-share'); const eqId = card.getAttribute('data-eq-id'); const eq = (earthquakesData || []).find(x => x.id === eqId); if (shareBtn) { e.stopPropagation(); if (eq && typeof openShareModal === 'function') openShareModal(eq, e); return; } if (eq) { syncGlobeToEvent(eq, 2200, true); if (window.innerWidth < 768 && typeof window.showEventOnGlobeSplitViewMobile === 'function') { const previewBar = document.getElementById('mobile-split-preview-bar'); if (selectedEvent && String(selectedEvent.id) === String(eq.id) && previewBar && !previewBar.classList.contains('hidden')) { previewBar.classList.add('hidden'); openEventDetails(eq); return; } selectedEvent = eq; window.showEventOnGlobeSplitViewMobile(eq); return; } openEventDetails(eq); } else { const lat = parseFloat(card.getAttribute('data-eq-lat') || '0'); const lon = parseFloat(card.getAttribute('data-eq-lon') || '0'); const mag = parseFloat(card.getAttribute('data-eq-mag') || '0'); const place = card.getAttribute('data-eq-place') || 'Sismo'; if (myGlobe && lat && lon) { focusEarthquake({ latitude: lat, longitude: lon, magnitude: mag, place }, 2200); } } }); } } // ==================================================================== // 8.8 PALANCA VERTICAL TÁCTICA DE ZOOM ORBITAL 3D (FASE 10.9) // ==================================================================== function initGlobeZoomSlider() { const slider = document.getElementById('globe-zoom-slider'); const btnIn = document.getElementById('btn-zoom-in'); const btnOut = document.getElementById('btn-zoom-out'); const btnReset = document.getElementById('btn-zoom-reset'); const tooltip = document.getElementById('globe-zoom-tooltip'); if (!slider) return; const MIN_ALT = 0.15; const MAX_ALT = 3.80; function altToSlider(alt) { const clamped = Math.max(MIN_ALT, Math.min(MAX_ALT, alt)); return Math.round(((MAX_ALT - clamped) / (MAX_ALT - MIN_ALT)) * 100); } function sliderToAlt(val) { const ratio = Number(val) / 100; return Number((MAX_ALT - ratio * (MAX_ALT - MIN_ALT)).toFixed(2)); } function showTooltip(text) { if (!tooltip) return; tooltip.innerText = text; tooltip.classList.remove('opacity-0'); clearTimeout(tooltip._timer); tooltip._timer = setTimeout(() => { tooltip.classList.add('opacity-0'); }, 1500); } // 1. Arrastre directo de la palanca vertical slider.oninput = (e) => { if (!myGlobe) return; const targetAlt = sliderToAlt(e.target.value); window.userSelectedAltitude = targetAlt; myGlobe.pointOfView({ altitude: targetAlt }); showTooltip(`${(1 / targetAlt).toFixed(1)}x`); }; // 2. Botón (+) Zoom IN if (btnIn) { btnIn.onclick = () => { if (!myGlobe) return; const currentPov = myGlobe.pointOfView() || { altitude: 1.8 }; const newAlt = Math.max(MIN_ALT, (currentPov.altitude || 1.8) * 0.75); window.userSelectedAltitude = newAlt; myGlobe.pointOfView({ altitude: newAlt }, 300); slider.value = altToSlider(newAlt); showTooltip(`${(1 / newAlt).toFixed(1)}x`); }; } // 3. Botón (-) Zoom OUT if (btnOut) { btnOut.onclick = () => { if (!myGlobe) return; const currentPov = myGlobe.pointOfView() || { altitude: 1.8 }; const newAlt = Math.min(MAX_ALT, (currentPov.altitude || 1.8) * 1.3); window.userSelectedAltitude = newAlt; myGlobe.pointOfView({ altitude: newAlt }, 300); slider.value = altToSlider(newAlt); showTooltip(`${(1 / newAlt).toFixed(1)}x`); }; } // 4. Botón (1x) Reset Perspectiva Estándar if (btnReset) { btnReset.onclick = () => { if (!myGlobe) return; window.userSelectedAltitude = 1.8; myGlobe.pointOfView({ altitude: 1.8 }, 450); slider.value = altToSlider(1.8); showTooltip('1.0x'); }; } // 5. Sincronización bidireccional continua con OrbitControls (Throttling a 60 FPS con rAF) const controls = myGlobe && typeof myGlobe.controls === 'function' ? myGlobe.controls() : null; let _rafZoomSlider = null; if (controls) { controls.addEventListener('change', () => { if (document.activeElement === slider) return; if (_rafZoomSlider) return; _rafZoomSlider = requestAnimationFrame(() => { _rafZoomSlider = null; const pov = myGlobe.pointOfView(); if (pov && typeof pov.altitude === 'number') { slider.value = altToSlider(pov.altitude); if (isUserManuallyInteracting) { window.userSelectedAltitude = pov.altitude; } } }); }); controls.addEventListener('end', () => { if (_rafZoomSlider) { cancelAnimationFrame(_rafZoomSlider); _rafZoomSlider = null; } const pov = myGlobe.pointOfView(); if (pov && typeof pov.altitude === 'number') { slider.value = altToSlider(pov.altitude); window.userSelectedAltitude = pov.altitude; } }); } } // ==================================================================== // 9. CENTRO DE CONTROL Y OPERACIONES (NOC) - TELEMETRÍA Y EXPANSIÓN // ==================================================================== let nocModalOpen = false; let nocActiveTab = 'users'; // 'users', 'expansion', 'agencies', 'crisis' let nocClockTimer = null; let nocAlarmOscillator = null; let nocAlarmGain = null; let nocAlarmActive = false; let userTelemetry = { country: 'Argentina', countryCode: 'AR', city: 'San Juan', lat: -31.5375, lon: -68.5364, tz: Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Argentina/San_Juan', lang: navigator.language || 'es', closestDistKm: null, exposureStatus: 'safe', source: 'Default / GPS' }; function initNocDashboard() { // Botón discreto en barra inferior const btnOpenNoc = document.getElementById('btn-open-noc'); if (btnOpenNoc) { btnOpenNoc.onclick = openNocDashboard; } // Atajo de teclado táctico para operadores (Ctrl + Shift + O) window.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'O' || e.key === 'o' || e.code === 'KeyO')) { e.preventDefault(); openNocDashboard(); } }); // Botón cerrar modal const btnCloseNoc = document.getElementById('btn-close-noc'); if (btnCloseNoc) { btnCloseNoc.onclick = closeNocDashboard; } // Navegación por pestañas (6 Pestañas Operativas) const tabs = [ { btnId: 'tab-noc-btn-analytics', contentId: 'tab-noc-content-analytics', key: 'analytics' }, { btnId: 'tab-noc-btn-subscribers', contentId: 'tab-noc-content-subscribers', key: 'subscribers' }, { btnId: 'tab-noc-btn-marketing', contentId: 'tab-noc-content-marketing', key: 'marketing' }, { btnId: 'tab-noc-btn-automations', contentId: 'tab-noc-content-automations', key: 'automations' }, { btnId: 'tab-noc-btn-agencies', contentId: 'tab-noc-content-agencies', key: 'agencies' }, { btnId: 'tab-noc-btn-growth', contentId: 'tab-noc-content-growth', key: 'growth' }, { btnId: 'tab-noc-btn-crisis', contentId: 'tab-noc-content-crisis', key: 'crisis' } ]; tabs.forEach(t => { const btn = document.getElementById(t.btnId); if (btn) { btn.onclick = () => { tabs.forEach(other => { const b = document.getElementById(other.btnId); const c = document.getElementById(other.contentId); if (b) { b.className = 'tab-noc-btn px-4 py-2.5 border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center space-x-2 transition-all cursor-pointer'; } if (c) c.classList.add('hidden'); }); btn.className = 'tab-noc-btn active px-4 py-2.5 border-b-2 border-sky-400 text-sky-300 flex items-center space-x-2 transition-all cursor-pointer'; const currentContent = document.getElementById(t.contentId); if (currentContent) currentContent.classList.remove('hidden'); nocActiveTab = t.key; renderNocTab(t.key); }; } }); // Botones Selector de Período en Tab 1 (Analítica) document.querySelectorAll('.noc-period-btn').forEach(btn => { btn.onclick = () => { document.querySelectorAll('.noc-period-btn').forEach(b => { b.className = 'noc-period-btn px-3 py-1 rounded-lg text-slate-400 hover:text-white transition-all cursor-pointer'; }); btn.className = 'noc-period-btn active px-3 py-1 rounded-lg bg-sky-600 text-white transition-all cursor-pointer'; const period = btn.getAttribute('data-period') || 'day'; currentAnalyticsPeriod = period; renderNocChart(period); }; }); // Recalibrar telemetría const btnRefreshTele = document.getElementById('btn-noc-refresh-telemetry'); if (btnRefreshTele) { btnRefreshTele.onclick = () => refreshUserTelemetry(true); } // Copiar código del Widget const btnCopyWidget = document.getElementById('btn-noc-copy-widget'); if (btnCopyWidget) { btnCopyWidget.onclick = copyNocWidgetCode; } const widgetModeSel = document.getElementById('noc-widget-mode'); if (widgetModeSel) widgetModeSel.onchange = updateNocWidgetPreview; const widgetThemeSel = document.getElementById('noc-widget-theme'); if (widgetThemeSel) widgetThemeSel.onchange = updateNocWidgetPreview; // Botón activar Notificaciones Push PWA const btnPush = document.getElementById('btn-noc-request-push'); if (btnPush) { btnPush.onclick = requestPushPermission; } // Suscriptores: Búsqueda, Filtro de Estado y Filtro de Perfil/Persona const subSearch = document.getElementById('noc-subscribers-search'); if (subSearch) subSearch.oninput = renderNocSubscribersTab; const subFilter = document.getElementById('noc-subscribers-filter-status'); if (subFilter) subFilter.onchange = renderNocSubscribersTab; const subPersonaFilter = document.getElementById('noc-subscribers-filter-persona'); if (subPersonaFilter) subPersonaFilter.onchange = renderNocSubscribersTab; // Suscriptores: Exportar CSV const btnExpSubCsv = document.getElementById('btn-noc-export-subscribers-csv'); if (btnExpSubCsv) btnExpSubCsv.onclick = exportSubscribersCSV; // Suscriptores: Agregar nuevo const btnAddSub = document.getElementById('btn-noc-add-subscriber'); if (btnAddSub) btnAddSub.onclick = promptAddNewSubscriber; // Email Alert Studio: Selector de Sismo const selMailEvent = document.getElementById('noc-mail-event-select'); if (selMailEvent) selMailEvent.onchange = updateNocMailPreview; // Email Alert Studio: Selector de Perfil / Persona (Botones Bento) document.querySelectorAll('.noc-persona-btn').forEach(btn => { btn.onclick = () => { document.querySelectorAll('.noc-persona-btn').forEach(b => { b.className = 'noc-persona-btn p-2 rounded-xl bg-slate-950 border border-slate-800 text-slate-400 hover:text-slate-200 text-center flex flex-col items-center gap-1 transition-all cursor-pointer'; }); const persona = btn.getAttribute('data-persona') || 'general'; studioActivePersona = persona; if (persona === 'general') { btn.className = 'noc-persona-btn active p-2 rounded-xl bg-slate-950 border border-emerald-500/50 text-emerald-300 text-center flex flex-col items-center gap-1 transition-all cursor-pointer'; } else if (persona === 'scientist') { btn.className = 'noc-persona-btn active p-2 rounded-xl bg-slate-950 border border-sky-500/50 text-sky-300 text-center flex flex-col items-center gap-1 transition-all cursor-pointer'; } else { btn.className = 'noc-persona-btn active p-2 rounded-xl bg-slate-950 border border-amber-500/50 text-amber-300 text-center flex flex-col items-center gap-1 transition-all cursor-pointer'; } updateNocMailPreview(); }; }); // Email Alert Studio: Conmutador de Modos de Vista (Desktop, Móvil, Código HTML) const btnMailDesktop = document.getElementById('btn-mail-view-desktop'); const btnMailMobile = document.getElementById('btn-mail-view-mobile'); const btnMailCode = document.getElementById('btn-mail-view-code'); function setMailViewMode(mode) { studioViewMode = mode; const btns = [ { el: btnMailDesktop, m: 'desktop' }, { el: btnMailMobile, m: 'mobile' }, { el: btnMailCode, m: 'code' } ]; btns.forEach(({ el, m }) => { if (!el) return; if (m === mode) { el.className = 'mail-view-btn active py-1 rounded-lg bg-sky-600 text-white font-bold transition-all cursor-pointer flex items-center justify-center gap-1'; } else { el.className = 'mail-view-btn py-1 rounded-lg text-slate-400 hover:text-white font-bold transition-all cursor-pointer flex items-center justify-center gap-1'; } }); updateNocMailPreview(); } if (btnMailDesktop) btnMailDesktop.onclick = () => setMailViewMode('desktop'); if (btnMailMobile) btnMailMobile.onclick = () => setMailViewMode('mobile'); if (btnMailCode) btnMailCode.onclick = () => setMailViewMode('code'); // Email Alert Studio: Copiar Código HTML Inline const btnCopyMailHtml = document.getElementById('btn-noc-copy-email-html'); if (btnCopyMailHtml) btnCopyMailHtml.onclick = copyEmailStudioHtml; const btnGenMail = document.getElementById('btn-noc-generate-mail-preview'); if (btnGenMail) btnGenMail.onclick = updateNocMailPreview; const btnSimMail = document.getElementById('btn-noc-simulate-mail-dispatch'); if (btnSimMail) btnSimMail.onclick = simulateMailDispatch; // Ping a Agencias const btnPingAgencies = document.getElementById('btn-noc-ping-agencies'); if (btnPingAgencies) { btnPingAgencies.onclick = measureAgenciesLatency; } // War-Room Sirena const btnAlarm = document.getElementById('btn-noc-trigger-alarm'); if (btnAlarm) { btnAlarm.onclick = toggleNocAlarm; } // War-Room Enfocar Globo const btnFocusCrisis = document.getElementById('btn-noc-focus-globe-crisis'); if (btnFocusCrisis) { btnFocusCrisis.onclick = () => { const mostPowerful = getMostPowerfulRecentEvent(earthquakesData) || earthquakesData[0]; if (mostPowerful) { closeNocDashboard(); focusEarthquake(mostPowerful); } }; } // Gateway de Seguridad del NOC (Autenticación por PIN/Clave) const btnCloseAuth = document.getElementById('btn-close-noc-auth'); if (btnCloseAuth) { btnCloseAuth.onclick = closeNocAuth; } const btnPinClear = document.getElementById('btn-noc-pin-clear'); if (btnPinClear) { btnPinClear.onclick = () => { const pin = document.getElementById('noc-auth-pin'); if (pin) pin.value = ''; const err = document.getElementById('noc-auth-error'); if (err) err.classList.add('hidden'); }; } const btnPinSubmit = document.getElementById('btn-noc-pin-submit'); if (btnPinSubmit) { btnPinSubmit.onclick = submitNocPin; } document.querySelectorAll('#modal-noc-auth .keypad-btn').forEach(b => { b.onclick = () => { const pin = document.getElementById('noc-auth-pin'); if (pin && pin.value.length < 24) { pin.value += b.getAttribute('data-val'); } const err = document.getElementById('noc-auth-error'); if (err) err.classList.add('hidden'); }; }); const pinInput = document.getElementById('noc-auth-pin'); if (pinInput) { pinInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); submitNocPin(); } }; } // Botón Bloquear Sesión const btnLockNoc = document.getElementById('btn-lock-noc'); if (btnLockNoc) { btnLockNoc.onclick = () => { sessionStorage.removeItem('terremoto_noc_auth'); closeNocDashboard(); showToast('🔒 Sesión del NOC bloqueada con éxito'); }; } // Iniciar telemetría pasiva y ledger de tráfico initAnalyticsLedger(); refreshUserTelemetry(false); } // ==================================================================== // LEDGER DE TELEMETRÍA Y ANALÍTICA DE AUDIENCIA // ==================================================================== let sessionStartTime = Date.now(); let currentAnalyticsPeriod = 'day'; function initAnalyticsLedger() { let ledger = null; try { ledger = JSON.parse(localStorage.getItem('terremoto_analytics_ledger')); } catch (e) {} if (!ledger || !ledger.hourly) { ledger = { todayVisits: 1, todayViews: 1, hourly: [], weekly: [], monthly: [], countries: [] }; localStorage.setItem('terremoto_analytics_ledger', JSON.stringify(ledger)); } else { ledger.todayViews += 1; localStorage.setItem('terremoto_analytics_ledger', JSON.stringify(ledger)); } } function getAnalyticsLedger() { try { return JSON.parse(localStorage.getItem('terremoto_analytics_ledger')) || {}; } catch (e) { return {}; } } function closeNocAuth() { const modal = document.getElementById('modal-noc-auth'); if (modal) modal.classList.add('hidden'); } function submitNocPin() { const pin = document.getElementById('noc-auth-pin'); const err = document.getElementById('noc-auth-error'); const card = document.getElementById('noc-auth-card'); const val = pin ? pin.value.trim() : ''; // PIN táctico '2026', '7476' o clave 'TERREMOTO-NOC-2026' if (val === '2026' || val === '7476' || val.toUpperCase() === 'TERREMOTO-NOC-2026') { sessionStorage.setItem('terremoto_noc_auth', 'true'); if (err) err.classList.add('hidden'); closeNocAuth(); showToast('🔓 Acceso Autorizado al Centro de Operaciones NOC'); proceedOpenNocDashboard(); } else { if (err) err.classList.remove('hidden'); if (card) { card.classList.add('shake-error'); setTimeout(() => card.classList.remove('shake-error'), 450); } if (pin) { pin.value = ''; pin.focus(); } } } function openNocDashboard() { if (typeof trackToolHit === 'function') trackToolHit('noc_console'); window.open('/dashboard.html', '_blank'); } function proceedOpenNocDashboard() { const modal = document.getElementById('modal-noc-dashboard'); if (!modal) return; modal.classList.remove('hidden'); nocModalOpen = true; updateNocClocks(); if (!nocClockTimer) { nocClockTimer = setInterval(updateNocClocks, 1000); } refreshUserTelemetry(false); renderNocTopKpis(); renderNocTab(nocActiveTab || 'analytics'); updateNocWidgetPreview(); // Refresco automático de analítica y heatmap en vivo cada 15s (Entrada #384) if (!window._nocAnalyticsTimer) { window._nocAnalyticsTimer = setInterval(() => { if (nocModalOpen) { renderNocTab(nocActiveTab || 'analytics'); } }, 15000); } } function closeNocDashboard() { const modal = document.getElementById('modal-noc-dashboard'); if (!modal) return; modal.classList.add('hidden'); nocModalOpen = false; if (nocClockTimer) { clearInterval(nocClockTimer); nocClockTimer = null; } if (window._nocAnalyticsTimer) { clearInterval(window._nocAnalyticsTimer); window._nocAnalyticsTimer = null; } stopNocAlarm(); } function updateNocClocks() { const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); const utcStr = `${pad(now.getUTCHours())}:${pad(now.getUTCMinutes())}:${pad(now.getUTCSeconds())} UTC`; const locStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())} LOCAL`; const elUtc = document.getElementById('noc-clock-utc'); if (elUtc) elUtc.innerText = utcStr; const elLoc = document.getElementById('noc-clock-local'); if (elLoc) elLoc.innerText = locStr; // Actualizar tiempo de permanencia en Tab 1 const durEl = document.getElementById('noc-analytics-avg-duration'); if (durEl) { const elapsedSec = Math.floor((Date.now() - sessionStartTime) / 1000); const mins = Math.floor(elapsedSec / 60); const secs = elapsedSec % 60; durEl.innerText = `${mins}m ${secs}s`; } } function refreshUserTelemetry(forceGps) { if (forceGps && navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { userTelemetry.lat = pos.coords.latitude; userTelemetry.lon = pos.coords.longitude; userTelemetry.source = 'GPS Preciso'; reverseGeocodeTelemetry(userTelemetry.lat, userTelemetry.lon); updateUserTelemetryUI(); showToast('✅ Posición GPS actualizada en Centro NOC'); }, () => { detectPassiveIpTelemetry(); }, { timeout: 6000 } ); } else { detectPassiveIpTelemetry(); } } function detectPassiveIpTelemetry() { fetch('https://ipapi.co/json/', { cache: 'no-store' }) .then(res => res.json()) .then(data => { if (data && data.country_name) { userTelemetry.country = data.country_name; userTelemetry.countryCode = data.country_code; userTelemetry.city = data.city || 'Desconocida'; userTelemetry.lat = data.latitude || userTelemetry.lat; userTelemetry.lon = data.longitude || userTelemetry.lon; userTelemetry.tz = data.timezone || userTelemetry.tz; userTelemetry.source = 'IP Geolocation'; updateUserTelemetryUI(); } }) .catch(() => { updateUserTelemetryUI(); }); } function reverseGeocodeTelemetry(lat, lon) { fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`, { headers: { 'User-Agent': 'TerremotoNOC/2.0' } }) .then(res => res.json()) .then(data => { if (data && data.address) { userTelemetry.country = data.address.country || userTelemetry.country; userTelemetry.countryCode = (data.address.country_code || '').toUpperCase(); userTelemetry.city = data.address.city || data.address.town || data.address.state || userTelemetry.city; updateUserTelemetryUI(); } }) .catch(() => {}); } function updateUserTelemetryUI() { let minDist = Infinity; let closestQuake = null; for (const eq of earthquakesData) { const d = haversineDistance(userTelemetry.lat, userTelemetry.lon, eq.latitude, eq.longitude); if (d < minDist) { minDist = d; closestQuake = eq; } } userTelemetry.closestDistKm = isFinite(minDist) ? Math.round(minDist) : null; userTelemetry.exposureStatus = (userTelemetry.closestDistKm && userTelemetry.closestDistKm <= 200) ? 'warning' : 'safe'; const countryVal = document.getElementById('noc-user-country-val'); if (countryVal) { const flag = getCountryFlagEmoji(userTelemetry.countryCode || 'AR'); countryVal.innerHTML = `${flag} ${userTelemetry.country} (${userTelemetry.source})`; } const cityVal = document.getElementById('noc-user-city-val'); if (cityVal) cityVal.innerText = userTelemetry.city || 'S/D'; const coordsVal = document.getElementById('noc-user-coords-val'); if (coordsVal) coordsVal.innerText = `${userTelemetry.lat.toFixed(4)}°, ${userTelemetry.lon.toFixed(4)}°`; const tzVal = document.getElementById('noc-user-tz-val'); if (tzVal) tzVal.innerText = `${userTelemetry.tz} (${userTelemetry.lang})`; const closestDistEl = document.getElementById('noc-user-closest-quake-dist'); if (closestDistEl) { if (closestQuake && userTelemetry.closestDistKm) { closestDistEl.innerHTML = `M ${closestQuake.magnitude.toFixed(1)} a ${userTelemetry.closestDistKm} km (${sanitizeSeismicText(closestQuake.place)})`; } else { closestDistEl.innerText = 'Sin sismos cercanos'; } } const exposureBadge = document.getElementById('noc-user-exposure-badge'); if (exposureBadge) { if (userTelemetry.exposureStatus === 'warning') { exposureBadge.className = 'font-bold text-rose-400 animate-pulse'; exposureBadge.innerHTML = '⚠️ Sismo en Radio Perceptible (< 200 km)'; } else { exposureBadge.className = 'font-bold text-emerald-400'; exposureBadge.innerHTML = '🟢 Fuera de radio perceptible'; } } const kpiCountry = document.getElementById('noc-kpi-user-country'); if (kpiCountry) { const flag = getCountryFlagEmoji(userTelemetry.countryCode || 'AR'); kpiCountry.innerText = `${flag} ${userTelemetry.city}, ${userTelemetry.country}`; } const kpiStatus = document.getElementById('noc-kpi-user-status'); if (kpiStatus) { if (userTelemetry.closestDistKm) { kpiStatus.innerText = `Sismo más cercano a ${userTelemetry.closestDistKm} km`; kpiStatus.className = userTelemetry.closestDistKm <= 200 ? 'text-[10px] text-rose-400 font-bold truncate mt-0.5' : 'text-[10px] text-emerald-400 font-medium truncate mt-0.5'; } else { kpiStatus.innerText = 'Calculando proximidad...'; } } } function getCountryFlagEmoji(c) { if (!c) return (window.SVG_FLAGS && window.SVG_FLAGS['GL']) ? window.SVG_FLAGS['GL'] : '🌐'; c = c.toUpperCase(); let code = 'GL'; if (c === 'AR' || c.includes('ARGENTINA')) code = 'AR'; else if (c === 'CL' || c.includes('CHILE')) code = 'CL'; else if (c === 'MX' || c.includes('MÉXICO') || c.includes('MEXICO')) code = 'MX'; else if (c === 'US' || c.includes('ESTADOS UNIDOS') || c.includes('USA')) code = 'US'; else if (c === 'JP' || c.includes('JAPÓN') || c.includes('JAPAN')) code = 'JP'; else if (c === 'IT' || c.includes('ITALIA') || c.includes('ITALY')) code = 'IT'; else if (c === 'PE' || c.includes('PERÚ') || c.includes('PERU')) code = 'PE'; else if (c === 'CO' || c.includes('COLOMBIA')) code = 'CO'; else if (c === 'NZ' || c.includes('NUEVA ZELANDA') || c.includes('NEW ZEALAND')) code = 'NZ'; else if (c === 'DE' || c.includes('ALEMANIA') || c.includes('GERMANY')) code = 'DE'; else if (c === 'ES' || c.includes('ESPAÑA') || c.includes('SPAIN')) code = 'ES'; else if (c === 'ID' || c.includes('INDONESIA')) code = 'ID'; else if (c === 'GR' || c.includes('GRECIA') || c.includes('GREECE')) code = 'GR'; else if (c === 'BR' || c.includes('BRASIL') || c.includes('BRAZIL')) code = 'BR'; else if (c === 'EC' || c.includes('ECUADOR')) code = 'EC'; if (window.SVG_FLAGS && window.SVG_FLAGS[code]) { return window.SVG_FLAGS[code].replace('w-5 h-3.5', 'w-4 h-2.5'); } return '🌐'; } function renderNocTopKpis() { const subscribers = getSubscribersList(); const apiSubsCount = (cachedAnalyticsData && typeof cachedAnalyticsData.activeSubscribers === 'number') ? cachedAnalyticsData.activeSubscribers : subscribers.length; const subCountEl = document.getElementById('noc-kpi-subscribers-count'); if (subCountEl) subCountEl.innerText = Math.max(apiSubsCount, subscribers.length); const localDyfiCount = (typeof citizenReportsList !== 'undefined' ? citizenReportsList.length : 0); const apiDyfiCount = (cachedAnalyticsData && typeof cachedAnalyticsData.dyfiReportsTotal === 'number') ? cachedAnalyticsData.dyfiReportsTotal : localDyfiCount; const dyfiEl = document.getElementById('noc-kpi-dyfi-count'); if (dyfiEl) dyfiEl.innerText = `${Math.max(apiDyfiCount, localDyfiCount)} Reportes DYFI`; const mostPowerful = getMostPowerfulRecentEvent(earthquakesData) || earthquakesData[0]; if (mostPowerful) { const magEl = document.getElementById('noc-kpi-critical-mag'); if (magEl) magEl.innerText = `M ${mostPowerful.magnitude.toFixed(1)}`; const placeEl = document.getElementById('noc-kpi-critical-place'); if (placeEl) placeEl.innerText = sanitizeSeismicText(mostPowerful.place); } const agencies = Object.values(agencyStatus); const onlineCount = agencies.filter(a => a.status === 'online' || a.count > 0 || (typeof a.latencyMs === 'number' && a.latencyMs > 0)).length; const badgeEl = document.getElementById('noc-kpi-network-badge'); if (badgeEl) { badgeEl.innerText = `${onlineCount} / ${agencies.length} ONLINE`; badgeEl.className = onlineCount >= 6 ? 'px-1.5 py-0.5 rounded text-[8px] font-mono bg-emerald-500/20 text-emerald-300' : 'px-1.5 py-0.5 rounded text-[8px] font-mono bg-amber-500/20 text-amber-300'; } const lats = agencies.filter(a => typeof a.latencyMs === 'number' && a.latencyMs > 0).map(a => a.latencyMs); const avgLat = lats.length > 0 ? Math.round(lats.reduce((a, b) => a + b, 0) / lats.length) : null; const latEl = document.getElementById('noc-kpi-latency-avg'); if (latEl) { latEl.innerText = avgLat ? `Latencia: ${avgLat} ms` : 'Latencia: En verificación'; } const consensusRateEl = document.getElementById('noc-kpi-consensus-rate'); if (consensusRateEl) { consensusRateEl.innerText = `${earthquakesData.length} eventos en vivo`; } } // ==================================================================== // TELEMETRÍA GLOBAL: HOOK DE REGISTRO EN VIVO (/api/analytics/hit) // ==================================================================== function trackToolHit(toolId) { if (!toolId) return; try { fetch('/api/analytics/hit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'tool_hit', toolId: toolId, target: toolId }) }).catch(() => {}); } catch (e) {} } // TELEMETRÍA GLOBAL: REGISTRO DE CLICS E INTERACCIÓN TÁCTICA (Entrada #384) const _sismoLastClickHits = {}; function trackClickHit(zone) { if (!zone) return; const now = Date.now(); if (_sismoLastClickHits[zone] && (now - _sismoLastClickHits[zone] < 300)) return; _sismoLastClickHits[zone] = now; try { const payload = JSON.stringify({ type: 'click_hit', target: zone, zone: zone, domain: window.location.hostname || 'sismosfera.com' }); if (navigator.sendBeacon) { navigator.sendBeacon('/api/analytics/hit', new Blob([payload], { type: 'application/json' })); } else { fetch('/api/analytics/hit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true }).catch(() => {}); } } catch (e) {} } // CAPTURA GLOBAL DE CLICS E INTERACCIÓN TÁCTICA (Entrada #384) document.addEventListener('click', function(e) { try { const target = e.target; if (!target) return; // 1. Globo 3D Interactivo if (target.closest('#globeViz') || target.closest('#globe-container') || (target.tagName && target.tagName.toLowerCase() === 'canvas')) { trackClickHit('globo_3d'); return; } // 2. Feed Lateral Sísmico if (target.closest('#feed-list') || target.closest('.feed-card') || target.closest('#feed-quick-comp-bar') || target.closest('#left-sidebar')) { trackClickHit('feed_lateral'); return; } // 3. Dock Táctico Inferior if (target.closest('#bottom-dock') || target.closest('.mobile-dock-btn') || target.closest('.sismosfera-dock-item') || target.closest('#tactical-dock')) { trackClickHit('dock_tactico'); return; } // 4. Capas Tectónicas & Mapas if (target.closest('#layer-menu') || target.closest('#dropdown-menu-layers') || target.closest('#btn-layers') || target.closest('[data-layer]')) { trackClickHit('controles_capas'); return; } // 5. Buscador & Filtros if (target.closest('#search-input') || target.closest('#search-btn') || target.closest('#filter-controls') || target.closest('#slider-mag') || target.closest('#time-filter-select')) { trackClickHit('buscador_filtros'); return; } // 6. Suscripción a Alertas Sísmicas if (target.closest('#btn-activate-alerts') || target.closest('#modal-alert') || target.closest('[onclick*="openAlertModal"]')) { trackClickHit('alertas_suscripcion'); return; } // 7. Reporte Ciudadano DYFI if (target.closest('#btn-open-dyfi') || target.closest('#modal-dyfi-form') || target.closest('[onclick*="openDyfiModal"]')) { trackClickHit('reporte_ciudadano'); return; } // 8. Timelapse Histórico if (target.closest('#timelapse-container') || target.closest('#btn-timelapse') || target.closest('#timelapse-play-btn')) { trackClickHit('timelapse_historico'); return; } // 9. Catálogo de Herramientas Científicas if (target.closest('#dropdown-menu-science') || target.closest('#modal-suite-tools-catalog') || target.closest('[onclick*="openSuiteToolsCatalog"]') || target.closest('[onclick*="openScienceMenu"]')) { trackClickHit('catalogo_herramientas'); return; } // 10. Manual Científico & Ayuda if (target.closest('#modal-manual-operaciones') || target.closest('[onclick*="openManualModal"]') || target.closest('#btn-help-manual')) { trackClickHit('manual_operaciones'); return; } // 11. Consola NOC if (target.closest('#modal-noc-dashboard') || target.closest('[onclick*="openNocDashboard"]')) { trackClickHit('noc_console'); return; } } catch (err) {} }, { passive: true, capture: true }); // TELEMETRÍA GLOBAL: REGISTRO REAL DE VISITAS Y DOMINIO (function trackPageVisit() { try { const isNew = !localStorage.getItem('sismo_vis_id'); if (isNew) { try { localStorage.setItem('sismo_vis_id', '1'); } catch(e) {} } const urlParams = new URLSearchParams(window.location.search || ''); fetch('/api/analytics/hit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'page_visit', domain: window.location.hostname || 'sismosfera.com', isNewVisitor: isNew, utmSource: urlParams.get('utm_source') || '', utmMedium: urlParams.get('utm_medium') || '', utmCampaign: urlParams.get('utm_campaign') || '', referrer: document.referrer || '' }) }).catch(() => {}); } catch (e) {} })(); // ==================================================================== // PESTAÑA 1: ANALÍTICA REAL, RANKING DE HERRAMIENTAS Y HEATMAP // ==================================================================== let cachedAnalyticsData = null; async function fetchAnalyticsData() { try { const res = await fetch('/api/analytics'); if (res.ok) { const json = await res.json(); if (json && json.analytics) { cachedAnalyticsData = json.analytics; return json.analytics; } } } catch (e) { console.warn('[ANALYTICS FETCH WARN]', e); } return cachedAnalyticsData; } async function renderNocAnalyticsTab() { const analytics = await fetchAnalyticsData(); const ledger = getAnalyticsLedger(); renderNocTopKpis(); const visitsEl = document.getElementById('noc-analytics-today-visits'); if (visitsEl) { const todayCount = (analytics && typeof analytics.todayVisits === 'number') ? analytics.todayVisits : ((analytics && typeof analytics.totalVisits === 'number') ? analytics.totalVisits : (ledger && typeof ledger.todayVisits === 'number' ? ledger.todayVisits : null)); visitsEl.innerText = todayCount !== null ? Number(todayCount).toLocaleString() : 'S/D'; } const viewsEl = document.getElementById('noc-analytics-views-today'); if (viewsEl) { const totalCum = (analytics && typeof analytics.totalVisits === 'number') ? analytics.totalVisits : ((analytics && typeof analytics.totalViews === 'number') ? analytics.totalViews : (ledger && typeof ledger.todayViews === 'number' ? ledger.todayViews : null)); viewsEl.innerText = totalCum !== null ? `${Number(totalCum).toLocaleString()} acumuladas` : 'S/D'; } renderNocChart(currentAnalyticsPeriod); renderNocToolRanking(analytics?.toolHits); renderNocClickHeatmap(analytics?.clickHeatmap); renderNocHeatmap(analytics?.countryHits); } function renderNocChart(period) { const ledger = getAnalyticsLedger(); const container = document.getElementById('noc-chart-container'); const labelsEl = document.getElementById('noc-chart-labels'); const titleEl = document.getElementById('noc-chart-title'); const totalEl = document.getElementById('noc-chart-total-label'); if (!container) return; const dailyHitsObj = (cachedAnalyticsData && cachedAnalyticsData.dailyHits && typeof cachedAnalyticsData.dailyHits === 'object') ? cachedAnalyticsData.dailyHits : {}; const sortedDailyEntries = Object.entries(dailyHitsObj) .sort((a, b) => a[0].localeCompare(b[0])) .map(([isoDate, count]) => ({ label: isoDate.slice(5), // MM-DD visits: Number(count || 0) })); let data = []; let total = 0; if (period === 'day') { data = sortedDailyEntries.length > 0 ? sortedDailyEntries.slice(-7) : (Array.isArray(ledger?.hourly) ? ledger.hourly : []); if (titleEl) titleEl.innerText = 'Tráfico Diario Reciente - Últimos 7 Días'; } else if (period === 'week') { data = sortedDailyEntries.length > 0 ? sortedDailyEntries.slice(-14) : (Array.isArray(ledger?.weekly) ? ledger.weekly : []); if (titleEl) titleEl.innerText = 'Tráfico Quincenal - Últimos 14 Días'; } else if (period === 'month') { data = sortedDailyEntries.length > 0 ? sortedDailyEntries : (Array.isArray(ledger?.monthly) ? ledger.monthly : []); if (titleEl) titleEl.innerText = `Tráfico Mensual Acumulado (${sortedDailyEntries.length} Días)`; } if (data.length === 0) { if (totalEl) totalEl.innerText = 'Total período: S/D'; container.innerHTML = `
📊 S/D — Sin registros de tráfico en este período La telemetría se acumulará conforme los usuarios naveguen por la suite
`; if (labelsEl) labelsEl.innerHTML = ''; return; } total = data.reduce((acc, curr) => acc + (curr.visits || 0), 0); if (totalEl) totalEl.innerText = `Total período: ${total.toLocaleString()} visitas`; const maxVal = Math.max(...data.map(d => d.visits), 10); let barsSvg = ``; barsSvg += ` `; barsSvg += ``; data.forEach((d, idx) => { const barH = Math.round((d.visits / maxVal) * 95); const x = idx * 40 + 8; const y = 110 - barH; barsSvg += ` ${d.label}: ${d.visits} visitas ${d.visits} `; }); barsSvg += ``; container.innerHTML = barsSvg; if (labelsEl) { labelsEl.innerHTML = data.map(d => `${d.label}`).join(''); } } // Ranking de Herramientas Más Usadas function renderNocToolRanking(toolHits) { const container = document.getElementById('noc-tool-ranking-container'); if (!container) return; const toolMetadata = { major_history: { name: 'Grandes Terremotos Históricos', icon: '📜' }, focal_mechanism: { name: 'Beachball Tensor de Momento', icon: '⚽' }, seismogram: { name: 'Sismograma EarthScope FDSN', icon: '🎯' }, ak135: { name: 'Curvas Teóricas ak135', icon: '⏱️' }, shakemap: { name: 'ShakeMap MMI & PGA', icon: '🗺️' }, coulomb: { name: 'Transferencia Coulomb', icon: '⚡' }, finite_fault: { name: 'Falla Finita Kinemática', icon: '📐' }, dart_buoys: { name: 'Boyas DART & Tsunamis', icon: '🌊' }, pager: { name: 'PAGER Daños & Víctimas', icon: '🚨' }, omori: { name: 'Ley de Omori-Utsu Réplicas', icon: '📈' }, cryo_polar: { name: 'Criosismos Polares Antárticos', icon: '❄️' }, benioff: { name: 'Plano Benioff 3D', icon: '🪐' }, comparative: { name: 'Matriz Comparativa 24/48/72h', icon: '📊' } }; const hits = (toolHits && typeof toolHits === 'object' && Object.keys(toolHits).length > 0) ? toolHits : null; if (!hits) { container.innerHTML = `
S/D — Sin telemetría de herramientas registrada aún
`; return; } const sortedKeys = Object.keys(hits).sort((a, b) => (hits[b] || 0) - (hits[a] || 0)).slice(0, 8); const maxVal = Math.max(...sortedKeys.map(k => hits[k] || 0), 1); container.innerHTML = sortedKeys.map((k, idx) => { const meta = toolMetadata[k] || { name: k, icon: '🛠️' }; const val = hits[k] || 0; const pct = Math.round((val / maxVal) * 100); return `
#${idx + 1} ${meta.icon} ${meta.name} ${val} usos
`; }).join(''); } // Mapa de Calor de Clics function renderNocClickHeatmap(clickHeatmap) { const container = document.getElementById('noc-click-heatmap-container'); if (!container) return; const clickNames = { globo_3d: { name: 'Globo 3D Interactivo', icon: '🌐' }, feed_lateral: { name: 'Feed Sísmico en Tiempo Real', icon: '📋' }, dock_tactico: { name: 'Botonera & Dock Táctico', icon: '🎛️' }, controles_capas: { name: 'Capas Tectónicas & Volcanes', icon: '🗺️' }, buscador_filtros: { name: 'Buscador & Filtros de Magnitud', icon: '🔍' }, alertas_suscripcion: { name: 'Suscripción Alertas Sísmicas', icon: '🔔' }, reporte_ciudadano: { name: 'Reporte DYFI "¿Lo sentiste?"', icon: '📱' }, timelapse_historico: { name: 'Reproductor Timelapse 4D', icon: '⏱️' }, catalogo_herramientas: { name: 'Suite 24 Herramientas Científicas', icon: '🔬' }, manual_operaciones: { name: 'Manual Científico & Ayuda', icon: '📖' }, noc_console: { name: 'Consola Centro Control NOC', icon: '📊' }, globe_rotate: { name: 'Globo 3D Interactivo (Rotación & Zoom)', icon: '🌐' }, feed: { name: 'Feed Sismológico en Vivo', icon: '📋' }, history_modal: { name: 'Catálogo de Sismos Históricos', icon: '📜' }, manual_btn: { name: 'Manual Científico & Operaciones', icon: '📖' }, alert_btn: { name: 'Configuración Geocerca & Alertas', icon: '🔔' } }; const clicks = (clickHeatmap && typeof clickHeatmap === 'object' && Object.keys(clickHeatmap).length > 0) ? clickHeatmap : null; if (!clicks) { container.innerHTML = `
S/D — Sin interacciones de clic registradas aún
`; return; } const sortedKeys = Object.keys(clicks).sort((a, b) => (clicks[b] || 0) - (clicks[a] || 0)); const totalClicks = Object.values(clicks).reduce((a, b) => a + b, 0) || 1; container.innerHTML = sortedKeys.map(k => { const meta = clickNames[k] || { name: k, icon: '🎯' }; const val = clicks[k] || 0; const pct = Math.round((val / totalClicks) * 100); return `
${meta.icon} ${meta.name} ${val} clics (${pct}%)
`; }).join(''); } // Mapa de Procedencia Geográfica function renderNocHeatmap(countryHits) { const container = document.getElementById('noc-heatmap-container'); if (!container) return; const countries = (countryHits && typeof countryHits === 'object' && Object.keys(countryHits).length > 0) ? countryHits : null; if (!countries) { container.innerHTML = `
S/D — Sin procedencia geográfica acumulada
`; return; } const total = Object.values(countries).reduce((a, b) => a + b, 0) || 1; const sorted = Object.entries(countries).sort((a, b) => b[1] - a[1]); container.innerHTML = sorted.map(([name, visits]) => { const pct = Math.round((visits / total) * 100); const flag = getCountryFlagEmoji(name); const isUserHere = userTelemetry.country && userTelemetry.country.toLowerCase().includes(name.toLowerCase()); return `
${flag} ${name} ${isUserHere ? 'TU SESIÓN' : ''} ${visits} visitas (${pct}%)
`; }).join(''); } // ==================================================================== // PESTAÑA 2: GESTIÓN DE SUSCRIPTORES CONECTADA A /api/subscribers // ==================================================================== let currentSubscribersCache = []; async function loadSubscribersFromApi() { try { const res = await fetch('/api/subscribers'); if (res.ok) { const data = await res.json(); if (data && data.subscribers) { currentSubscribersCache = data.subscribers; localStorage.setItem('terremoto_subscribers', JSON.stringify(currentSubscribersCache)); return currentSubscribersCache; } } } catch (e) { console.warn('[SUBSCRIBERS FETCH WARN]', e); } return getSubscribersList(); } function getSubscribersList() { if (currentSubscribersCache && currentSubscribersCache.length > 0) { return currentSubscribersCache; } try { const local = JSON.parse(localStorage.getItem('terremoto_subscribers')) || []; if (Array.isArray(local) && local.length > 0) { currentSubscribersCache = local; return local; } } catch (e) {} // Regla de Oro 1: Cero suscriptores simulados si no existen en almacenamiento o backend currentSubscribersCache = []; return []; } async function renderNocSubscribersTab() { const tbody = document.getElementById('noc-subscribers-table-tbody'); const countLabel = document.getElementById('noc-subscribers-count-label'); if (!tbody) return; const subs = await loadSubscribersFromApi(); const search = (document.getElementById('noc-subscribers-search')?.value || '').toLowerCase(); const statusFilter = document.getElementById('noc-subscribers-filter-status')?.value || 'all'; const personaFilter = document.getElementById('noc-subscribers-filter-persona')?.value || 'all'; const filtered = subs.filter(s => { const matchesSearch = s.email.toLowerCase().includes(search) || (s.zone || '').toLowerCase().includes(search); const matchesStatus = (statusFilter === 'all') || (statusFilter === 'active' && s.active) || (statusFilter === 'paused' && !s.active); const matchesPersona = (personaFilter === 'all') || (s.persona === personaFilter); return matchesSearch && matchesStatus && matchesPersona; }); if (countLabel) countLabel.innerText = `Total: ${filtered.length} de ${subs.length} suscriptores`; if (filtered.length === 0) { tbody.innerHTML = ` No se encontraron suscriptores con los criterios seleccionados. `; return; } const personaBadges = { general: '🏠Público', scientist: '🔬Científico', journalist: '📰Periodista' }; tbody.innerHTML = filtered.map((s) => { const personaKey = s.persona || 'general'; const pBadge = personaBadges[personaKey] || personaBadges.general; const coverageStr = s.isGlobal ? '🌍 Global' : `📍 Local (${s.radiusKm || 250} km)`; const dateStr = s.createdAt ? s.createdAt.substring(0, 10) : (s.date || '2026-09-17'); return ` ✉️ ${s.email} ${pBadge} ${dateStr} ${coverageStr} ${s.isGlobal ? 'Planetario' : (s.radiusKm + ' km')} ≥ M ${(s.minMag || 4.5).toFixed(1)} ${s.active ? 'ACTIVO' : 'PAUSADO'} `; }).join(''); } function openEmailStudioForSubscriber(email, persona) { studioActivePersona = persona || 'general'; const tabBtnAuto = document.getElementById('tab-noc-btn-automations'); if (tabBtnAuto) tabBtnAuto.click(); // Sincronizar botones de persona document.querySelectorAll('.noc-persona-btn').forEach(btn => { const p = btn.getAttribute('data-persona'); if (p === studioActivePersona) { btn.click(); } }); showToast(`✉️ Email Studio configurado para: ${email} (${persona})`); } async function toggleSubscriberStatus(email) { const subs = getSubscribersList(); const sub = subs.find(s => s.email === email); if (sub) { sub.active = !sub.active; try { await fetch('/api/subscribers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) }); } catch (e) {} renderNocSubscribersTab(); renderNocTopKpis(); showToast(`Suscriptor ${email} ${sub.active ? 'activado' : 'pausado'}`); } } async function deleteSubscriberByEmail(email) { if (!confirm(`¿Eliminar de forma permanente al suscriptor ${email}?`)) return; try { const res = await fetch(`/api/subscribers?email=${encodeURIComponent(email)}`, { method: 'DELETE' }); if (res.ok) { currentSubscribersCache = currentSubscribersCache.filter(s => s.email !== email); localStorage.setItem('terremoto_subscribers', JSON.stringify(currentSubscribersCache)); renderNocSubscribersTab(); renderNocTopKpis(); showToast('✅ Suscriptor eliminado del padrón.'); } } catch (e) { showToast('Error al eliminar suscriptor.'); } } async function promptAddNewSubscriber() { const email = prompt('Ingrese correo electrónico del nuevo suscriptor:'); if (!email || !email.includes('@')) { if (email) alert('Correo electrónico inválido.'); return; } const personaChoice = prompt('Seleccione perfil (1 = Público General, 2 = Científico, 3 = Periodista):', '1'); let persona = 'general'; if (personaChoice === '2') persona = 'scientist'; else if (personaChoice === '3') persona = 'journalist'; const isGlobalConfirm = confirm('¿Desea que reciba Alertas Globales (todo el planeta)? Cancelar para geocerca local.'); try { const res = await fetch('/api/subscribers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim().toLowerCase(), persona: persona, isGlobal: isGlobalConfirm, minMag: isGlobalConfirm ? 6.0 : 4.5, centerLat: -31.5375, centerLon: -68.5364, radiusKm: 300, active: true }) }); if (res.ok) { await loadSubscribersFromApi(); renderNocSubscribersTab(); renderNocTopKpis(); showToast('✅ Suscriptor registrado con éxito en el servidor.'); } } catch (e) { showToast('Error al sincronizar con el servidor.'); } } function exportSubscribersCSV() { const subs = getSubscribersList(); const headers = ['Email', 'Perfil_Destinatario', 'Fecha_Registro', 'Tipo_Alerta', 'Radio_km', 'Magnitud_Minima', 'Estado']; const rows = subs.map(s => [ `"${s.email}"`, `"${s.persona || 'general'}"`, `"${(s.createdAt || s.date || '2026-09-17').substring(0, 10)}"`, `"${s.isGlobal ? 'Global' : 'Geocerca Local'}"`, s.isGlobal ? 'Planetario' : (s.radiusKm || 250), s.minMag || 4.5, s.active ? 'ACTIVO' : 'PAUSADO' ]); const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\r\n'); downloadBlob(csv, 'text/csv;charset=utf-8;', `sismosfera_suscriptores_${Date.now()}.csv`); showToast('📥 Directorio de suscriptores exportado en CSV.'); } // ==================================================================== // PESTAÑA 3: EMAIL ALERT STUDIO (MULTI-PERSONA & CSS INLINE) // ==================================================================== let studioActivePersona = 'general'; // 'general' | 'scientist' | 'journalist' let studioViewMode = 'desktop'; // 'desktop' | 'mobile' | 'code' let currentGeneratedEmailHtml = ''; function renderNocAutomationsTab() { const selEvent = document.getElementById('noc-mail-event-select'); if (!selEvent) return; const currentVal = selEvent.value; selEvent.innerHTML = ''; if (!earthquakesData || earthquakesData.length === 0) { selEvent.innerHTML = ''; return; } // Ordenar por magnitud descendente para dar los más relevantes primero const sorted = [...earthquakesData].sort((a, b) => b.magnitude - a.magnitude).slice(0, 20); sorted.forEach(eq => { const opt = document.createElement('option'); opt.value = eq.id; opt.innerText = `M ${eq.magnitude.toFixed(1)} - ${sanitizeSeismicText(eq.place)} (${eq.validatingAgencies.join('/')})`; selEvent.appendChild(opt); }); if (currentVal && sorted.some(e => e.id === currentVal)) { selEvent.value = currentVal; } else { selEvent.value = sorted[0].id; } updateNocMailPreview(); } function updateNocMailPreview() { const previewCard = document.getElementById('noc-mail-preview-card'); const codeContainer = document.getElementById('noc-mail-code-container'); const rawHtmlTextarea = document.getElementById('noc-mail-raw-html'); const selEvent = document.getElementById('noc-mail-event-select'); const wrapper = document.getElementById('noc-mail-viewport-wrapper'); const previewTitle = document.getElementById('noc-mail-preview-title'); const badgePersona = document.getElementById('noc-mail-preview-persona-badge'); const featBox = document.getElementById('noc-mail-persona-features'); if (!previewCard || !selEvent) return; const eqId = selEvent.value; const eq = earthquakesData.find(e => e.id === eqId) || earthquakesData[0]; if (!eq) { previewCard.innerHTML = '
Seleccione un sismo para previsualizar la alerta.
'; return; } const isMobile = studioViewMode === 'mobile'; // Actualizar información de características de la plantilla if (featBox) { if (studioActivePersona === 'general') { featBox.innerHTML = ` 🛡️ Características de la Plantilla Ciudadana: `; if (badgePersona) { badgePersona.innerText = 'PÚBLICO GENERAL'; badgePersona.className = 'px-2 py-0.5 rounded text-[9px] font-mono bg-emerald-500/20 text-emerald-300'; } } else if (studioActivePersona === 'scientist') { featBox.innerHTML = ` 🔬 Características del Boletín Científico: `; if (badgePersona) { badgePersona.innerText = 'CIENTÍFICO / SISMÓLOGO'; badgePersona.className = 'px-2 py-0.5 rounded text-[9px] font-mono bg-sky-500/20 text-sky-300'; } } else { featBox.innerHTML = ` 📰 Características del Cable de Prensa: `; if (badgePersona) { badgePersona.innerText = 'PERIODISTA / MEDIOS'; badgePersona.className = 'px-2 py-0.5 rounded text-[9px] font-mono bg-amber-500/20 text-amber-300'; } } } // Generar HTML según la persona activa if (studioActivePersona === 'general') { currentGeneratedEmailHtml = generateGeneralEmailTemplate(eq, isMobile); } else if (studioActivePersona === 'scientist') { currentGeneratedEmailHtml = generateScientistEmailTemplate(eq, isMobile); } else { currentGeneratedEmailHtml = generateJournalistEmailTemplate(eq, isMobile); } // Aplicar ancho y visualización según el modo seleccionado if (studioViewMode === 'code') { if (wrapper) wrapper.style.maxWidth = '100%'; if (codeContainer) codeContainer.classList.remove('hidden'); if (previewCard) previewCard.classList.add('hidden'); if (rawHtmlTextarea) rawHtmlTextarea.value = currentGeneratedEmailHtml; if (previewTitle) previewTitle.innerText = 'Código HTML Inline Generado (Email Responsive)'; } else if (studioViewMode === 'mobile') { if (wrapper) wrapper.style.maxWidth = '380px'; if (codeContainer) codeContainer.classList.add('hidden'); if (previewCard) { previewCard.classList.remove('hidden'); previewCard.innerHTML = currentGeneratedEmailHtml; } if (previewTitle) previewTitle.innerText = 'Vista Previa Móvil (380px)'; } else { if (wrapper) wrapper.style.maxWidth = '600px'; if (codeContainer) codeContainer.classList.add('hidden'); if (previewCard) { previewCard.classList.remove('hidden'); previewCard.innerHTML = currentGeneratedEmailHtml; } if (previewTitle) previewTitle.innerText = 'Vista Previa Desktop (600px)'; } } // ------------------------------------------------------------- // PLANTILLA 1: PÚBLICO GENERAL / CIUDADANO // ------------------------------------------------------------- function generateGeneralEmailTemplate(eq, isMobile) { const magColor = eq.magnitude >= 6.0 ? '#dc2626' : (eq.magnitude >= 5.0 ? '#d97706' : '#0284c7'); const localTime = formatDateTimeLocal(eq.time); const distToUser = Math.round(haversineDistance(userTelemetry.lat, userTelemetry.lon, eq.latitude, eq.longitude)); const feltYesUrl = `https://sismosfera.com/?felt=1&eq_id=${encodeURIComponent(eq.id)}&mag=${eq.magnitude.toFixed(1)}`; const feltNoUrl = `https://sismosfera.com/?felt=0&eq_id=${encodeURIComponent(eq.id)}`; // Enlaces de Difusión Comunitaria Multicanal const shareText = `🚨 ALERTA SÍSMICA SISMOSFERA: M ${eq.magnitude.toFixed(1)} - ${eq.place} (Profundidad: ${eq.depth.toFixed(1)} km). Monitoreo en vivo:`; const shareUrl = `https://sismosfera.com/?event=${encodeURIComponent(eq.id)}&utm_source=email_alert&utm_medium=share_btn`; const waUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(shareText + ' ' + shareUrl)}`; const tgUrl = `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareText)}`; const twUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`; return ` Alerta Sísmica Sismosfera
🌐 SISMOSFERA
ALERTA DE PROTECCIÓN CIVIL
🏠 CIUDADANO
M ${eq.magnitude.toFixed(1)}

${sanitizeSeismicText(eq.place)}

Consenso oficial: ${eq.validatingAgencies.join(' • ')}
Hora Local: ${localTime}
Profundidad: ${eq.depth.toFixed(1)} km
Distancia a tu zona: ${distToUser} km
🛡️ Recomendaciones Clave de Autoprotección:
  • Agáchate, Cúbrete y Sujétate bajo un escritorio o mesa resistente.
  • Corta el gas y desconecta la electricidad para prevenir incendios.
  • No utilices ascensores; desciende únicamente por escaleras.
  • Aléjate de ventanas, ventanales de vidrio y estantes pesados.
¿Sentiste este temblor en tu zona?

Tu reporte inmediato retroalimenta el mapa de intensidad ciudadana.

👍 Sí, se sintió 👎 No se sintió
📢 DIFUSIÓN COMUNITARIA DE EMERGENCIA
Comparte esta alerta con tus familiares y amigos
Difundir la información en los primeros minutos ayuda a prevenir y mantener la calma.
✈️ Telegram ✖️ X (Twitter)
⚠️ AVISO LEGAL: Sismosfera es un servicio informativo bajo régimen AS IS. No sustituye a los organismos de protección civil. En emergencias, acata prioritariamente a las autoridades locales. Términos de Uso.
Sismosfera • San Juan, Argentina • Las Islas Malvinas son Argentinas
`; } // ------------------------------------------------------------- // PLANTILLA 2: CIENTÍFICO / SISMÓLOGO // ------------------------------------------------------------- function generateScientistEmailTemplate(eq, isMobile) { const utcTime = eq.time.toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; const moJoules = eq.energyJoules || Math.pow(10, 1.5 * eq.magnitude + 4.8); const moNm = (Math.pow(10, 1.5 * eq.magnitude + 9.1)).toExponential(3); const pArrival = Math.round(haversineDistance(userTelemetry.lat, userTelemetry.lon, eq.latitude, eq.longitude) / 7.8); const sArrival = Math.round(haversineDistance(userTelemetry.lat, userTelemetry.lon, eq.latitude, eq.longitude) / 4.4); return ` Boletín Sismológico Técnico Sismosfera
SISMOSFERA // SEISMIC RESEARCH TELEMETRY
FDSN / USGS / EMSC MULTI-AGENCY CONVERGENCE
🔬 CIENTÍFICO
Identificador Oficial de Evento
${eq.id} • ${sanitizeSeismicText(eq.place)}
Magnitud Momento (Mw): M ${eq.magnitude.toFixed(2)}
Momento Sísmico Escalar (Mo): ${moNm} N•m
Energía Radiada Sísmica: ${Math.round(moJoules).toExponential(2)} J
Hipocentro / Profundidad: ${eq.latitude.toFixed(4)}°, ${eq.longitude.toFixed(4)}° • ${eq.depth.toFixed(1)} km
Tiempo de Origen UTC: ${utcTime}
Tiempos Teóricos ak135 (P / S): P: +${pArrival}s • S: +${sArrival}s
📡 Telemetría de Estación FDSN Más Cercana:
Red: IU / TRQA • Canal: BHZ (Broadband Vertical)
Servicio de Waveform: EarthScope DataSelect / IRIS WS Timeseries
Abrir Beachball, Sismograma & Curvas ak135 ➔
⚠️ AVISO CIENTÍFICO & LEGAL: Datos y modelos sismológicos teóricos bajo régimen AS IS. No constituyen predicciones deterministas. Términos Legales.
Servidor Sismológico Sismosfera • Protocolo QuakeML / FDSN-WS • San Juan, Argentina
`; } // ------------------------------------------------------------- // PLANTILLA 3: PERIODISTA / MEDIOS DE COMUNICACIÓN // ------------------------------------------------------------- function generateJournalistEmailTemplate(eq, isMobile) { const localTime = formatDateTimeLocal(eq.time); const magStr = eq.magnitude.toFixed(1); const isMajor = eq.magnitude >= 6.0; return ` Cable de Prensa Sismosfera
[CABLE DE PRENSA URGENTE // BREAKING NEWS]
REDACCIÓN SISMOSFERA
📰 PRENSA

${isMajor ? 'URGENTE: Potente' : 'ALERTA: Registran'} sismo de magnitud ${magStr} en la región de ${sanitizeSeismicText(eq.place)}

SAN JUAN, ARGENTINA • ${localTime} • DESPACHO INMEDIATO PARA MEDIOS
Un sismo de magnitud ${magStr} se registró a las ${localTime} en las inmediaciones de ${sanitizeSeismicText(eq.place)}, a una profundidad calculada de ${eq.depth.toFixed(1)} kilómetros, de acuerdo al reporte consolidado en tiempo real emitido por el centro de monitoreo de Sismosfera con validación de agencias sismológicas internacionales (${eq.validatingAgencies.join(', ')}).
"El evento telúrico fue captado de manera automática por las estaciones de la red FDSN. El epicentro se localizó en las coordenadas ${eq.latitude.toFixed(2)}°, ${eq.longitude.toFixed(2)}°. Se realiza el seguimiento activo de posibles réplicas en las próximas horas."
— Vocería Oficial del Centro de Operaciones NOC, Sismosfera
Magnitud Oficial: M ${magStr}
Epicentro: ${sanitizeSeismicText(eq.place)}
Profundidad: ${eq.depth.toFixed(1)} km
Amenaza de Tsunami: ${eq.isTsunamiPotential ? '⚠️ Posible (Bajo Evaluación)' : '🛡️ Descartada'}
⚠️ AVISO PARA MEDIOS: Datos preliminares sujetos a revisión científica oficial de las agencias. Régimen AS IS. Términos de Uso.
Servicio de Noticias Sísmicas Sismosfera • Libre reproducción citando la fuente oficial
`; } function copyEmailStudioHtml() { if (!currentGeneratedEmailHtml) { showToast('⚠️ No hay código HTML generado para copiar'); return; } navigator.clipboard.writeText(currentGeneratedEmailHtml).then(() => { showToast('📋 Código HTML inline copiado con éxito al portapapeles'); }).catch(() => { const ta = document.getElementById('noc-mail-raw-html'); if (ta) { ta.select(); document.execCommand('copy'); showToast('📋 Código HTML inline copiado'); } }); } async function simulateMailDispatch() { try { const activeQuake = (window.earthquakesData && window.earthquakesData.length > 0) ? window.earthquakesData[0] : { magnitude: 6.2, depth: 25.0, place: 'San Juan, Argentina', title: 'M 6.2 - San Juan, Argentina' }; const resp = await fetch('/api/email-automation/test-dispatch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ magnitude: activeQuake.magnitude || 6.2, depth: activeQuake.depth || 25, place: activeQuake.place || 'Región Epicentral', title: activeQuake.title || `M ${(activeQuake.magnitude || 6.2).toFixed(1)} - ${activeQuake.place || 'Región Epicentral'}`, persona: studioActivePersona }) }); const data = await resp.json(); if (data && data.success) { showToast(`✉️ Alerta emitida desde [email protected] a ${data.dispatch ? data.dispatch.recipientsCount : 0} suscriptores.`); } else { showToast(`⚠️ Error al emitir alerta: ${(data && data.error) || 'Fallo de transporte'}`); } } catch (err) { showToast(`✉️ Alerta procesada desde [email protected] (${studioActivePersona.toUpperCase()}).`); } } // ==================================================================== // PESTAÑA 4: RED Y SALUD DE AGENCIAS SÍSMICAS (QoS & INGESTION TELEMETRY) // ==================================================================== let nocServerTelemetry = null; async function fetchServerHealthTelemetry() { try { const res = await fetch('/api/health'); if (res.ok) { const data = await res.json(); nocServerTelemetry = data; updateServerHealthCard(data); if (data && Array.isArray(data.services)) { data.services.forEach(srv => { let key = null; if (srv.name.includes('USGS')) key = 'USGS'; else if (srv.name.includes('EMSC')) key = 'EMSC'; else if (srv.name.includes('EarthScope')) key = 'EarthScope'; else if (srv.name.includes('NOAA')) key = 'NOAA'; else if (srv.name.includes('GeoNet')) key = 'GeoNet'; else if (srv.name.includes('INGV')) key = 'INGV'; else if (srv.name.includes('GFZ')) key = 'GFZ'; if (key && agencyStatus[key]) { if (!agencyStatus[key].latencyMs || (Date.now() - (agencyStatus[key].lastSyncTimestamp || 0) > 30000)) { agencyStatus[key].latencyMs = srv.latencyMs; agencyStatus[key].status = srv.status === 'operational' ? 'online' : (srv.status === 'degraded' ? 'degraded' : 'offline'); agencyStatus[key].lastSyncTimestamp = Date.now(); } } }); } } } catch (err) { console.warn('[NOC] Telemetría /api/health no disponible temporalmente:', err.message); } } function updateServerHealthCard(data) { if (!data) return; const badge = document.getElementById('noc-server-status-badge'); if (badge) { if (data.status === 'healthy') { badge.className = 'px-2 py-0.5 rounded text-[9px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'; badge.innerText = 'OPERACIONAL'; } else if (data.status === 'degraded') { badge.className = 'px-2 py-0.5 rounded text-[9px] font-mono font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30'; badge.innerText = 'DEGRADADO'; } else { badge.className = 'px-2 py-0.5 rounded text-[9px] font-mono font-bold bg-rose-500/20 text-rose-300 border border-rose-500/30'; badge.innerText = 'CRÍTICO'; } } const upEl = document.getElementById('noc-server-uptime'); if (upEl && typeof data.uptimeSeconds === 'number') { const h = Math.floor(data.uptimeSeconds / 3600); const m = Math.floor((data.uptimeSeconds % 3600) / 60); const s = data.uptimeSeconds % 60; upEl.innerText = `${h}h ${m}m ${s}s`; } const memEl = document.getElementById('noc-server-mem'); if (memEl && data.memoryMb) { memEl.innerText = `${data.memoryMb.rss} MB`; } const cacheEl = document.getElementById('noc-server-cache'); if (cacheEl && typeof data.cacheItems === 'number') { cacheEl.innerText = `${data.cacheItems} ítems`; } const avgEl = document.getElementById('noc-server-avg-rtt'); if (avgEl && Array.isArray(data.services) && data.services.length > 0) { const valid = data.services.filter(s => typeof s.latencyMs === 'number' && s.latencyMs > 0); const avg = valid.length > 0 ? Math.round(valid.reduce((acc, v) => acc + v.latencyMs, 0) / valid.length) : null; avgEl.innerText = avg ? `${avg} ms` : 'S/D'; } const probeEl = document.getElementById('noc-server-last-probe'); if (probeEl) { const now = new Date(); probeEl.innerText = `Último sondeo: ${now.toLocaleTimeString()}`; } } function renderNocAgenciesTab() { const tbody = document.getElementById('noc-agencies-tbody'); if (!tbody) return; tbody.innerHTML = ''; fetchServerHealthTelemetry(); const agencyCatalog = [ { key: 'USGS', name: 'USGS Earthquake Hazards', region: 'Global & EE.UU.', protocol: 'HTTP Stream Diferencial (10s)', url: 'earthquake.usgs.gov' }, { key: 'EMSC', name: 'EMSC SeismicPortal', region: 'Europa & Mediterráneo', protocol: 'WebSocket Directo (< 2s)', url: 'seismicportal.eu' }, { key: 'EarthScope', name: 'EarthScope / IRIS', region: 'Red Global de Estaciones FDSN', protocol: 'FDSNWS Station & Waveforms', url: 'service.earthscope.org' }, { key: 'NOAA', name: 'NOAA NDBC DART', region: 'Boyas de Tsunamis del Pacífico', protocol: 'Realtime Coastal Telemetry', url: 'ndbc.noaa.gov' }, { key: 'GeoNet', name: 'GeoNet New Zealand', region: 'Pacífico Sur & Oceanía', protocol: 'REST GeoJSON FDSN (25s)', url: 'geonet.org.nz' }, { key: 'INGV', name: 'INGV Roma', region: 'Italia & Mediterráneo Central', protocol: 'REST GeoJSON FDSN (25s)', url: 'ingv.it' }, { key: 'GFZ', name: 'GFZ GEOFON Potsdam', region: 'Alemania & Monitoreo Global', protocol: 'GEOFON FDSN Text (25s)', url: 'geofon.gfz.de' }, { key: 'JMA', name: 'JMA / P2PQuake', region: 'Japón & Cinturón de Fuego', protocol: 'P2P WebSocket/REST (25s)', url: 'p2pquake.net' } ]; // Contar sismos aportados reales del catálogo en memoria const agencyEventCounts = {}; if (Array.isArray(earthquakesData)) { earthquakesData.forEach(eq => { const prov = (eq.provider || eq.agency || '').toUpperCase(); if (prov) { agencyEventCounts[prov] = (agencyEventCounts[prov] || 0) + 1; } if (Array.isArray(eq.sources)) { eq.sources.forEach(s => { const srcUpper = String(s).toUpperCase(); agencyEventCounts[srcUpper] = (agencyEventCounts[srcUpper] || 0) + 1; }); } }); } agencyCatalog.forEach(a => { const stat = agencyStatus[a.key] || { status: 'pending', count: 0, latencyMs: null, lastSyncTimestamp: null }; const realCount = agencyEventCounts[a.key.toUpperCase()] || stat.count || 0; let lastSyncStr = 'S/D'; if (stat.lastSyncTimestamp) { const sec = Math.max(1, Math.floor((Date.now() - stat.lastSyncTimestamp) / 1000)); if (sec < 60) lastSyncStr = `Hace ${sec}s`; else if (sec < 3600) lastSyncStr = `Hace ${Math.floor(sec / 60)}m`; else lastSyncStr = `Hace ${Math.floor(sec / 3600)}h`; } else if (realCount > 0) { lastSyncStr = 'Sincronizado'; } let latencyBadge = 'S/D'; if (stat.latencyMs !== null && typeof stat.latencyMs === 'number') { let colorCls = 'text-emerald-400'; let dotCls = 'bg-emerald-400'; if (stat.latencyMs > 800) { colorCls = 'text-rose-400'; dotCls = 'bg-rose-400'; } else if (stat.latencyMs > 350) { colorCls = 'text-amber-400'; dotCls = 'bg-amber-400'; } latencyBadge = `${stat.latencyMs} ms`; } else if (stat.status === 'offline') { latencyBadge = 'TIMEOUT'; } let statusBadge = 'PENDIENTE'; if (stat.status === 'online' || realCount > 0 || (stat.latencyMs && stat.latencyMs > 0)) { statusBadge = 'ONLINE'; } else if (stat.status === 'offline') { statusBadge = 'OFFLINE'; } const tr = document.createElement('tr'); tr.className = 'hover:bg-slate-800/40 transition-colors'; tr.innerHTML = ` ${a.name} ${a.region} ${a.protocol} ${latencyBadge} ${lastSyncStr} ${realCount} ${statusBadge} `; tbody.appendChild(tr); }); const statUniques = document.getElementById('noc-consensus-stat-uniques'); if (statUniques) statUniques.innerText = `${earthquakesData.length} sismos consolidados sin duplicados`; } async function measureAgenciesLatency() { if (typeof showToast === 'function') { showToast('⚡ Midiendo latencias RTT en tiempo real hacia las 8 agencias...'); } const btn = document.getElementById('btn-noc-ping-agencies'); if (btn) { btn.innerText = 'Probando...'; btn.disabled = true; } // 1. Sondas del servidor await fetchServerHealthTelemetry(); // 2. Sondas del cliente en paralelo const endpoints = [ { key: 'USGS', url: 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson' }, { key: 'EMSC', url: 'https://www.seismicportal.eu/fdsnws/event/1/query?format=json&limit=2' }, { key: 'GeoNet', url: 'https://api.geonet.org.nz/quake?MMI=1' }, { key: 'INGV', url: 'https://webservices.ingv.it/fdsnws/event/1/query?format=geojson&limit=2' }, { key: 'GFZ', url: 'https://geofon.gfz.de/fdsnws/event/1/query?format=text&limit=2' }, { key: 'JMA', url: 'https://api.p2pquake.net/v2/history?codes=551&limit=2' }, { key: 'EarthScope', url: 'https://service.earthscope.org/fdsnws/station/1/query?level=network&format=text&limit=1' }, { key: 'NOAA', url: 'https://www.ndbc.noaa.gov/data/realtime2/46404.dart' } ]; await Promise.allSettled(endpoints.map(async (ep) => { const t0 = performance.now(); try { await fetch(ep.url, { method: 'GET', mode: 'no-cors', cache: 'no-store', signal: AbortSignal.timeout(4000) }); const rtt = Math.round(performance.now() - t0); if (agencyStatus[ep.key]) { agencyStatus[ep.key].latencyMs = rtt; agencyStatus[ep.key].status = 'online'; agencyStatus[ep.key].lastSyncTimestamp = Date.now(); } } catch (e) { if (agencyStatus[ep.key] && !agencyStatus[ep.key].latencyMs) { agencyStatus[ep.key].status = 'offline'; } } })); if (btn) { btn.innerHTML = '⚡Probar Latencias Ahora'; btn.disabled = false; } renderNocAgenciesTab(); renderNocTopKpis(); const validLats = Object.values(agencyStatus).filter(a => typeof a.latencyMs === 'number' && a.latencyMs > 0).map(a => a.latencyMs); const avg = validLats.length > 0 ? Math.round(validLats.reduce((a, b) => a + b, 0) / validLats.length) : null; if (typeof showToast === 'function') { showToast(avg ? `✅ Latencias medidas con éxito (Promedio: ${avg} ms)` : '✅ Telemetría de agencias actualizada'); } } // ==================================================================== // PESTAÑA 5: HERRAMIENTAS DE CRECIMIENTO Y GESTIÓN WEB // ==================================================================== function renderNocGrowthTab() { updateNocWidgetPreview(); checkPushStatus(); } function checkPushStatus() { const label = document.getElementById('noc-push-status-label'); if (!label) return; if (!('Notification' in window)) { label.innerText = 'El navegador no soporta Notificaciones Push.'; } else if (Notification.permission === 'granted') { label.innerText = '🟢 Permiso concedido. Alertas Push activadas.'; } else if (Notification.permission === 'denied') { label.innerText = '🔴 Permiso denegado por el usuario en el navegador.'; } else { label.innerText = '⚪ Permiso pendiente de solicitud.'; } } function requestPushPermission() { if (!('Notification' in window)) { alert('Este navegador no soporta notificaciones push.'); return; } Notification.requestPermission().then(permission => { checkPushStatus(); if (permission === 'granted') { showToast('🔔 Notificaciones Web Push activadas para sismos críticos'); new Notification('🚨 Sismosfera NOC • Alertas Críticas', { body: '[USGS + EMSC Verificado] Notificaciones Web Push activas con monitoreo sísmico multi-red en tiempo real.', icon: 'earth-satellite-4k.png' }); } }); } function updateNocWidgetPreview() { const mode = document.getElementById('noc-widget-mode')?.value || 'globe'; const theme = document.getElementById('noc-widget-theme')?.value || 'dark'; const iframeCode = `\n
Monitoreo sísmico en tiempo real por Sismosfera • TerremotosHoy
`; const preview = document.getElementById('noc-widget-code-preview'); if (preview) preview.value = iframeCode; } function copyNocWidgetCode() { const preview = document.getElementById('noc-widget-code-preview'); if (!preview) return; navigator.clipboard.writeText(preview.value).then(() => { showToast('📋 Código del Widget copiado al portapapeles'); }).catch(() => { preview.select(); document.execCommand('copy'); showToast('📋 Código copiado al portapapeles'); }); } // ==================================================================== // PESTAÑA 6: WAR-ROOM DE CRISIS SÍSMICA // ==================================================================== function renderNocCrisisTab() { const detailsContainer = document.getElementById('noc-crisis-event-details'); if (!detailsContainer) return; detailsContainer.innerHTML = ''; const mostPowerful = getMostPowerfulRecentEvent(earthquakesData) || earthquakesData[0]; if (!mostPowerful) { detailsContainer.innerHTML = '
Sin sismos activos en el sistema.
'; return; } const distToUser = Math.round(haversineDistance(userTelemetry.lat, userTelemetry.lon, mostPowerful.latitude, mostPowerful.longitude)); const etaSecondsS = Math.round(distToUser / 4.5); detailsContainer.innerHTML = `
M ${mostPowerful.magnitude.toFixed(1)} ${sanitizeSeismicText(mostPowerful.place)}
Profundidad: ${mostPowerful.depth.toFixed(1)} km
Distancia a ti: ${distToUser} km
Consenso: ${mostPowerful.consensusScore} agencia(s)
Energía: ${Math.round(mostPowerful.energyJoules).toLocaleString()} J
`; const etaUserEl = document.getElementById('noc-crisis-eta-user'); if (etaUserEl) { if (etaSecondsS < 60) { etaUserEl.innerText = `${etaSecondsS} s (Onda S)`; } else { etaUserEl.innerText = `${Math.round(etaSecondsS / 60)} min ${etaSecondsS % 60} s`; } } const tsunamiEl = document.getElementById('noc-crisis-tsunami-status'); if (tsunamiEl) { if (mostPowerful.isTsunamiPotential || mostPowerful.tsunami === 1) { tsunamiEl.innerHTML = `
🌊 POTENCIAL DE TSUNAMI ACTIVO

Epicentro marino con magnitud >= 6.8 o alerta oficial declarada.

`; } else { tsunamiEl.innerHTML = `
🛡️ SIN AMENAZA DE TSUNAMI

Sismo de baja profundidad o en corteza continental sin empuje vertical de columna de agua.

`; } } } function toggleNocAlarm() { if (nocAlarmActive) { stopNocAlarm(); } else { startNocAlarm(); } } function startNocAlarm() { initAudioContext(); if (!audioCtx) return; try { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(650, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(950, audioCtx.currentTime + 0.4); gain.gain.setValueAtTime(0.2, audioCtx.currentTime); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); let high = true; const interval = setInterval(() => { if (!nocAlarmActive) { clearInterval(interval); return; } if (high) { osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.35); } else { osc.frequency.exponentialRampToValueAtTime(950, audioCtx.currentTime + 0.35); } high = !high; }, 400); nocAlarmOscillator = osc; nocAlarmGain = gain; nocAlarmActive = true; const btnText = document.getElementById('btn-noc-alarm-text'); if (btnText) btnText.innerText = 'Detener Sirena de Emergencia'; showToast('📢 Sirena acústica NOC activada (Modo War-Room)'); } catch (e) { console.warn('Error al activar sirena:', e); } } function stopNocAlarm() { if (nocAlarmOscillator) { try { nocAlarmOscillator.stop(); nocAlarmOscillator.disconnect(); } catch (e) {} nocAlarmOscillator = null; } nocAlarmActive = false; const btnText = document.getElementById('btn-noc-alarm-text'); if (btnText) btnText.innerText = 'Simular Sirena de Alerta NOC'; } function downloadBlob(content, mimeType, filename) { const blob = new Blob([content], { type: mimeType }); 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); } // ==================================================================== // PESTAÑA 7: EMAIL MARKETING & CAMPAÑAS DE INVITACIÓN (FASE 53.0) // ==================================================================== let nocMktActiveTemplate = 'institucional'; let nocMktViewMode = 'desktop'; let nocMktCurrentHtml = ''; const NOC_MKT_TEMPLATES_INFO = { institucional: { badge: '🏛️ INSTITUCIONES & PROTECCIÓN CIVIL', badgeColor: '#10b981', badgeBg: 'rgba(16, 185, 129, 0.15)', defaultSubject: '🏢 Alerta Temprana & Monitoreo Sísmico 24/7 para Instituciones y Protección Civil', heroTitle: 'Tecnología Sismológica de Misión Crítica para Organismos de Emergencia', heroSubtitle: 'Fortalezca la capacidad de respuesta, análisis de aceleración de suelo y protección de vidas en su jurisdicción con datos oficiales de grado instrumental.', leadText: 'Ponemos a disposición de su organismo el acceso prioritario al Centro Global de Monitoreo de Sismosfera, la infraestructura de consenso sísmico y alerta temprana más moderna de la región.', benefits: [ { icon: '🚨', title: 'Alertas Tempranas Inmediatas en < 15 Segundos', desc: 'Motor de consenso multi-agencia (USGS, EMSC, GeoNet, CSN) que consolida sismos reales eliminando falsas alarmas y saturación de red.' }, { icon: '📊', title: 'Aceleración Máxima de Suelo (PGA) & ShakeMap MMI', desc: 'Cálculo cinemático en tiempo real con modelos GMM NGA-West2 para dimensionar sacudidas superficiales y radio destructivo.' }, { icon: '🏗️', title: 'Estimación Oficial de Impacto Edilicio PAGER', desc: 'Semáforo de vulnerabilidad estructural y población expuesta para movilización táctica de defensas civiles y bomberos.' }, { icon: '📡', title: 'Sala de Situación y Monitoreo Multi-Pantalla', desc: 'Tablero operativo NOC con soporte para videowalls, visualización de sismogramas y exportación de reportes forenses.' } ], ctaText: '🏛️ Explorar el Centro Táctico Institucional en Sismosfera →', highlightText: 'Infraestructura 100% libre de datos simulados o aproximados. Acceso sin costo para municipios, cuerpos de bomberos y entidades de defensa civil.' }, cientifico: { badge: '🔬 ACADÉMICO & GEOFÍSICA', badgeColor: '#38bdf8', badgeBg: 'rgba(56, 189, 248, 0.15)', defaultSubject: '🔬 Plataforma Geocientífica de Telemetría Tectónica, Tensores de Momento y Modelado 3D', heroTitle: 'Sismología Instrumental, Geodinámica de Placas y Fórmulas Físicas Reales', heroSubtitle: 'Un entorno analítico riguroso diseñado por y para investigadores de las ciencias de la Tierra, geólogos, sismólogos y universidades.', leadText: 'Le invitamos a conocer Sismosfera, una plataforma concebida bajo el principio de Veracidad Absoluta: cero datos sintéticos, modelado analítico riguroso y telemetría de observatorios internacionales.', benefits: [ { icon: '🌋', title: 'Límites de Placas Tectónicas (Peter Bird 2003)', desc: 'Estratificación hiper-dramática de 76 trazos de fallas, subducción, dorsales de expansión y tasas geodésicas en mm/año.' }, { icon: '⚡', title: 'Tensores de Momento Sísmico M0 & Mecanismos Focales', desc: 'Cálculo de energía radiada (Hanks & Kanamori 1979), Joules a TNT y visualizador de tensores en 3D.' }, { icon: '⏱️', title: 'Tiempos de Viaje de Ondas ak135 (Fases P y S)', desc: 'Curvas teóricas de propagación tele-sísmica a través de la corteza y manto para estimación exacta de tiempos de arribo.' }, { icon: '📉', title: 'Leyes Omori-Utsu & Gutenberg-Richter Aki-Utsu', desc: 'Modelado estadístico de decaimiento de réplicas cosísmicas y parámetro b-value para evaluación de estrés cortical.' } ], ctaText: '🔬 Acceder al Entorno Analítico y Geofísico →', highlightText: 'Conexión directa con EarthScope FDSN, ISC y catálogos de terremotos históricos instrumentales desde 1900 a la fecha.' }, prensa: { badge: '📰 MEDIOS DE COMUNICACIÓN & PRENSA', badgeColor: '#fbbf24', badgeBg: 'rgba(251, 191, 36, 0.15)', defaultSubject: '📰 Cobertura Sísmica Inmediata: Cables de Teletipo 5W y Mapas 3D Embebibles para Redacciones', heroTitle: 'Información Sísmica en Segundos: Primicias Verificadas sin Errores para su Redacción', heroSubtitle: 'Brinde a sus lectores cobertura instantánea, rigurosa y visualmente impactante ante cualquier terremoto en el país o el mundo.', leadText: 'En el periodismo de último minuto, cada segundo cuenta. Sismosfera proporciona a canales de noticias, portales web y radios herramientas automatizadas para informar con total certeza científica.', benefits: [ { icon: '⚡', title: 'Cables de Teletipo Automáticos (Formato 5W)', desc: 'Texto redactado al instante con Qué, Cuándo, Dónde, Magnitud y Profundidad listo para copiar y emitir al aire o publicar en web.' }, { icon: '🗺️', title: 'Widgets Embebibles 3D para Noticias Digitales', desc: 'Incruste el mapa del sismo interactivo directamente en sus artículos periodísticos en menos de 10 segundos.' }, { icon: '🗣️', title: 'Módulo de Percepción Humana en Tiempo Real', desc: 'Datos de la escala Mercalli y reportes ciudadanos verificados para saber de inmediato qué ciudades sintieron la sacudida.' }, { icon: '📚', title: 'Contexto Histórico y Tectónico Inmediato', desc: 'Píldoras informativas sobre sismos históricos en la misma región para enriquecer sus notas con antecedentes fiables.' } ], ctaText: '📰 Probar Herramientas para Redacciones y Medios →', highlightText: 'Evite bulos y desinformación en redes sociales con datos certificados en tiempo real de agencias sismológicas oficiales.' }, comunidad: { badge: '🌍 PÚBLICO GENERAL & CIUDADANÍA', badgeColor: '#a855f7', badgeBg: 'rgba(168, 85, 247, 0.15)', defaultSubject: '🌍 Conoce Sismosfera: El Monitor Sísmico y Globo 3D en Vivo Más Avanzado del Planeta', heroTitle: 'Descubre los Terremotos del Mundo en Tiempo Real en un Globo 3D Interactivo', heroSubtitle: 'La forma más intuitiva, visual y educativa de seguir la actividad de la Tierra y cuidar a quienes más quieres.', leadText: 'Te invitamos a descubrir Sismosfera, la plataforma pública y abierta donde puedes explorar el planeta en 3D, ver los sismos a medida que ocurren y ser parte activa de la ciencia ciudadana.', benefits: [ { icon: '🌐', title: 'Globo 3D Orbital Fluido e Interactivo', desc: 'Explora placas tectónicas incandescentes, volcanes activos y epicentros con un nivel de realismo nunca antes visto.' }, { icon: '📱', title: 'Alertas Tempranas Gratuitas a tu Correo y WhatsApp', desc: 'Entérate antes que nadie de sismos cercanos a tu ciudad o a la región donde viven tus familiares y amigos.' }, { icon: '🙋', title: 'Ciencia Ciudadana: ¿Lo sentiste?', desc: 'Suma tu reporte en vivo con solo 1 clic cuando ocurra un temblor para ayudar a calcular la intensidad real en tu barrio.' }, { icon: '🛡️', title: 'Guías de Autoprotección y Prevención', desc: 'Consejos prácticos de seguridad civil para saber exactamente qué hacer antes, durante y después de un movimiento sísmico.' } ], ctaText: '🌍 Entrar al Globo 3D en Vivo en Sismosfera.com →', highlightText: '100% gratuito, accesible desde cualquier celular, tablet o computadora, sin necesidad de instalar aplicaciones.' } }; function parseMarketingEmails(rawText) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const tokens = String(rawText || '').split(/[\r\n,;]+/).map(s => s.trim().toLowerCase()).filter(Boolean); const valid = []; const invalid = []; const seen = new Set(); let duplicates = 0; tokens.forEach(tok => { if (emailRegex.test(tok)) { if (seen.has(tok)) { duplicates++; } else { seen.add(tok); valid.push(tok); } } else { invalid.push(tok); } }); return { valid, invalid, duplicates, total: tokens.length }; } function updateMarketingRecipientsStats() { const textarea = document.getElementById('noc-mkt-recipients-textarea'); if (!textarea) return; const stats = parseMarketingEmails(textarea.value); const statValid = document.getElementById('noc-mkt-stat-valid'); if (statValid) statValid.innerText = `✅ ${stats.valid.length} válidos`; const statInvalid = document.getElementById('noc-mkt-stat-invalid'); if (statInvalid) statInvalid.innerText = `❌ ${stats.invalid.length} inválidos`; const statDup = document.getElementById('noc-mkt-stat-dup'); if (statDup) statDup.innerText = `♻️ ${stats.duplicates} duplicados`; const sendLabel = document.getElementById('btn-noc-mkt-send-label'); if (sendLabel) { sendLabel.innerText = `Emitir Campaña de Marketing (${stats.valid.length} Destinatarios)`; } } function buildMarketingHtmlForPreview(templateKey, customSubject) { const tpl = NOC_MKT_TEMPLATES_INFO[templateKey] || NOC_MKT_TEMPLATES_INFO.institucional; const subject = customSubject || tpl.defaultSubject; const ctaUrl = `https://sismosfera.com/?utm_source=email_marketing&utm_campaign=invitacion_${templateKey}`; const waUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent('Te invito a conocer Sismosfera: ' + ctaUrl)}`; const tgUrl = `https://t.me/share/url?url=${encodeURIComponent(ctaUrl)}&text=${encodeURIComponent('Sismosfera 3D')}`; const twUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent('Monitor sísmico en tiempo real: ' + ctaUrl)}`; const benefitsRows = tpl.benefits.map((b, idx) => `
${b.icon}
${b.title}
${b.desc}
${idx < tpl.benefits.length - 1 ? '' : ''} `).join(''); return ` ${subject}
🌐 SISMOSFERA.COM
CENTRO GLOBAL DE MONITOREO SÍSMICO • 24/7
${tpl.badge}

${tpl.heroTitle}

${tpl.heroSubtitle}

${tpl.leadText}
⚡ CAPACIDADES Y SERVICIOS DESTACADOS
${benefitsRows}
🛡️ COMPROMISO DE VERACIDAD ABSOLUTA
${tpl.highlightText}
${tpl.ctaText}
Acceso instantáneo sin descargas • Optimizado para móviles y computadoras.
📢 COMPARTE ESTA INVITACIÓN
¿Conoces a colegas o amigos interesados en monitoreo sísmico?
Sismosfera NOC • Centro de Monitoreo Sísmico 24/7 • San Juan, República Argentina
🇦🇷 Las Islas Malvinas son Argentinas
Has recibido este correo informativo de sismosfera.com.
`; } function renderMarketingPreview() { const subjectInput = document.getElementById('noc-mkt-subject-input'); const customSub = subjectInput ? subjectInput.value.trim() : ''; nocMktCurrentHtml = buildMarketingHtmlForPreview(nocMktActiveTemplate, customSub); const previewCard = document.getElementById('noc-mkt-preview-card'); const rawTextarea = document.getElementById('noc-mkt-raw-html'); const wrapper = document.getElementById('noc-mkt-viewport-wrapper'); const title = document.getElementById('noc-mkt-preview-title'); const codeContainer = document.getElementById('noc-mkt-code-container'); if (previewCard) previewCard.innerHTML = nocMktCurrentHtml; if (rawTextarea) rawTextarea.value = nocMktCurrentHtml; if (wrapper) { if (nocMktViewMode === 'mobile') { wrapper.style.maxWidth = '380px'; if (title) title.innerText = 'Vista Previa Móvil (380px)'; } else { wrapper.style.maxWidth = '600px'; if (title) title.innerText = 'Vista Previa Desktop (600px)'; } } if (codeContainer && previewCard) { if (nocMktViewMode === 'code') { codeContainer.classList.remove('hidden'); previewCard.classList.add('hidden'); } else { codeContainer.classList.add('hidden'); previewCard.classList.remove('hidden'); } } } let mktListenersInitialized = false; function setupMarketingEventListeners() { if (mktListenersInitialized) return; mktListenersInitialized = true; // 1. Selector de Plantillas const tplBtns = document.querySelectorAll('.noc-mkt-tpl-btn'); tplBtns.forEach(btn => { btn.addEventListener('click', () => { tplBtns.forEach(b => { b.className = 'noc-mkt-tpl-btn p-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white transition-all cursor-pointer flex flex-col gap-1 hover:border-slate-700'; }); btn.className = 'noc-mkt-tpl-btn active p-2.5 rounded-xl bg-slate-950 border border-emerald-500/60 text-white transition-all cursor-pointer flex flex-col gap-1'; const tplKey = btn.getAttribute('data-template') || 'institucional'; nocMktActiveTemplate = tplKey; const info = NOC_MKT_TEMPLATES_INFO[tplKey]; const subInput = document.getElementById('noc-mkt-subject-input'); if (subInput && info) { subInput.value = info.defaultSubject; } renderMarketingPreview(); }); }); // 2. Input de Asunto const subjectInput = document.getElementById('noc-mkt-subject-input'); if (subjectInput) { const info = NOC_MKT_TEMPLATES_INFO[nocMktActiveTemplate]; if (info && !subjectInput.value) subjectInput.value = info.defaultSubject; subjectInput.addEventListener('input', () => { renderMarketingPreview(); }); } // 3. Textarea de Destinatarios const textarea = document.getElementById('noc-mkt-recipients-textarea'); if (textarea) { textarea.addEventListener('input', () => { updateMarketingRecipientsStats(); }); } // 4. Botón Importar Suscriptores const btnImport = document.getElementById('btn-noc-mkt-import-subs'); if (btnImport) { btnImport.addEventListener('click', async () => { try { showToast('⏳ Cargando padrón de suscriptores...'); const res = await fetch('/api/subscribers'); if (res.ok) { const data = await res.json(); if (data && Array.isArray(data.subscribers) && data.subscribers.length > 0) { const emails = data.subscribers.map(s => s.email).filter(Boolean); const currentText = textarea ? textarea.value.trim() : ''; const combined = currentText ? (currentText + '\n' + emails.join('\n')) : emails.join('\n'); if (textarea) { textarea.value = combined; updateMarketingRecipientsStats(); } showToast(`✅ ${emails.length} suscriptores importados a la lista`); } else { showToast('ℹ️ No hay suscriptores registrados en el sistema'); } } } catch (e) { showToast('❌ Error al importar suscriptores'); } }); } // 5. Botón Limpiar const btnClear = document.getElementById('btn-noc-mkt-clear-recipients'); if (btnClear && textarea) { btnClear.addEventListener('click', () => { textarea.value = ''; updateMarketingRecipientsStats(); showToast('🧹 Lista de destinatarios vaciada'); }); } // 6. Botones de Modo de Vista const btnDesk = document.getElementById('btn-mkt-view-desktop'); const btnMob = document.getElementById('btn-mkt-view-mobile'); const btnCode = document.getElementById('btn-mkt-view-code'); const allModeBtns = [btnDesk, btnMob, btnCode]; function setMode(mode, activeBtn) { nocMktViewMode = mode; allModeBtns.forEach(b => { if (b) b.className = 'mkt-view-btn px-2 py-0.5 rounded text-slate-400 hover:text-white font-bold transition-all cursor-pointer'; }); if (activeBtn) activeBtn.className = 'mkt-view-btn active px-2 py-0.5 rounded bg-sky-600 text-white font-bold transition-all cursor-pointer'; renderMarketingPreview(); } if (btnDesk) btnDesk.addEventListener('click', () => setMode('desktop', btnDesk)); if (btnMob) btnMob.addEventListener('click', () => setMode('mobile', btnMob)); if (btnCode) btnCode.addEventListener('click', () => setMode('code', btnCode)); // 7. Botón Copiar HTML const btnCopy = document.getElementById('btn-noc-mkt-copy-html'); if (btnCopy) { btnCopy.addEventListener('click', () => { if (navigator.clipboard && nocMktCurrentHtml) { navigator.clipboard.writeText(nocMktCurrentHtml).then(() => { showToast('📋 Código HTML de marketing copiado'); }); } }); } // 8. Botón Enviar Campaña const btnSend = document.getElementById('btn-noc-mkt-send-campaign'); if (btnSend) { btnSend.addEventListener('click', async () => { const textarea = document.getElementById('noc-mkt-recipients-textarea'); const rawText = textarea ? textarea.value.trim() : ''; const stats = parseMarketingEmails(rawText); if (stats.valid.length === 0) { alert('Por favor ingresa al menos una dirección de correo válida para emitir la campaña.'); return; } const info = NOC_MKT_TEMPLATES_INFO[nocMktActiveTemplate]; const subjectInput = document.getElementById('noc-mkt-subject-input'); const finalSubject = subjectInput ? subjectInput.value.trim() : (info ? info.defaultSubject : ''); const confirmMsg = `¿Confirmas el envío de la campaña '${info ? info.badge : nocMktActiveTemplate}' a ${stats.valid.length} destinatarios?\n\nAsunto: ${finalSubject}`; if (!confirm(confirmMsg)) return; // Mostrar monitor de progreso const progCont = document.getElementById('noc-mkt-progress-container'); const progStatus = document.getElementById('noc-mkt-progress-status'); const progPct = document.getElementById('noc-mkt-progress-pct'); const progBar = document.getElementById('noc-mkt-progress-bar'); const statusLog = document.getElementById('noc-mkt-status-log'); if (progCont) progCont.classList.remove('hidden'); if (progStatus) progStatus.innerText = 'Despachando campaña...'; if (progPct) progPct.innerText = '0%'; if (progBar) progBar.style.width = '0%'; if (statusLog) statusLog.innerHTML = '
🚀 Iniciando despacho por lotes hacia backend...
'; btnSend.disabled = true; btnSend.classList.add('opacity-50', 'cursor-not-allowed'); try { const res = await fetch('/api/marketing/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template: nocMktActiveTemplate, customSubject: finalSubject, recipients: stats.valid }) }); const data = await res.json(); if (res.ok && data.success) { if (progStatus) progStatus.innerText = '✅ Campaña completada exitosamente'; if (progPct) progPct.innerText = '100%'; if (progBar) progBar.style.width = '100%'; const camp = data.campaign || {}; if (statusLog) { let logHtml = `
🏁 Campaña finalizada: ${camp.delivered || 0} enviados, ${camp.errors || 0} fallidos de ${camp.total || 0} totales.
`; if (Array.isArray(camp.results)) { camp.results.slice(0, 10).forEach(r => { const ok = r.status === 'delivered'; logHtml += `
${ok ? '✔️' : '❌'} ${r.email} (${r.transport || r.error || 'ok'})
`; }); if (camp.results.length > 10) { logHtml += `
...y ${camp.results.length - 10} más registrados en log
`; } } statusLog.innerHTML = logHtml; } showToast(`🚀 Campaña emitida: ${camp.delivered || 0} correos enviados`); } else { if (progStatus) progStatus.innerText = '❌ Error en despacho'; if (statusLog) { statusLog.innerHTML += `
Error: ${data.error || 'Fallo desconocido'}
`; } showToast('❌ Error al emitir campaña: ' + (data.error || 'desconocido')); } } catch (e) { if (progStatus) progStatus.innerText = '❌ Error de conexión'; if (statusLog) { statusLog.innerHTML += `
Error de red: ${e.message}
`; } showToast('❌ Error de conexión al emitir campaña'); } finally { btnSend.disabled = false; btnSend.classList.remove('opacity-50', 'cursor-not-allowed'); } }); } } function renderNocMarketingTab() { setupMarketingEventListeners(); const subjectInput = document.getElementById('noc-mkt-subject-input'); const info = NOC_MKT_TEMPLATES_INFO[nocMktActiveTemplate]; if (subjectInput && info && !subjectInput.value) { subjectInput.value = info.defaultSubject; } updateMarketingRecipientsStats(); renderMarketingPreview(); } function renderNocTab(tabKey) { if (tabKey === 'analytics') renderNocAnalyticsTab(); else if (tabKey === 'marketing') renderNocMarketingTab(); else if (tabKey === 'subscribers') renderNocSubscribersTab(); else if (tabKey === 'automations') renderNocAutomationsTab(); else if (tabKey === 'agencies') renderNocAgenciesTab(); else if (tabKey === 'growth') renderNocGrowthTab(); else if (tabKey === 'crisis') renderNocCrisisTab(); } // ==================================================================== // SEO PROGRAMÁTICO & HUBS GEOGRÁFICOS POR PAÍS (FASE 72.0) // ==================================================================== function detectAndApplyCountryFocus() { const ssrFilter = window.__INITIAL_COUNTRY_FILTER__; let slug = window.__INITIAL_COUNTRY__ || (ssrFilter && (ssrFilter.slug || ssrFilter.name)) || ''; if (!slug) { const params = new URLSearchParams(window.location.search); slug = params.get('pais') || params.get('country') || params.get('region'); } if (!slug && window.location.pathname.startsWith('/sismos/')) { slug = window.location.pathname.replace(/^\/sismos\//, '').replace(/\/$/, '').toLowerCase(); } if (!slug && !ssrFilter) return; slug = String(slug || '').toLowerCase().trim(); const countryHubsCoords = { 'chile': { name: 'Chile', lat: -33.4489, lng: -70.6693, altitude: 1.45 }, 'mexico': { name: 'México', lat: 19.4326, lng: -99.1332, altitude: 1.5 }, 'argentina': { name: 'Argentina', lat: -34.6037, lng: -64.3816, altitude: 1.55 }, 'peru': { name: 'Perú', lat: -12.0464, lng: -77.0428, altitude: 1.45 }, 'japon': { name: 'Japón', lat: 36.2048, lng: 138.2529, altitude: 1.4 }, 'estados-unidos': { name: 'Estados Unidos', lat: 36.7783, lng: -119.4179, altitude: 1.5 }, 'california': { name: 'California', lat: 35.1258, lng: -119.1456, altitude: 1.25 }, 'colombia': { name: 'Colombia', lat: 4.5709, lng: -74.2973, altitude: 1.45 }, 'ecuador': { name: 'Ecuador', lat: -1.8312, lng: -78.1834, altitude: 1.35 }, 'espana': { name: 'España', lat: 40.4637, lng: -3.7492, altitude: 1.4 }, 'italia': { name: 'Italia', lat: 41.8719, lng: 12.5674, altitude: 1.35 }, 'turquia': { name: 'Turquía', lat: 38.9637, lng: 35.2433, altitude: 1.4 }, 'indonesia': { name: 'Indonesia', lat: -0.7893, lng: 113.9213, altitude: 1.5 }, 'taiwan': { name: 'Taiwán', lat: 23.6978, lng: 120.9605, altitude: 1.3 }, 'guatemala': { name: 'Guatemala', lat: 15.7835, lng: -90.2308, altitude: 1.35 }, 'costa-rica': { name: 'Costa Rica', lat: 9.7489, lng: -83.7534, altitude: 1.3 }, 'panama': { name: 'Panamá', lat: 8.5380, lng: -80.7821, altitude: 1.3 }, 'el-salvador': { name: 'El Salvador', lat: 13.7942, lng: -88.8965, altitude: 1.3 }, 'nueva-zelanda': { name: 'Nueva Zelanda', lat: -40.9006, lng: 174.8860, altitude: 1.45 }, 'filipinas': { name: 'Filipinas', lat: 12.8797, lng: 121.7740, altitude: 1.4 }, 'grecia': { name: 'Grecia', lat: 39.0742, lng: 21.8243, altitude: 1.35 } }; const baseSlug = slug.includes('/') ? slug.split('/')[0] : slug; let info = countryHubsCoords[slug] || countryHubsCoords[baseSlug] || null; if (ssrFilter && ssrFilter.name) { const cLat = Array.isArray(ssrFilter.center) ? Number(ssrFilter.center[0]) : (ssrFilter.center && ssrFilter.center.lat !== undefined ? Number(ssrFilter.center.lat) : (info ? info.lat : 0)); const cLng = Array.isArray(ssrFilter.center) ? Number(ssrFilter.center[1]) : (ssrFilter.center && ssrFilter.center.lng !== undefined ? Number(ssrFilter.center.lng) : (info ? info.lng : 0)); info = { name: ssrFilter.regionName ? (ssrFilter.regionName + ', ' + ssrFilter.name) : ssrFilter.name, filterQuery: ssrFilter.regionName || ssrFilter.name, lat: cLat, lng: cLng, altitude: ssrFilter.isRegionHub ? 1.15 : (info ? info.altitude : 1.45) }; } if (!info) return; // 0. Fase 198.9: Activar filtro regional real en la lista (#seismic-feed-list) para que /sismos/:pais muestre los sismos de ese país const slugToCode = { 'chile': 'cl', 'mexico': 'mx', 'argentina': 'ar', 'peru': 'pe', 'japon': 'jp', 'estados-unidos': 'us', 'usa': 'us', 'california': 'ca', 'colombia': 'co', 'ecuador': 'ec', 'espana': 'es', 'italia': 'it', 'turquia': 'tr', 'indonesia': 'id', 'taiwan': 'tw', 'guatemala': 'gt', 'el-salvador': 'gt', 'panama': 'gt', 'costa-rica': 'cr', 'nueva-zelanda': 'nz', 'filipinas': 'ph', 'grecia': 'gr' }; const mappedCode = slugToCode[baseSlug] || slugToCode[slug] || ''; if (mappedCode && REGIONAL_INTELLIGENCE_COUNTRIES[mappedCode]) { activeCountryFilter = mappedCode; } else if (info.filterQuery || info.name) { window.activeGeoSearchQuery = info.filterQuery || info.name; } if (typeof renderFeedList === 'function') renderFeedList(); setTimeout(() => { if (typeof renderFeedList === 'function') renderFeedList(); }, 600); // 1. Centrado suave de la cámara orbital const checkGlobeAndCenter = (retries = 0) => { if (typeof myGlobe !== 'undefined' && myGlobe && typeof myGlobe.pointOfView === 'function') { myGlobe.pointOfView({ lat: info.lat, lng: info.lng, altitude: info.altitude }, 1800); } else if (retries < 15) { setTimeout(() => checkGlobeAndCenter(retries + 1), 300); } }; setTimeout(() => checkGlobeAndCenter(0), 400); // 2. Banner informativo superior Glassmorphic const existingBanner = document.getElementById('country-focus-banner'); if (existingBanner) existingBanner.remove(); const banner = document.createElement('div'); banner.id = 'country-focus-banner'; banner.className = 'fixed top-[102px] md:top-14 left-1/2 -translate-x-1/2 z-40 px-3.5 py-1.5 rounded-full bg-slate-950/90 border border-sky-500/50 shadow-2xl backdrop-blur-md flex items-center gap-2.5 text-xs text-white pointer-events-auto transition-all max-w-[calc(100vw-20px)] whitespace-nowrap'; banner.innerHTML = ` 📍 Región: ${info.name} | 📊 Hub ↗ | `; document.body.appendChild(banner); const btnReset = document.getElementById('btn-reset-country-focus'); if (btnReset) { btnReset.onclick = () => { banner.remove(); activeCountryFilter = 'all'; window.activeGeoSearchQuery = ''; window.__INITIAL_COUNTRY__ = null; window.__INITIAL_COUNTRY_FILTER__ = null; if (typeof renderFeedList === 'function') renderFeedList(); if (typeof window.updateMobileHumanPulseBar === 'function') window.updateMobileHumanPulseBar(); if (typeof myGlobe !== 'undefined' && myGlobe && typeof myGlobe.pointOfView === 'function') { myGlobe.pointOfView({ lat: 15.0, lng: 0.0, altitude: 2.5 }, 1500); } if (window.history && window.history.pushState) { window.history.pushState(null, '', '/'); } }; } } // ==================================================================== // EVENT LISTENERS // ==================================================================== window.addEventListener('DOMContentLoaded', () => { // Manejador de deep-linking de retroalimentación '¿Lo sentiste?' try { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('felt') === '1' || urlParams.get('felt') === 'true') { const eqId = String(urlParams.get('event') || urlParams.get('eq_id') || urlParams.get('eq') || urlParams.get('id') || '').trim(); const attemptOpenFelt = (retries = 0) => { if (eqId && Array.isArray(earthquakesData) && earthquakesData.length > 0) { const found = earthquakesData.find(e => e.id === eqId || String(e.id).toLowerCase() === eqId.toLowerCase()); if (found) { selectedEvent = found; if (typeof openEventDetails === 'function') openEventDetails(found); openFeltItModal(); updateFeltLocationDisplay(); return; } } if (retries < 5) { setTimeout(() => attemptOpenFelt(retries + 1), 600); } else { openFeltItModal(); } }; setTimeout(() => attemptOpenFelt(0), 500); } } catch (e) {} initGlobalToolEventListeners(); if (typeof hydrateFromInitialEarthquakes === 'function') { hydrateFromInitialEarthquakes(); } initGlobe(); if (typeof myGlobe !== 'undefined' && myGlobe) { try { if (earthquakesData.length > 0) { refreshGlobeLayers(); } const deepLinkHandled = typeof initDeepLinkRouter === 'function' && initDeepLinkRouter(); if (!deepLinkHandled && earthquakesData.length > 0) { syncGlobeToMostRecentEvent(true); } } catch (eSync) {} } detectAndApplyCountryFocus(); // ==================================================================== // GCO (GOOGLE CONVERSION OPTIMIZATION) & CONSENT MANAGER ENGINE (FASE 60.0) // ==================================================================== window.trackGCOEvent = function(eventName, eventParams = {}) { try { if (typeof window.gtag === 'function') { window.gtag('event', eventName, { event_category: eventParams.category || 'conversion', event_label: eventParams.label || '', value: eventParams.value || 1, send_to: 'G-Y2RB580V4F', ...eventParams }); } } catch (err) { console.warn('[GCO Analytics] Error despachando evento:', err); } }; function initConsentManager() { const banner = document.getElementById('cookie-consent-banner'); const customOptions = document.getElementById('consent-custom-options'); const btnAcceptAll = document.getElementById('btn-consent-accept-all'); const btnReject = document.getElementById('btn-consent-reject'); const btnCloseX = document.getElementById('btn-consent-close-x'); const btnToggleCustom = document.getElementById('btn-consent-toggle-custom'); const chkAnalytics = document.getElementById('consent-check-analytics'); const chkMarketing = document.getElementById('consent-check-marketing'); window.dismissTop3MobilePod = function() { const top3 = document.getElementById('aside-top3-ranking'); if (top3) { top3.classList.add('hidden'); top3.style.setProperty('display', 'none', 'important'); } }; if (!banner) return; const saved = localStorage.getItem('cookie_consent_v2') || localStorage.getItem('sismosfera_cookie_consent'); if (!saved) { setTimeout(() => { const stillUnsaved = !localStorage.getItem('cookie_consent_v2') && !localStorage.getItem('sismosfera_cookie_consent'); if (stillUnsaved) { banner.style.removeProperty('display'); banner.classList.remove('hidden'); document.body.classList.add('cookie-banner-open'); } }, 1200); } else { banner.classList.add('hidden'); banner.style.setProperty('display', 'none', 'important'); document.body.classList.remove('cookie-banner-open'); try { const parsed = JSON.parse(saved); applyConsent(parsed.analytics !== false, parsed.marketing !== false, false); if (chkAnalytics) chkAnalytics.checked = parsed.analytics !== false; if (chkMarketing) chkMarketing.checked = parsed.marketing !== false; } catch(e) {} } function applyConsent(analyticsGranted, marketingGranted, showToastAndEvent = true) { if (typeof window.gtag === 'function') { window.gtag('consent', 'update', { 'analytics_storage': analyticsGranted ? 'granted' : 'denied', 'ad_storage': marketingGranted ? 'granted' : 'denied', 'ad_user_data': marketingGranted ? 'granted' : 'denied', 'ad_personalization': marketingGranted ? 'granted' : 'denied', 'personalization_storage': marketingGranted ? 'granted' : 'denied' }); window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'consent_updated', consent_analytics: analyticsGranted, consent_marketing: marketingGranted }); if (analyticsGranted && showToastAndEvent) { window.gtag('event', 'page_view', { page_title: document.title, page_location: window.location.href, send_to: 'G-Y2RB580V4F' }); } } const consentData = { analytics: analyticsGranted, marketing: marketingGranted, timestamp: new Date().toISOString() }; try { localStorage.setItem('cookie_consent_v2', JSON.stringify(consentData)); localStorage.setItem('sismosfera_cookie_consent', JSON.stringify(consentData)); } catch (e) {} banner.classList.add('hidden'); banner.style.setProperty('display', 'none', 'important'); document.body.classList.remove('cookie-banner-open'); if (analyticsGranted && showToastAndEvent) { if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('consent_granted', { category: 'privacy', label: 'all' }); } if (typeof showToast === 'function') { showToast('✅ Preferencias de cookies guardadas'); } } } window.acceptAllCookies = () => applyConsent(true, true, true); window.rejectOptionalCookies = () => applyConsent(false, false, true); window.dismissCookieBanner = (acceptAll = false) => applyConsent(Boolean(acceptAll), Boolean(acceptAll), false); window.toggleCustomCookies = () => { if (customOptions) customOptions.classList.toggle('hidden'); }; if (btnAcceptAll) { btnAcceptAll.onclick = () => applyConsent(true, true, true); } if (btnReject) { btnReject.onclick = () => applyConsent(false, false, true); } if (btnCloseX) { btnCloseX.onclick = () => applyConsent(false, false, false); } const btnSaveCustom = document.getElementById('btn-consent-save-custom'); if (btnSaveCustom) { btnSaveCustom.onclick = () => { const a = chkAnalytics ? chkAnalytics.checked : true; const m = chkMarketing ? chkMarketing.checked : false; applyConsent(a, m, true); }; } if (btnToggleCustom) { btnToggleCustom.onclick = () => { if (customOptions) customOptions.classList.toggle('hidden'); }; } window.openConsentManagerSettings = function() { if (banner) { if (customOptions) customOptions.classList.remove('hidden'); banner.style.removeProperty('display'); banner.classList.remove('hidden'); document.body.classList.add('cookie-banner-open'); } }; } initSidebarControls(); initCountryFilter(); initConsentManager(); initNocDashboard(); loadMultiAgencyData(); // Inicializar estado de botones rápidos destacados (Fase 50.0) const initChkPlates = document.getElementById('chk-plates'); if (initChkPlates) updatePlatesQuickButton(initChkPlates.checked); const initChkHeatmap = document.getElementById('chk-heatmap'); if (initChkHeatmap) updateHeatmapQuickButton(initChkHeatmap.checked); // Pipeline de arranque progresivo: diferir catálogos complementarios (FDSN y DART) para pintar sismos al instante setTimeout(() => { loadDartStations(); loadSeismicStations(); }, 1200); // Selector de Idioma Global const selLang = document.getElementById('select-language'); if (selLang) { selLang.value = currentLanguage; selLang.onchange = (e) => setLanguage(e.target.value); } setLanguage(currentLanguage); // Botones de Filtro por Magnitud & Umbral de Percepción (Sidebar en Vivo) // Botón Abrir Matriz Comparativa (24h/48h/72h) const btnOpenComp = document.getElementById('btn-open-comp-stats'); if (btnOpenComp) btnOpenComp.onclick = openComparativeStatsModal; // Botón Abrir Suite Benioff & Enjambres (Sidebar & Top Navbar) const btnOpenBenioff = document.getElementById('btn-open-benioff'); if (btnOpenBenioff) btnOpenBenioff.onclick = openBenioffModal; const btnOpenBenioffTop = document.getElementById('btn-open-benioff-top'); if (btnOpenBenioffTop) btnOpenBenioffTop.onclick = openBenioffModal; const btnCloseBenioff = document.getElementById('btn-close-benioff'); if (btnCloseBenioff) btnCloseBenioff.onclick = closeBenioffModal; const btnCloseBenioff2 = document.getElementById('btn-close-benioff-2'); if (btnCloseBenioff2) btnCloseBenioff2.onclick = closeBenioffModal; const barQuickComp = document.getElementById('feed-quick-comp-bar'); if (barQuickComp) barQuickComp.onclick = openComparativeStatsModal; const btnCloseComp = document.getElementById('btn-close-comparative-stats'); if (btnCloseComp) btnCloseComp.onclick = closeComparativeStatsModal; // Selector de modo de comparativa (Discreto 24h vs Acumulado) const btnCompDaily = document.getElementById('btn-comp-mode-daily'); if (btnCompDaily) { btnCompDaily.onclick = () => { comparativeViewMode = 'daily'; btnCompDaily.className = 'comp-mode-btn active px-3 py-1 rounded-lg bg-sky-600 text-white transition-all cursor-pointer'; const btnCumul = document.getElementById('btn-comp-mode-cumul'); if (btnCumul) btnCumul.className = 'comp-mode-btn px-3 py-1 rounded-lg text-slate-400 hover:text-white transition-all cursor-pointer'; renderComparativeStats(); }; } const btnCompCumul = document.getElementById('btn-comp-mode-cumul'); if (btnCompCumul) { btnCompCumul.onclick = () => { comparativeViewMode = 'cumulative'; btnCompCumul.className = 'comp-mode-btn active px-3 py-1 rounded-lg bg-sky-600 text-white transition-all cursor-pointer'; const btnDaily = document.getElementById('btn-comp-mode-daily'); if (btnDaily) btnDaily.className = 'comp-mode-btn px-3 py-1 rounded-lg text-slate-400 hover:text-white transition-all cursor-pointer'; renderComparativeStats(); }; } // Botón Alternar Ancho del Sidebar const btnToggleWidth = document.getElementById('btn-sidebar-toggle-width'); if (btnToggleWidth) { btnToggleWidth.onclick = () => { const sidebar = document.getElementById('left-sidebar'); if (sidebar) { const isWide = sidebar.classList.contains('sm:w-[440px]') || sidebar.classList.contains('w-[440px]'); if (!isWide) { sidebar.classList.remove('sm:w-80', 'sm:w-96', '2xl:w-96'); sidebar.classList.add('sm:w-[440px]'); showToast('📐 Ancho de columna ampliado'); } else { sidebar.classList.remove('sm:w-[440px]', 'w-[440px]'); sidebar.classList.add('sm:w-80', '2xl:w-96'); showToast('📐 Ancho de columna normal'); } if (typeof window.updateLayoutOffsets === 'function') { setTimeout(() => { window.updateLayoutOffsets(!sidebar.classList.contains('opacity-0')); }, 50); } } }; } // Cargar estadísticas comparativas iniciales setTimeout(loadComparativeStats, 1500); // Botones de Filtro por Magnitud & Umbral de Percepción (Sincronizado con Autopiloto) document.querySelectorAll('#live-filter-buttons .btn-live-filter').forEach(btn => { btn.onclick = () => { document.querySelectorAll('#live-filter-buttons .btn-live-filter').forEach(b => { b.classList.remove('active', 'bg-sky-500/20', 'text-sky-300', 'border-sky-500/40', 'bg-amber-500/20', 'text-amber-300', 'border-amber-500/40', 'bg-emerald-500/20', 'text-emerald-300', 'border-emerald-500/40', 'bg-rose-500/30', 'text-rose-100', 'border-rose-400/70', 'font-bold', 'shadow-sm'); if (b.getAttribute('data-filter') !== 'video3d') { b.classList.add('text-slate-400'); } }); const f = btn.getAttribute('data-filter') || 'all'; btn.classList.remove('text-slate-400'); btn.classList.add('active', 'font-bold', 'shadow-sm'); if (f === 'perceptible') { btn.classList.add('bg-amber-500/20', 'text-amber-300', 'border-amber-500/40'); } else if (f === 'quiet') { btn.classList.add('bg-emerald-500/20', 'text-emerald-300', 'border-emerald-500/40'); } else if (f === 'video3d') { btn.classList.add('bg-rose-500/30', 'text-rose-100', 'border-rose-400/70'); } else { btn.classList.add('bg-sky-500/20', 'text-sky-300', 'border-sky-500/40'); } liveFeedFilter = f; renderFeedList(); // Sincronizar inmediatamente para comenzar con los eventos filtrados if (syncEngineActive) { syncCurrentIndex = 0; stepGlobeSync(true); } }; }); // Switch de Sincronización del Globo con el Listado const chkAutoCenter = document.getElementById('chk-auto-center'); if (chkAutoCenter) { chkAutoCenter.onchange = (e) => { setSyncEngineState(e.target.checked); }; } // Botón Centrar Más Reciente const btnFocusLatest = document.getElementById('btn-focus-latest-eq'); if (btnFocusLatest) { btnFocusLatest.onclick = () => { syncGlobeToMostRecentEvent(true); const feedContainer = document.getElementById('seismic-feed-list'); if (feedContainer) feedContainer.scrollTo({ top: 0, behavior: 'smooth' }); showToast('🎯 Centrado en el sismo más reciente registrado'); }; } // Botones Mi Ubicación document.getElementById('btn-my-location').onclick = () => locateUser('header'); document.getElementById('btn-modal-locate').onclick = () => locateUser('modal'); // Botón Autopiloto document.getElementById('btn-autopilot').onclick = toggleAutopilot; // Lógica de Toggling y Cierre de Menús Tácticos Desplegables (Fase 10 / Fase 49.2) window.toggleTacticalMenu = function(menuId) { const science = document.getElementById('dropdown-menu-science'); const layers = document.getElementById('dropdown-menu-layers'); const lm = document.getElementById('layer-menu'); const zc = document.getElementById('globe-zoom-slider-container'); if (menuId === 'menu-science') { if (layers) layers.classList.add('hidden'); if (lm && !lm.classList.contains('hidden')) { lm.classList.add('hidden'); if (zc) zc.classList.remove('docked-beside-layer-menu'); } const willOpen = science ? science.classList.contains('hidden') : false; if (science) science.classList.toggle('hidden'); if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('science_suite_toggle', { category: 'engagement', label: 'science_menu', action: willOpen ? 'open' : 'close' }); } } else if (menuId === 'menu-layers') { window.toggleLayerTacticalMenu(); } }; window.toggleLayerTacticalMenu = function() { const lm = document.getElementById('layer-menu'); const zc = document.getElementById('globe-zoom-slider-container'); const science = document.getElementById('dropdown-menu-science'); const dropdownLayers = document.getElementById('dropdown-menu-layers'); if (science) science.classList.add('hidden'); if (dropdownLayers) dropdownLayers.classList.add('hidden'); if (lm) { const isHidden = lm.classList.contains('hidden'); lm.classList.toggle('hidden'); if (zc) { if (isHidden) { zc.classList.add('docked-beside-layer-menu'); } else { zc.classList.remove('docked-beside-layer-menu'); } } } }; window.focusGlobeCenter = function() { if (myGlobe && typeof myGlobe.pointOfView === 'function') { myGlobe.pointOfView({ lat: -31.5375, lng: -68.5364, altitude: 2.0 }, 1000); } }; window.toggleMobileSidebar = function() { const sidebar = document.getElementById('left-sidebar'); if (!sidebar) return; const isCurrentlyHidden = sidebar.classList.contains('hidden') || sidebar.classList.contains('-translate-x-[calc(100%+32px)]'); if (isCurrentlyHidden) { sidebar.classList.remove('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); } else { sidebar.classList.add('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); } if (typeof updateMobileLayout === 'function') updateMobileLayout(isCurrentlyHidden); }; // Botón Alertas Acústicas / Soundscapes (Web Audio API) const btnSound = document.getElementById('btn-toggle-sound'); if (btnSound) btnSound.onclick = toggleSound; // Función para conmutar pestañas temáticas del Menú de Capas Bento window.switchLayerMenuTab = function(tabKey) { const tabs = ['textures', 'geology', 'oceans', 'sensors']; tabs.forEach(t => { const btn = document.getElementById('btn-tab-layer-' + t); const panel = document.getElementById('layer-panel-' + t); if (t === tabKey) { if (btn) { btn.className = 'py-1.5 px-1 rounded-xl font-bold transition-all text-center cursor-pointer bg-sky-500/20 border border-sky-400/80 text-sky-300 shadow-sm flex flex-col items-center gap-0.5'; } if (panel) panel.classList.remove('hidden'); } else { if (btn) { btn.className = 'py-1.5 px-1 rounded-xl font-bold transition-all text-center cursor-pointer bg-slate-900/60 border border-slate-800/80 text-slate-400 hover:text-white flex flex-col items-center gap-0.5'; } if (panel) panel.classList.add('hidden'); } }); }; // Botón Menú Capas (con Desacople Espacial de Palanca de Zoom - Fase 22.0 / Fase 49.2) const btnToggleLayers = document.getElementById('btn-toggle-layers'); if (btnToggleLayers) { btnToggleLayers.onclick = () => { window.toggleLayerTacticalMenu(); }; } // Selector de Texturas document.querySelectorAll('#texture-selector .btn-tex').forEach(btn => { btn.onclick = () => { document.querySelectorAll('#texture-selector .btn-tex').forEach(b => { const isCol2 = b.classList.contains('col-span-2'); b.className = `btn-tex text-left p-2 rounded-lg bg-slate-900/60 border border-slate-700/60 text-slate-400 hover:text-white ${isCol2 ? 'col-span-2' : ''}`; }); const isCol2 = btn.classList.contains('col-span-2'); btn.className = `btn-tex active text-left p-2 rounded-lg bg-sky-500/20 border border-sky-400/60 font-semibold text-white ${isCol2 ? 'col-span-2 shadow-lg shadow-amber-500/10 border-amber-400/70' : ''}`; const texKey = btn.getAttribute('data-texture'); selectGlobeTexture(texKey); }; }); // Toggles de Capas document.getElementById('chk-borders').onchange = updateVectorLayers; document.getElementById('chk-plates').onchange = () => { const isChecked = document.getElementById('chk-plates').checked; updatePlatesQuickButton(isChecked); updateVectorLayers(); updateGlobeLabels(); }; const chkSmGlobe = document.getElementById('chk-shakemap-globe'); if (chkSmGlobe) chkSmGlobe.onchange = updateVectorLayers; document.getElementById('chk-cities').onchange = updateGlobeLabels; const chkInfra = document.getElementById('chk-infra'); if (chkInfra) chkInfra.onchange = updateGlobeLabels; document.getElementById('chk-atmosphere').onchange = (e) => { if (myGlobe) myGlobe.showAtmosphere(e.target.checked); }; // Botón Ciencia Ciudadana «¡Lo Sentí!» const btnFelt = document.getElementById('btn-felt-it'); if (btnFelt) btnFelt.onclick = openFeltItModal; const btnBottomFelt = document.getElementById('btn-bottom-felt'); if (btnBottomFelt) btnBottomFelt.onclick = openFeltItModal; const btnDesktopFelt = document.getElementById('btn-desktop-felt'); if (btnDesktopFelt) btnDesktopFelt.onclick = openFeltItModal; // Modal «¡Lo Sentí!» const btnCloseFelt = document.getElementById('btn-close-felt-it'); if (btnCloseFelt) { btnCloseFelt.onclick = () => { const modal = document.getElementById('modal-felt-it'); if (modal) modal.classList.add('hidden'); }; } const btnFeltGps = document.getElementById('btn-felt-detect-gps'); if (btnFeltGps) { btnFeltGps.onclick = () => { locateUser('modal'); setTimeout(updateFeltLocationDisplay, 1200); }; } document.querySelectorAll('.btn-felt-opt').forEach(btn => { btn.onclick = () => { const intensity = btn.getAttribute('data-intensity') || 'moderate'; submitCitizenReport(intensity); }; }); // Banner de Alerta Temprana Ciudadana const btnCitizenClose = document.getElementById('btn-citizen-close-banner'); if (btnCitizenClose) { btnCitizenClose.onclick = () => { const banner = document.getElementById('citizen-alert-banner'); if (banner) banner.classList.add('hidden'); }; } const btnCitizen3D = document.getElementById('btn-citizen-view-3d'); if (btnCitizen3D) { btnCitizen3D.onclick = () => { if (activeCitizenAlert && myGlobe) { const controls = myGlobe.controls(); if (controls) controls.autoRotate = false; myGlobe.pointOfView({ lat: activeCitizenAlert.lat, lng: activeCitizenAlert.lng, altitude: 1.1 }, 2000); } }; } // Capa de Tsunamis e Isócronas const chkTsunami = document.getElementById('chk-tsunami'); if (chkTsunami) { chkTsunami.onchange = () => { updateVectorLayers(); refreshGlobeLayers(); }; } const btnPreviewTsunami = document.getElementById('btn-preview-tsunami'); if (btnPreviewTsunami) { btnPreviewTsunami.onclick = () => { tsunamiDemoActive = !tsunamiDemoActive; if (tsunamiDemoActive) { btnPreviewTsunami.innerText = 'Desactivar Demo'; btnPreviewTsunami.className = 'text-amber-400 hover:text-white font-bold underline cursor-pointer'; showToast('🌊 Demo Tsunami Japón 2011 M 9.1 activada con isócronas'); if (myGlobe) { const controls = myGlobe.controls(); if (controls) controls.autoRotate = false; myGlobe.pointOfView({ lat: 38.297, lng: 142.372, altitude: 1.4 }, 2000); } } else { btnPreviewTsunami.innerText = 'Demo Japón 2011'; btnPreviewTsunami.className = 'text-cyan-400 hover:text-white font-bold underline cursor-pointer'; showToast('🌊 Demo Tsunami desactivada'); } updateVectorLayers(); refreshGlobeLayers(); }; } function handleCloseEventDetails() { if (typeof pauseSeismogramPlayback === 'function') pauseSeismogramPlayback(); try { const ytIframe = document.getElementById('detail-youtube-iframe'); if (ytIframe && ytIframe.getAttribute('src') !== 'about:blank') { ytIframe.setAttribute('src', 'about:blank'); } const ytCard = document.getElementById('detail-youtube-embed-card'); if (ytCard) ytCard.classList.add('hidden'); const ytFacade = document.getElementById('detail-youtube-facade'); if (ytFacade) ytFacade.style.display = 'flex'; const multiCard = document.getElementById('detail-multi-agency-card'); if (multiCard) multiCard.classList.add('hidden'); const aftershockCard = document.getElementById('detail-main-vs-aftershock-card'); if (aftershockCard) aftershockCard.classList.add('hidden'); const pgaCard = document.getElementById('detail-attenuation-pga-card'); if (pgaCard) pgaCard.classList.add('hidden'); const histCard = document.getElementById('detail-historical-precedent-card'); if (histCard) histCard.classList.add('hidden'); const focalBentoCard = document.getElementById('detail-focal-mechanism-card'); if (focalBentoCard) focalBentoCard.classList.add('hidden'); } catch (_) {} const modal = document.getElementById('modal-event-details'); if (modal) modal.classList.add('hidden'); if (typeof updateGlobeRenderLifecycle === 'function') updateGlobeRenderLifecycle(); // Limpiar identificadores deep-link en memoria para que no reactiven el modal window.__INITIAL_EVENT_ID__ = null; window.__INITIAL_EVENT_DATA__ = null; // Restauración incondicional de la URL limpia hacia '/' if (window.history && window.history.replaceState) { if (window.location.pathname.startsWith('/sismo/') || window.location.search.includes('eq=') || window.location.search.includes('event=') || window.location.search.includes('id=')) { window.history.replaceState(null, 'Sismosfera 3D — Monitoreo Sísmico Global en Tiempo Real', '/'); } } document.title = 'Sismosfera 3D — Monitoreo Sísmico Global en Tiempo Real'; } window.handleCloseEventDetails = handleCloseEventDetails; // Modal Detalles del Evento document.getElementById('btn-close-details').onclick = handleCloseEventDetails; const btnCloseDetails2 = document.getElementById('btn-close-details-2'); if (btnCloseDetails2) { btnCloseDetails2.onclick = handleCloseEventDetails; } document.getElementById('btn-details-focus').onclick = () => { if (selectedEvent) focusEarthquake(selectedEvent); }; const btnPlayAudio = document.getElementById('btn-play-event-audio'); if (btnPlayAudio) { btnPlayAudio.onclick = () => { if (selectedEvent) { playEarthquakeAcoustic(selectedEvent.magnitude, selectedEvent.depth); const txt = document.getElementById('btn-play-audio-text'); if (txt) { const original = txt.innerText; txt.innerText = 'Reproduciendo Acústica...'; setTimeout(() => { if (txt) txt.innerText = original; }, 1800); } } }; } // Corte Transversal Subterráneo 3D & Planos de Falla (Herramienta 3.12) const btnOpenSlice = document.getElementById('btn-open-slice-view'); if (btnOpenSlice) { btnOpenSlice.onclick = () => { if (selectedEvent) openSubterraneanSlice(selectedEvent); }; } const btnFocalOpenSlice = document.getElementById('btn-focal-open-slice'); if (btnFocalOpenSlice) { btnFocalOpenSlice.onclick = () => { if (selectedEvent) openSubterraneanSlice(selectedEvent); }; } const btnCloseSlice = document.getElementById('btn-close-slice-view'); if (btnCloseSlice) { btnCloseSlice.onclick = closeSubterraneanSlice; } // ==================================================================== // LISTENERS MODAL VEDETTE COMPARTIR (FASE 26.0) // ==================================================================== const btnDetailsShare = document.getElementById('btn-details-share'); if (btnDetailsShare) { btnDetailsShare.onclick = () => { if (selectedEvent) openShareModal(selectedEvent); }; } const btnDetailsHeaderShare = document.getElementById('btn-details-header-share'); if (btnDetailsHeaderShare) { btnDetailsHeaderShare.onclick = () => { if (selectedEvent) openShareModal(selectedEvent); }; } const btnCloseShareModal = document.getElementById('btn-close-share-modal'); if (btnCloseShareModal) btnCloseShareModal.onclick = closeShareModal; const btnCloseShareModalBottom = document.getElementById('btn-close-share-modal-bottom'); if (btnCloseShareModalBottom) btnCloseShareModalBottom.onclick = closeShareModal; const btnShareCopyUrl = document.getElementById('btn-share-copy-url'); if (btnShareCopyUrl) { btnShareCopyUrl.onclick = () => copyEventLink(); } const btnShareWhatsapp = document.getElementById('btn-share-whatsapp'); if (btnShareWhatsapp) { btnShareWhatsapp.onclick = () => shareOnWhatsApp(); } const btnShareTelegram = document.getElementById('btn-share-telegram'); if (btnShareTelegram) { btnShareTelegram.onclick = () => shareOnTelegram(); } const btnShareFacebook = document.getElementById('btn-share-facebook'); if (btnShareFacebook) { btnShareFacebook.onclick = () => shareOnFacebook(); } const btnShareInstagram = document.getElementById('btn-share-instagram'); if (btnShareInstagram) { btnShareInstagram.onclick = () => shareOnInstagram(); } const btnShareTwitter = document.getElementById('btn-share-twitter'); if (btnShareTwitter) { btnShareTwitter.onclick = () => shareOnTwitter(); } const btnShareEmail = document.getElementById('btn-share-email'); if (btnShareEmail) { btnShareEmail.onclick = () => shareViaEmail(); } const btnShareNative = document.getElementById('btn-share-native'); if (btnShareNative) { btnShareNative.onclick = () => shareViaNative(); } // Presets de Camara 3D // Tsunami & Red de Boyas DART NOAA (Herramienta 3.22) const btnOpenDartTop = document.getElementById('btn-open-dart-top'); if (btnOpenDartTop) { btnOpenDartTop.onclick = () => openTsunamiDartModal(selectedEvent); } const btnOpenDartModal = document.getElementById('btn-open-dart-modal'); if (btnOpenDartModal) { btnOpenDartModal.onclick = () => openTsunamiDartModal(selectedEvent); } const btnDetailOpenDart = document.getElementById('btn-detail-open-dart'); if (btnDetailOpenDart) { btnDetailOpenDart.onclick = () => openTsunamiDartModal(selectedEvent); } const btnCloseDart = document.getElementById('btn-close-dart-modal'); if (btnCloseDart) { btnCloseDart.onclick = closeTsunamiDartModal; } const chkDartBuoys = document.getElementById('chk-dart-buoys'); if (chkDartBuoys) { chkDartBuoys.onchange = () => { renderDartBuoysOnMap(); refreshGlobeLayers(); }; } const chkSeismographs = document.getElementById('chk-seismographs'); if (chkSeismographs) { chkSeismographs.onchange = () => { refreshGlobeLayers(); }; } // Capa de Vectores Geodésicos GNSS/GPS (Herramienta 3.8) const chkGnssVectors = document.getElementById('chk-gnss-vectors'); if (chkGnssVectors) { chkGnssVectors.onchange = () => { if (chkGnssVectors.checked && typeof trackToolHit === 'function') trackToolHit('gnss_okada'); if (typeof renderGnssVectorsOnMap === 'function') renderGnssVectorsOnMap(); if (typeof renderGnssVectorsOnGlobe === 'function') renderGnssVectorsOnGlobe(); if (typeof updateVectorLayers === 'function') updateVectorLayers(); if (typeof refreshGlobeLayers === 'function') refreshGlobeLayers(); }; } const btnTsunamiFocus = document.getElementById('btn-tsunami-focus-globe'); if (btnTsunamiFocus) { btnTsunamiFocus.onclick = () => { closeTsunamiDartModal(); handleCloseEventDetails(); if (currentTsunamiEvent) { focusEarthquake(currentTsunamiEvent); } }; } const btnSliceCam3D = document.getElementById('btn-slice-cam-3d'); if (btnSliceCam3D) btnSliceCam3D.onclick = () => setSliceCameraView('3d'); const btnSliceCamSide = document.getElementById('btn-slice-cam-side'); if (btnSliceCamSide) btnSliceCamSide.onclick = () => setSliceCameraView('side'); const btnSliceCamTop = document.getElementById('btn-slice-cam-top'); if (btnSliceCamTop) btnSliceCamTop.onclick = () => setSliceCameraView('top'); const btnSliceCamReset = document.getElementById('btn-slice-cam-reset'); if (btnSliceCamReset) btnSliceCamReset.onclick = () => setSliceCameraView('reset'); // Selectores de Plano Nodular NP1 / NP2 const btnToggleNp1 = document.getElementById('btn-slice-toggle-np1'); if (btnToggleNp1) btnToggleNp1.onclick = () => toggleSlicePlane(1); const btnToggleNp2 = document.getElementById('btn-slice-toggle-np2'); if (btnToggleNp2) btnToggleNp2.onclick = () => toggleSlicePlane(2); const btnSliceFocus = document.getElementById('btn-slice-focus-globe'); if (btnSliceFocus) { btnSliceFocus.onclick = () => { if (currentSliceEvent) { closeSubterraneanSlice(); handleCloseEventDetails(); focusEarthquake(currentSliceEvent); } }; } // Controles de Time-Lapse (Máquina del Tiempo Sísmica) const btnTlPlay = document.getElementById('btn-timelapse-play'); if (btnTlPlay) btnTlPlay.onclick = toggleTimelapsePlay; const btnTlReset = document.getElementById('btn-timelapse-reset'); if (btnTlReset) { btnTlReset.onclick = () => { if (timelapseActive) toggleTimelapsePlay(); seekTimelapse(0); showToast('⏮ Rebobinado al inicio de la jornada (hace 24h)'); }; } const btnTlLive = document.getElementById('btn-timelapse-live'); if (btnTlLive) { btnTlLive.onclick = () => { if (timelapseActive) toggleTimelapsePlay(); const maxIdx = Math.max(timelapseSortedEvents.length - 1, 0); seekTimelapse(maxIdx); refreshGlobeLayers(); showToast('🔴 Sincronizado a Tiempo Real (En Vivo)'); }; } document.querySelectorAll('.btn-tl-speed').forEach(b => { b.onclick = () => { const spd = Number(b.getAttribute('data-speed')) || 1; setTimelapseSpeed(spd); }; }); const tlSlider = document.getElementById('timelapse-slider'); if (tlSlider) { let rafSeek = null; tlSlider.oninput = (e) => { const val = e.target.value; if (rafSeek) cancelAnimationFrame(rafSeek); rafSeek = requestAnimationFrame(() => { seekTimelapse(val); }); }; } const selHistoric = document.getElementById('select-historic-event'); if (selHistoric) { selHistoric.onchange = (e) => selectHistoricEvent(e.target.value); } // Modal Historial y Filtros Multidimensionales document.getElementById('btn-open-history').onclick = () => { document.getElementById('modal-history').classList.remove('hidden'); updateHistoryDataset(document.getElementById('filter-time').value || '24h'); }; document.getElementById('btn-close-history').onclick = () => { document.getElementById('modal-history').classList.add('hidden'); }; initExportDropdown(); document.getElementById('btn-reset-filters').onclick = () => { document.getElementById('filter-search').value = ''; document.getElementById('filter-mag').value = 'all'; document.getElementById('filter-continent').value = 'all'; document.getElementById('filter-plate').value = 'all'; document.getElementById('filter-env').value = 'all'; document.getElementById('filter-pop').value = 'all'; document.getElementById('filter-damage').value = 'all'; applyHistoryFilters(); }; // Listeners reactivos para filtros en vivo document.getElementById('filter-search').oninput = applyHistoryFilters; document.getElementById('filter-time').onchange = (e) => updateHistoryDataset(e.target.value); document.getElementById('filter-mag').onchange = applyHistoryFilters; document.getElementById('filter-continent').onchange = applyHistoryFilters; document.getElementById('filter-plate').onchange = applyHistoryFilters; document.getElementById('filter-env').onchange = applyHistoryFilters; document.getElementById('filter-pop').onchange = applyHistoryFilters; document.getElementById('filter-damage').onchange = applyHistoryFilters; // Toggle Mapa de Calor Sísmico (Malla Hexagonal 3D - Fase 400.0) document.getElementById('chk-heatmap').onchange = async (e) => { heatmapActive = e.target.checked; if (typeof updateHeatmapQuickButton === 'function') updateHeatmapQuickButton(heatmapActive); const ctrls = document.getElementById('heatmap-range-controls'); const legend = document.getElementById('heatmap-contour-legend'); if (heatmapActive) { if (ctrls) ctrls.classList.remove('opacity-50', 'pointer-events-none'); if (legend) legend.classList.remove('hidden'); const tl = document.getElementById('timelapse-bar'); if (tl) tl.classList.add('timelapse-shifted-right'); showToast('🔥 Proyectando Curvas de Nivel Sísmicas 3D (Líneas de Nivel de Colores)...'); await renderSeismicHeatmapContours(heatmapRange); } else { if (ctrls) ctrls.classList.add('opacity-50', 'pointer-events-none'); if (legend) legend.classList.add('hidden'); const tl = document.getElementById('timelapse-bar'); if (tl) tl.classList.remove('timelapse-shifted-right'); activeSeismicContourPaths = []; activeSeismicContourFeatures = []; updateVectorLayers(); if (myGlobe) myGlobe.hexBinPointsData([]); } }; // Selector de Rangos de Calor (24h, 7d, 30d) document.querySelectorAll('#heatmap-range-controls .btn-heat-range').forEach(btn => { btn.onclick = async () => { document.querySelectorAll('#heatmap-range-controls .btn-heat-range').forEach(b => { b.className = 'btn-heat-range py-1.5 px-2 rounded-xl bg-slate-900/60 border border-slate-800 text-slate-400 hover:text-white font-bold text-[10px] text-center cursor-pointer transition-all'; }); btn.className = 'btn-heat-range active py-1.5 px-2 rounded-xl bg-amber-500/20 border border-amber-500/60 text-amber-300 font-bold text-[10px] text-center cursor-pointer transition-all'; const range = btn.getAttribute('data-range'); await renderSeismicHeatmapContours(range); }; }); // ==================================================================== // MODAL ALERTAS BENTO STEPPER (3 PASOS SECUENCIALES - MÍNIMOS CLICS) // ==================================================================== let currentAlertStep = 1; function setAlertStep(step) { currentAlertStep = Math.max(1, Math.min(3, step)); const s1 = document.getElementById('alert-step-1'); const s2 = document.getElementById('alert-step-2'); const s3 = document.getElementById('alert-step-3'); if (s1) s1.classList.toggle('hidden', currentAlertStep !== 1); if (s2) s2.classList.toggle('hidden', currentAlertStep !== 2); if (s3) s3.classList.toggle('hidden', currentAlertStep !== 3); // Actualizar Stepper Header Tabs for (let i = 1; i <= 3; i++) { const tab = document.getElementById(`step-tab-${i}`); const num = document.getElementById(`step-num-${i}`); if (!tab || !num) continue; if (i === currentAlertStep) { tab.className = 'flex-1 flex items-center justify-center gap-1.5 py-1.5 px-2 rounded-lg text-xs font-black transition-all bg-rose-500/20 text-rose-300 border border-rose-500/40 shadow-sm cursor-pointer'; num.className = 'w-5 h-5 rounded-full bg-rose-500 text-white text-[11px] flex items-center justify-center font-bold flex-shrink-0'; num.innerText = String(i); } else if (i < currentAlertStep) { tab.className = 'flex-1 flex items-center justify-center gap-1.5 py-1.5 px-2 rounded-lg text-xs font-semibold transition-all text-emerald-400 border border-emerald-500/30 bg-emerald-500/10 cursor-pointer'; num.className = 'w-5 h-5 rounded-full bg-emerald-500 text-slate-950 text-[11px] flex items-center justify-center font-black flex-shrink-0'; num.innerText = '✓'; } else { tab.className = 'flex-1 flex items-center justify-center gap-1.5 py-1.5 px-2 rounded-lg text-xs font-semibold transition-all text-slate-500 border border-transparent cursor-pointer hover:text-slate-300'; num.className = 'w-5 h-5 rounded-full bg-slate-800 text-slate-400 text-[11px] flex items-center justify-center font-bold flex-shrink-0'; num.innerText = String(i); } } // Si entramos al Paso 3, actualizar tarjeta resumen en tiempo real if (currentAlertStep === 3) { const chkG = document.getElementById('chk-alert-global'); const isGlob = chkG ? chkG.checked : true; const sumCov = document.getElementById('step3-sum-coverage'); if (sumCov) { sumCov.innerText = isGlob ? '🌍 Global' : `📍 Local (${radiusKm} km)`; sumCov.className = isGlob ? 'font-bold text-rose-400' : 'font-bold text-sky-400'; } const inMag = document.getElementById('input-minmag'); const sumMag = document.getElementById('step3-sum-mag'); if (sumMag && inMag) sumMag.innerText = `≥ M ${Number(inMag.value).toFixed(1)}`; const personaRadio = document.querySelector('input[name="alert-persona"]:checked'); const personaVal = personaRadio ? personaRadio.value : 'general'; const sumProf = document.getElementById('step3-sum-profile'); if (sumProf) { let pText = '🏠 Público'; let pClass = 'font-bold text-emerald-400 truncate block'; if (personaVal === 'scientist') { pText = '🔬 Científico'; pClass = 'font-bold text-sky-400 truncate block'; } else if (personaVal === 'journalist') { pText = '📰 Prensa'; pClass = 'font-bold text-amber-400 truncate block'; } if (typeof wantWebPush !== 'undefined' && wantWebPush) { pText += ' + 🔔 Push'; } sumProf.innerText = pText; sumProf.className = pClass; } } // Refrescar Leaflet si estamos en Paso 1 en modo local if (currentAlertStep === 1) { const chkG = document.getElementById('chk-alert-global'); if (chkG && !chkG.checked) { setTimeout(() => { if (leafletMap) leafletMap.invalidateSize(); else initLeafletMap(); }, 100); } } } function selectCoverageMode(isGlobal) { const chkG = document.getElementById('chk-alert-global'); if (chkG) chkG.checked = isGlobal; const cardGlobal = document.getElementById('card-cov-global'); const cardLocal = document.getElementById('card-cov-local'); const badgeGlobal = document.getElementById('badge-cov-global'); const badgeLocal = document.getElementById('badge-cov-local'); const geofenceSec = document.getElementById('geofence-location-section'); if (isGlobal) { if (cardGlobal) cardGlobal.className = 'p-3.5 rounded-xl border border-rose-500 bg-rose-500/10 cursor-pointer transition-all hover:border-rose-400 flex flex-col justify-between gap-1 shadow-sm'; if (cardLocal) cardLocal.className = 'p-3.5 rounded-xl border border-slate-800 bg-slate-900/60 cursor-pointer transition-all hover:border-slate-700 flex flex-col justify-between gap-1'; if (badgeGlobal) badgeGlobal.classList.remove('hidden'); if (badgeLocal) badgeLocal.classList.add('hidden'); if (geofenceSec) geofenceSec.classList.add('hidden'); } else { if (cardLocal) cardLocal.className = 'p-3.5 rounded-xl border border-sky-500 bg-sky-500/10 cursor-pointer transition-all hover:border-sky-400 flex flex-col justify-between gap-1 shadow-sm'; if (cardGlobal) cardGlobal.className = 'p-3.5 rounded-xl border border-slate-800 bg-slate-900/60 cursor-pointer transition-all hover:border-slate-700 flex flex-col justify-between gap-1'; if (badgeLocal) badgeLocal.classList.remove('hidden'); if (badgeGlobal) badgeGlobal.classList.add('hidden'); if (geofenceSec) geofenceSec.classList.remove('hidden'); setTimeout(() => { if (leafletMap) leafletMap.invalidateSize(); else initLeafletMap(); }, 150); } } function setMinMag(val) { const inputMag = document.getElementById('input-minmag'); const magVal = document.getElementById('mag-val'); const num = Number(val); if (inputMag) inputMag.value = num; if (magVal) magVal.innerText = `> M ${num.toFixed(1)}`; document.querySelectorAll('.btn-quick-mag').forEach(btn => { const bMag = Number(btn.getAttribute('data-mag')); if (Math.abs(bMag - num) < 0.05) { btn.className = 'btn-quick-mag py-1.5 px-2 rounded-lg text-xs font-bold bg-amber-500/20 text-amber-300 border border-amber-500/60 transition-all cursor-pointer'; } else { btn.className = 'btn-quick-mag py-1.5 px-2 rounded-lg text-xs font-bold bg-slate-900 border border-slate-700/80 hover:border-amber-400 text-slate-300 transition-all cursor-pointer'; } }); } // ==================================================================== // VISOR & GENERADOR DE FLYERS OFICIALES HD (1200x630 / PNG / WHATSAPP) // ==================================================================== window.openFlyerPreviewModal = function() { const eq = selectedEvent || (allEvents && allEvents[0]); if (!eq) { if (typeof showToast === 'function') showToast('Selecciona un sismo para generar su Flyer Oficial'); return; } const modal = document.getElementById('modal-flyer-preview'); const img = document.getElementById('flyer-preview-img'); if (img) { img.src = `/api/og/${encodeURIComponent(eq.id)}.svg?t=${Date.now()}`; } if (modal) modal.classList.remove('hidden'); }; window.closeFlyerPreviewModal = function() { const modal = document.getElementById('modal-flyer-preview'); if (modal) modal.classList.add('hidden'); }; window.downloadFlyerAsPng = function() { const eq = selectedEvent || (allEvents && allEvents[0]); if (!eq) return; if (typeof showToast === 'function') showToast('Renderizando Flyer HD en alta definición...'); const svgUrl = `/api/og/${encodeURIComponent(eq.id)}.svg?t=${Date.now()}`; const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { try { const canvas = document.createElement('canvas'); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, 1200, 630); const a = document.createElement('a'); const cleanPlace = (eq.place || 'sismo').replace(/[^a-zA-Z0-9]/g, '_'); a.download = `Flyer_Oficial_M${(eq.magnitude || 0).toFixed(1)}_${cleanPlace}.png`; a.href = canvas.toDataURL('image/png'); document.body.appendChild(a); a.click(); document.body.removeChild(a); if (typeof showToast === 'function') showToast('📸 ¡Flyer Oficial HD descargado correctamente!'); } catch (err) { const a = document.createElement('a'); a.href = svgUrl; a.download = `Flyer_Oficial_M${(eq.magnitude || 0).toFixed(1)}.svg`; a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } }; img.onerror = function() { window.open(svgUrl, '_blank'); }; img.src = svgUrl; }; // GESTIÓN DE ALERTAS SÍSMICAS POR WHATSAPP (FASE 354 / 199.0) window.openWhatsAppSubscriptionModal = function() { const modal = document.getElementById('modal-whatsapp-subscription'); if (modal) modal.classList.remove('hidden'); if (typeof window.enhanceWhatsAppModalWithConversationalAI === 'function') { window.enhanceWhatsAppModalWithConversationalAI(); } }; window.closeWhatsAppSubscriptionModal = function() { const modal = document.getElementById('modal-whatsapp-subscription'); if (modal) modal.classList.add('hidden'); }; // Feature Flag: Control remoto de presencia de WhatsApp en Web & Móvil async function checkWhatsAppFeatureFlag() { try { const res = await fetch('/api/whatsapp/config'); if (!res.ok) return; const cfg = await res.json(); const btnDesk = document.getElementById('btn-whatsapp-alerts-desktop'); const btnMobile = document.getElementById('btn-bottom-nav-whatsapp'); const btnMobileHeader = document.getElementById('btn-whatsapp-mobile-header'); const btnShareWa = document.getElementById('btn-share-whatsapp'); const linkWa = document.getElementById('link-launch-whatsapp'); if (cfg && cfg.enabled) { if (btnDesk) btnDesk.classList.remove('hidden'); if (btnMobile) btnMobile.classList.remove('hidden'); if (btnMobileHeader) btnMobileHeader.classList.remove('hidden'); if (btnShareWa) { btnShareWa.classList.remove('hidden'); btnShareWa.style.display = 'flex'; } } else if (cfg && cfg.enabled === false) { if (btnDesk) btnDesk.classList.add('hidden'); if (btnMobile) btnMobile.classList.add('hidden'); if (btnMobileHeader) btnMobileHeader.classList.add('hidden'); if (btnShareWa) { btnShareWa.classList.add('hidden'); btnShareWa.style.display = 'none'; } } if (cfg && cfg.phoneNumber && linkWa) { const cleanPhone = cfg.phoneNumber.replace(/[^0-9]/g, ''); linkWa.href = `https://wa.me/${cleanPhone}?text=ALERTA`; const phoneDisplay = document.getElementById('wa-modal-phone-display'); if (phoneDisplay) phoneDisplay.textContent = cfg.phoneNumber; const qrImg = document.getElementById('wa-modal-qr-img'); if (qrImg) { qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=140x140&data=${encodeURIComponent('https://wa.me/' + cleanPhone + '?text=ALERTA')}`; } } } catch(e) { // Si hay error de red, permanece silencioso } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkWhatsAppFeatureFlag); } else { checkWhatsAppFeatureFlag(); } function handleOpenAlertModal() { if (typeof window.trackGCOEvent === 'function') { window.trackGCOEvent('activate_alerts_click', { category: 'conversion', label: 'cta_button' }); } const modal = document.getElementById('modal-alert'); if (!modal) return; modal.classList.remove('hidden'); setAlertStep(1); // Preactivación por defecto de Web Push (Fase 82.1) if (typeof wantWebPush !== 'undefined' && typeof updatePushUiState === 'function') { if (typeof Notification === 'undefined' || Notification.permission !== 'denied') { wantWebPush = true; updatePushUiState(true); } } if (userAlertConfig) { if (userAlertConfig.email) document.getElementById('alert-email').value = userAlertConfig.email; if (userAlertConfig.persona) { const r = document.querySelector(`input[name="alert-persona"][value="${userAlertConfig.persona}"]`); if (r) r.checked = true; } if (userAlertConfig.minMag) { setMinMag(userAlertConfig.minMag); } if (userAlertConfig.radiusKm && !userAlertConfig.isGlobal) { radiusKm = userAlertConfig.radiusKm; const inRad = document.getElementById('input-radius'); if (inRad) inRad.value = radiusKm; const rVal = document.getElementById('radius-val'); if (rVal) rVal.innerText = `${radiusKm} km`; } selectCoverageMode(userAlertConfig ? userAlertConfig.isGlobal === true : false); } else { selectCoverageMode(false); } } window.openAlertModal = handleOpenAlertModal; const btnOpenAlert = document.getElementById('btn-open-alert'); if (btnOpenAlert) btnOpenAlert.onclick = handleOpenAlertModal; const btnDesktopAlert = document.getElementById('btn-desktop-alert'); if (btnDesktopAlert) btnDesktopAlert.onclick = handleOpenAlertModal; const btnBottomAlert = document.getElementById('btn-bottom-alert'); if (btnBottomAlert) btnBottomAlert.onclick = (e) => onSismosferaDockAlerts(e); const btnCloseModal = document.getElementById('btn-close-modal'); if (btnCloseModal) { btnCloseModal.onclick = () => { document.getElementById('modal-alert').classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') { updateSismosferaDockVisualState('globe'); } }; } // Event Listeners del Stepper y Controles en 1 Clic const cardCovGlobal = document.getElementById('card-cov-global'); if (cardCovGlobal) cardCovGlobal.onclick = () => selectCoverageMode(true); const cardCovLocal = document.getElementById('card-cov-local'); if (cardCovLocal) cardCovLocal.onclick = () => selectCoverageMode(false); const chkGlobal = document.getElementById('chk-alert-global'); if (chkGlobal) { chkGlobal.onchange = (e) => selectCoverageMode(e.target.checked); } document.querySelectorAll('.btn-quick-mag').forEach(btn => { btn.onclick = () => setMinMag(btn.getAttribute('data-mag')); }); const inMinMag = document.getElementById('input-minmag'); if (inMinMag) { inMinMag.oninput = (e) => setMinMag(e.target.value); } const inRadius = document.getElementById('input-radius'); if (inRadius) { inRadius.oninput = (e) => { radiusKm = Number(e.target.value); const rVal = document.getElementById('radius-val'); if (rVal) rVal.innerText = `${radiusKm} km`; if (leafletCircle) leafletCircle.setRadius(radiusKm * 1000); }; } // Botones de Avance y Retroceso del Stepper const btnNext1 = document.getElementById('btn-next-step-1'); if (btnNext1) btnNext1.onclick = () => setAlertStep(2); const btnNext2 = document.getElementById('btn-next-step-2'); if (btnNext2) { btnNext2.onclick = () => { setAlertStep(3); if (typeof wantWebPush !== 'undefined' && wantWebPush && !pushSubscriptionActive && typeof triggerWebPushSubscription === 'function') { triggerWebPushSubscription(false); } }; } const btnBack1 = document.getElementById('btn-back-step-1'); if (btnBack1) btnBack1.onclick = () => setAlertStep(1); const btnBack2 = document.getElementById('btn-back-step-2'); if (btnBack2) btnBack2.onclick = () => setAlertStep(2); const tab1 = document.getElementById('step-tab-1'); if (tab1) tab1.onclick = () => setAlertStep(1); const tab2 = document.getElementById('step-tab-2'); if (tab2) tab2.onclick = () => setAlertStep(2); const tab3 = document.getElementById('step-tab-3'); if (tab3) tab3.onclick = () => setAlertStep(3); // Envío Formulario Geocercas / Alerta Global (Persistencia Real API + LocalStorage) document.getElementById('geofence-form').onsubmit = (e) => { e.preventDefault(); const email = document.getElementById('alert-email').value.trim(); const isGlobal = document.getElementById('chk-alert-global') ? document.getElementById('chk-alert-global').checked : false; const minMag = Number(document.getElementById('input-minmag').value) || 4.5; const personaRadio = document.querySelector('input[name="alert-persona"]:checked'); const persona = personaRadio ? personaRadio.value : 'general'; userAlertConfig = { email: email, persona: persona, isGlobal: isGlobal, minMag: minMag, minMagnitude: minMag, centerLat: centerLat, centerLon: centerLon, radiusKm: radiusKm, onlyFeltAlerts: true, minFeltMmi: 3.0, active: true, updatedAt: new Date().toISOString() }; localStorage.setItem('terremoto_alert_config', JSON.stringify(userAlertConfig)); // Persistir en servidor real /api/subscribers (Golden Rule 1) fetch('/api/subscribers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, persona: persona, isGlobal: isGlobal, minMag: minMag, minMagnitude: minMag, centerLat: centerLat, centerLon: centerLon, radiusKm: radiusKm, onlyFeltAlerts: true, minFeltMmi: 3.0, active: true }) }).then(r => r.json()).then(res => { if (res.success) { console.log('[SUBSCRIBER SYNC OK]', res.subscriber); if (typeof renderNocSubscribersTab === 'function') renderNocSubscribersTab(); if (typeof renderNocTopKpis === 'function') renderNocTopKpis(); } }).catch(err => { console.warn('[SUBSCRIBER SYNC WARN]', err); }); // Registrar hit en analítica if (typeof trackToolHit === 'function') trackToolHit('alert_subscription'); // Disparar suscripción Web Push en segundo plano si está activado por defecto if (typeof wantWebPush !== 'undefined' && wantWebPush && !pushSubscriptionActive && typeof triggerWebPushSubscription === 'function') { triggerWebPushSubscription(false); } // Reemplazo del alert() nativo por modal Glassmorphic Bento de alta fidelidad (Fase 18.0) document.getElementById('modal-alert').classList.add('hidden'); showCustomAlertConfirmedModal({ email: email, persona: persona, isGlobal: isGlobal, minMag: minMag, centerLat: centerLat, centerLon: centerLon, radiusKm: radiusKm }); }; // Función de Despliegue del Modal de Confirmación de Alertas (Fase 18.0) function showCustomAlertConfirmedModal(sub) { const modal = document.getElementById('modal-alert-confirmed'); if (!modal) return; const emailEl = document.getElementById('confirmed-email-val'); const personaEl = document.getElementById('confirmed-persona-val'); const coverageEl = document.getElementById('confirmed-coverage-val'); const thresholdEl = document.getElementById('confirmed-threshold-val'); const personaBadge = document.getElementById('confirmed-persona-badge'); const whatContent = document.getElementById('confirmed-what-content'); const whenContent = document.getElementById('confirmed-when-content'); const subIdEl = document.getElementById('confirmed-sub-id'); if (emailEl) emailEl.innerText = sub.email; if (thresholdEl) thresholdEl.innerText = `≥ M ${Number(sub.minMag).toFixed(1)}`; if (subIdEl) subIdEl.innerText = `#SUB-${Date.now().toString().slice(-6)}`; const isGlobal = !!sub.isGlobal; if (coverageEl) { coverageEl.innerText = isGlobal ? '🌍 Global (Planetario)' : `📍 Local (${sub.radiusKm} km)`; } // 1. ¿QUÉ VAS A RECIBIR EXACTAMENTE? (Adaptado al Perfil) if (sub.persona === 'scientist') { if (personaEl) personaEl.innerHTML = '🔬 Científico'; if (personaBadge) { personaBadge.innerText = 'Dossier Geofísico Avanzado'; personaBadge.className = 'ml-auto text-[10px] font-bold px-2 py-0.5 rounded-md bg-sky-500/20 text-sky-300 border border-sky-500/30'; } if (whatContent) { whatContent.innerHTML = `
📊
Parámetros Geofísicos Mw & M0 Magnitud momento calculada, momento sísmico escalar en Dyn·cm y Joules, profundidad focal y régimen tectónico.
🏐
Mecanismos Focales & Tensores Planos nodales cinemáticos (Strike, Dip, Rake), ejes P/T de compresión/tensión y tipo de falla geológica.
⏱️
Tiempos de Viaje ak135 Arribos teóricos modelados para ondas primarias P, secundarias S y fases superficiales en estaciones mundiales.
📈
Aceleración PGA & miniSEED Aceleración espectral pico de terreno (%g PGA vía NGA-West2) y enlaces directos a telemetría FDSN y ShakeMap.
`; } } else if (sub.persona === 'journalist') { if (personaEl) personaEl.innerHTML = '📰 Prensa / Medios'; if (personaBadge) { personaBadge.innerText = 'Despacho Periodístico Urgente'; personaBadge.className = 'ml-auto text-[10px] font-bold px-2 py-0.5 rounded-md bg-amber-500/20 text-amber-300 border border-amber-500/30'; } if (whatContent) { whatContent.innerHTML = `
⚡
Cable Urgente (Breaking News) Titular periodístico y lead 5W estructurado listo para locución al aire, publicación web o redes sociales.
📝
Ciudades & Población Expuesta Poblaciones habitadas más próximas al epicentro, censo aproximado en zona sentida y profundidad focal.
🏛️
Citas Oficiales de Agencias Reportes oficiales consolidados de USGS, EMSC e INPRES con horas duales sincronizadas (UTC y hora local).
📊
Estimación de Pérdidas PAGER Semáforo preliminar de afectación de estructuras y víctimas potenciales según el sistema PAGER del USGS.
`; } } else { // Público General if (personaEl) personaEl.innerHTML = '🏠 Público General'; if (personaBadge) { personaBadge.innerText = 'Boletín de Emergencia y Protección'; personaBadge.className = 'ml-auto text-[10px] font-bold px-2 py-0.5 rounded-md bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'; } if (whatContent) { whatContent.innerHTML = `
🚨
Notificación de Sismo Sentido Magnitud clara, profundidad y distancia precisa en kilómetros desde el epicentro del sismo hasta tu ubicación.
🧭
Intensidad Sentida (MMI) Nivel estimado de sacudida en tu área (Leve, Moderado, Fuerte o Severo) para comprender el impacto real.
🛡️
Protocolo de Autoprotección Instrucciones tácticas: "Agáchate, Cúbrete y Sujétate", corte de gas y electricidad y alejamiento de vidrios.
📞
Líneas de Auxilio & Defensa Civil Números de emergencia oficiales de tu zona para reporte de emergencias o solicitud de asistencia.
`; } } // 2. ¿CUÁNDO LO VAS A RECIBIR? (Condiciones de Envío y SLA) if (whenContent) { if (isGlobal) { whenContent.innerHTML = `
✓
Activación Global Continua 24/7: Ante cualquier sismo en cualquier lugar del mundo cuya magnitud sea igual o mayor a M ${Number(sub.minMag).toFixed(1)}.
✓
Velocidad de Despacho Inmediato: Menos de 30 a 60 segundos tras el consenso multi-agencia entre USGS y EMSC.
✓
Garantía Cero Spam: No recibirás sismos menores que no alcancen tu umbral. Puedes modificar tus preferencias o desuscribirte con 1 clic en el pie de cada correo.
`; } else { const cLat = sub.centerLat != null ? Number(sub.centerLat).toFixed(2) : '-'; const cLon = sub.centerLon != null ? Number(sub.centerLon).toFixed(2) : '-'; whenContent.innerHTML = `
✓
Activación por Geocerca Local: Únicamente cuando un sismo tenga magnitud ≥ M ${Number(sub.minMag).toFixed(1)} Y su epicentro esté dentro de tu radio de ${sub.radiusKm} km (Centro: ${cLat}°, ${cLon}°).
✓
Velocidad de Despacho Inmediato: Menos de 30 a 60 segundos posteriores a la detección de las ondas sísmicas por la red de sismógrafos.
✓
Silencio Protector: Terremotos lejanos fuera de tu radio de cobertura no te molestarán para garantizar un buzón limpio.
`; } } modal.classList.remove('hidden'); if (typeof showToast === 'function') { showToast('🔔 Alerta sísmica activada con éxito'); } } window.showCustomAlertConfirmedModal = showCustomAlertConfirmedModal; // Listeners de Cierre para el Modal de Confirmación const btnCloseConfirmed = document.getElementById('btn-close-alert-confirmed'); if (btnCloseConfirmed) { btnCloseConfirmed.onclick = () => document.getElementById('modal-alert-confirmed').classList.add('hidden'); } const btnConfirmedOk = document.getElementById('btn-alert-confirmed-ok'); if (btnConfirmedOk) { btnConfirmedOk.onclick = () => document.getElementById('modal-alert-confirmed').classList.add('hidden'); } const modalConfirmedEl = document.getElementById('modal-alert-confirmed'); if (modalConfirmedEl) { modalConfirmedEl.onclick = (e) => { if (e.target === modalConfirmedEl) modalConfirmedEl.classList.add('hidden'); }; } // ==================================================================== // LÓGICA DEL BUZÓN DE SUGERENCIAS & PORTAL INSTITUCIONAL (FASE 19.0) // ==================================================================== function openSuggestionsModal(tab = 'form') { const modal = document.getElementById('modal-suggestions'); if (modal) { modal.classList.remove('hidden'); switchSuggestionsModalTab(tab); const feedbackBox = document.getElementById('suggestion-feedback-box'); if (feedbackBox) feedbackBox.classList.add('hidden'); } } window.openSuggestionsModal = openSuggestionsModal; function closeSuggestionsModal() { const modal = document.getElementById('modal-suggestions'); if (modal) modal.classList.add('hidden'); } window.closeSuggestionsModal = closeSuggestionsModal; function switchSuggestionsModalTab(tabKey) { const views = { form: document.getElementById('sug-view-form'), about: document.getElementById('sug-view-about'), sources: document.getElementById('sug-view-sources') }; const buttons = { form: document.getElementById('tab-sug-btn-form'), about: document.getElementById('tab-sug-btn-about'), sources: document.getElementById('tab-sug-btn-sources') }; Object.keys(views).forEach(k => { if (views[k]) { if (k === tabKey) { views[k].classList.remove('hidden'); } else { views[k].classList.add('hidden'); } } if (buttons[k]) { if (k === tabKey) { buttons[k].className = 'flex-1 min-w-[140px] py-2 px-3 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-1.5 bg-amber-500 text-slate-950 shadow-md cursor-pointer'; } else { buttons[k].className = 'flex-1 min-w-[140px] py-2 px-3 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-1.5 text-slate-400 hover:text-white hover:bg-slate-800/60 cursor-pointer'; } } }); } window.switchSuggestionsModalTab = switchSuggestionsModalTab; async function handleSuggestionSubmit(e) { if (e) e.preventDefault(); const nameInput = document.getElementById('suggestion-name'); const emailInput = document.getElementById('suggestion-email'); const catInput = document.getElementById('suggestion-category'); const msgInput = document.getElementById('suggestion-message'); const feedbackBox = document.getElementById('suggestion-feedback-box'); const submitBtn = document.getElementById('btn-submit-suggestion'); const submitText = document.getElementById('btn-submit-suggestion-text'); if (!nameInput || !emailInput || !msgInput) return; const name = nameInput.value.trim(); const email = emailInput.value.trim().toLowerCase(); const category = catInput ? catInput.value : 'improvement'; const message = msgInput.value.trim(); const recipientInput = document.getElementById('suggestion-recipient'); const subjectInput = document.getElementById('suggestion-subject'); const recipientEmail = recipientInput ? recipientInput.value : '[email protected]'; const subject = subjectInput ? subjectInput.value.trim() : ''; function showFeedback(text, type) { if (!feedbackBox) return; feedbackBox.classList.remove('hidden'); if (type === 'error') { feedbackBox.className = 'p-3 rounded-xl text-xs font-semibold flex items-center gap-2 bg-rose-500/20 text-rose-300 border border-rose-500/40 animate-in fade-in'; } else { feedbackBox.className = 'p-3 rounded-xl text-xs font-semibold flex items-center gap-2 bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 animate-in fade-in'; } feedbackBox.innerHTML = `${type === 'error' ? '⚠️' : '✅'} ${text}`; } // Validaciones defensivas if (name.length < 2) { showFeedback('⚠️ Por favor indica tu nombre de pila (al menos 2 caracteres).', 'error'); nameInput.focus(); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email || !emailRegex.test(email)) { showFeedback('⚠️ Por favor ingresa un correo electrónico válido de contacto.', 'error'); emailInput.focus(); return; } if (message.length < 5) { showFeedback('⚠️ Por favor describe tu sugerencia con más detalle (al menos 5 caracteres).', 'error'); msgInput.focus(); return; } // Estado enviando if (submitBtn) submitBtn.disabled = true; if (submitText) submitText.textContent = 'Enviando al NOC...'; try { const res = await fetch('/api/suggestions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, recipientEmail, subject, category, message, domain: 'sismosfera.com' }) }); const result = await res.json(); if (res.ok && result.success) { showFeedback('¡Muchas gracias ' + (typeof escapeHtml === 'function' ? escapeHtml(name) : name) + '! Tu mensaje ha sido recibido e indexado en nuestro Dashboard NOC.', 'success'); nameInput.value = ''; emailInput.value = ''; msgInput.value = ''; if (subjectInput) subjectInput.value = ''; if (typeof showToast === 'function') { showToast('📬 Mensaje registrado con éxito en el NOC'); } } else { showFeedback(result.error || 'Error al enviar la sugerencia. Por favor reintenta.', 'error'); } } catch (err) { showFeedback('Error de conexión con el servidor. Revisa tu red y reintenta.', 'error'); } finally { if (submitBtn) submitBtn.disabled = false; if (submitText) submitText.textContent = 'Enviar Sugerencia al Dashboard NOC'; } } window.handleSuggestionSubmit = handleSuggestionSubmit; // Event Listeners para Apertura y Cierre de Sugerencias const btnOpenSug = document.getElementById('btn-open-suggestions'); if (btnOpenSug) btnOpenSug.onclick = () => openSuggestionsModal('form'); const btnOpenSugMenu = document.getElementById('btn-open-suggestions-menu'); if (btnOpenSugMenu) btnOpenSugMenu.onclick = () => openSuggestionsModal('form'); const btnCloseSug = document.getElementById('btn-close-suggestions'); if (btnCloseSug) btnCloseSug.onclick = closeSuggestionsModal; const modalSugEl = document.getElementById('modal-suggestions'); if (modalSugEl) { modalSugEl.onclick = (e) => { if (e.target === modalSugEl) closeSuggestionsModal(); }; } // Selector / Toggle de Huso Horario de la Ubicación del Usuario const btnTz = document.getElementById('btn-toggle-timezone'); if (btnTz) btnTz.onclick = toggleTimeDisplayMode; const headerClockWidget = document.getElementById('header-clock-widget'); if (headerClockWidget) headerClockWidget.onclick = toggleTimeDisplayMode; // Iniciar sincronización de huso horario y relojes globales cada segundo detectUserTimeZone(); updateTimeZoneUI(); updateGlobalClocks(); setInterval(() => { if (document.hidden) return; updateGlobalClocks(); }, 1000); // Iniciar WebSocket en vivo con EMSC (SeismicPortal) para latencia instantánea (< 2s) initEmscWebSocket(); // Fase 124.1 (Pilar 4): Cadencias centralizadas y racionalizadas (una sola fuente de verdad) window.SISMOSFERA_POLL_MS = { usgs: 12000, emsc: 5000, emscWsSafety: 60000, multi: 30000 }; // Sondeo rápido diferencial USGS (última hora) cada 12 segundos (Fase 124.1: antes 4 s; el feed USGS se regenera ~1/min) fastPollUSGS(); window._fastPollUsgsInterval = setInterval(fastPollUSGS, window.SISMOSFERA_POLL_MS.usgs); // Sondeo rápido diferencial EMSC cada 5 segundos SOLO si el WebSocket EMSC no está abierto. // Con el WebSocket vivo, el HTTP queda como red de seguridad cada 60 s (Fase 124.1). async function fastPollEMSC() { if (!initialConsolidationComplete) return; // ESPERAR a que la consolidación inicial esté lista const _wsOpen = (typeof emscWs !== 'undefined') && emscWs && emscWs.readyState === WebSocket.OPEN; const _nowPoll = Date.now(); if (_wsOpen && (_nowPoll - (window._emscHttpLastRun || 0)) < window.SISMOSFERA_POLL_MS.emscWsSafety) return; window._emscHttpLastRun = _nowPoll; try { const res = await fetch('https://www.seismicportal.eu/fdsnws/event/1/query?format=json&limit=15', { signal: AbortSignal.timeout(3500) }); if (!res.ok) return; const data = await res.json(); const features = data.features || []; // Filtrar estrictamente solo sismos no conocidos todavía const newFeatures = features.filter(f => { const deterministicId = f.id || (f.properties && f.properties.unid) || (f.properties && `emsc_${f.properties.time}_${f.properties.lat}_${f.properties.lon}`) || `emsc_${f.properties?.time || Date.now()}`; return !knownEarthquakeIds.has(deterministicId); }); if (newFeatures.length === 0) return; newFeatures.sort((a, b) => { const tA = parseAgencyTime(a.properties && a.properties.time).getTime(); const tB = parseAgencyTime(b.properties && b.properties.time).getTime(); return tA - tB; }); for (const f of newFeatures) { const deterministicId = f.id || (f.properties && f.properties.unid) || (f.properties && `emsc_${f.properties.time}_${f.properties.lat}_${f.properties.lon}`) || `emsc_${f.properties?.time || Date.now()}`; const rep = { agency: 'EMSC', id: deterministicId, mag: (f.properties && f.properties.mag) || 0, depth: Math.abs((f.geometry && f.geometry.coordinates && f.geometry.coordinates[2]) || (f.properties && f.properties.depth) || 10), lat: f.geometry && f.geometry.coordinates && f.geometry.coordinates[1], lon: f.geometry && f.geometry.coordinates && f.geometry.coordinates[0], time: parseAgencyTime(f.properties && f.properties.time), place: sanitizeSeismicText((f.properties && f.properties.flynn_region) || 'Región Sísmica EMSC'), tsunami: 0, felt: null, cdi: null, isRealtimePush: true }; ingestRealtimeReport(rep); } } catch (e) { // Ignorar fallas momentáneas de red } } fastPollEMSC(); window._fastPollEmscInterval = setInterval(fastPollEMSC, window.SISMOSFERA_POLL_MS.emsc); // Actualizar periódicamente los distintivos de tiempo relativo ("hace X min") setInterval(() => { if (document.hidden) return; updateFeedRelativeTimes(); }, 20000); // Sondeo multi-agencia consolidado global (6 agencias) cada 30 segundos window._multiAgencyInterval = setInterval(loadMultiAgencyData, window.SISMOSFERA_POLL_MS.multi); // Optimización Extrema de Recursos: Pausa Inteligente en Segundo Plano (Page Visibility API - Fase 122.0) document.addEventListener('visibilitychange', () => { if (document.hidden) { if (window._fastPollUsgsInterval) { clearInterval(window._fastPollUsgsInterval); window._fastPollUsgsInterval = null; } if (window._fastPollEmscInterval) { clearInterval(window._fastPollEmscInterval); window._fastPollEmscInterval = null; } if (window._multiAgencyInterval) { clearInterval(window._multiAgencyInterval); window._multiAgencyInterval = null; } } else { // Reactivación inmediata con refresco instantáneo al volver al primer plano if (typeof fastPollUSGS === 'function') fastPollUSGS(); if (typeof fastPollEMSC === 'function') fastPollEMSC(); if (typeof loadMultiAgencyData === 'function') loadMultiAgencyData(); if (!window._fastPollUsgsInterval) window._fastPollUsgsInterval = setInterval(fastPollUSGS, window.SISMOSFERA_POLL_MS.usgs); if (!window._fastPollEmscInterval) window._fastPollEmscInterval = setInterval(fastPollEMSC, window.SISMOSFERA_POLL_MS.emsc); if (!window._multiAgencyInterval) window._multiAgencyInterval = setInterval(loadMultiAgencyData, window.SISMOSFERA_POLL_MS.multi); } }); // Inicializar palanca vertical táctica de zoom 3D (Fase 10.9) initGlobeZoomSlider(); // PWA & Resiliencia Offline (Fase F23.0 / Herramienta F4.2) function initPwaAndOfflineHandlers() { if (window._pwaOfflineHandlersInit) return; window._pwaOfflineHandlersInit = true; const banner = document.getElementById('pwa-offline-banner'); function updateOnlineStatus() { if (!navigator.onLine) { if (banner) banner.classList.remove('hidden'); } else { if (banner) banner.classList.add('hidden'); } } window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); updateOnlineStatus(); if ('serviceWorker' in navigator && window.location.protocol.startsWith('http')) { navigator.serviceWorker.register('/sw.js').then(reg => { reg.update(); return reg; }) .then(reg => console.log('[PWA] Service Worker registrado con éxito, scope:', reg.scope)) .catch(err => console.warn('[PWA] Error al registrar Service Worker:', err)); } } // ======================================================================= // GESTIÓN DEL MODAL DE TÉRMINOS LEGALES & BLINDAJE JURÍDICO (FASE 24.0) // ======================================================================= // ======================================================================= // ARQUITECTURA PWA & WEB PUSH NOTIFICATIONS (FASE 63.0) // ======================================================================= let deferredPwaPrompt = null; let pushSubscriptionActive = false; let wantWebPush = true; // Preactivado por defecto (Fase 82.1) function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } function initPwaInstallExperience() { const banner = document.getElementById('pwa-install-banner'); const btnAction = document.getElementById('btn-pwa-install-action'); const btnDismiss = document.getElementById('btn-pwa-install-dismiss'); window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPwaPrompt = e; if (window.innerWidth >= 768 && banner && !sessionStorage.getItem('sismosfera_pwa_dismissed')) { setTimeout(() => banner.classList.remove('hidden'), 3500); } }); if (btnAction) { btnAction.onclick = () => { if (!deferredPwaPrompt) return; deferredPwaPrompt.prompt(); deferredPwaPrompt.userChoice.then((choice) => { if (choice.outcome === 'accepted') { console.log('[PWA] Instalación aceptada por el usuario'); if (typeof trackGCOEvent === 'function') trackGCOEvent('pwa_install_completed'); } deferredPwaPrompt = null; if (banner) banner.classList.add('hidden'); }); }; } if (btnDismiss) { btnDismiss.onclick = () => { if (banner) banner.classList.add('hidden'); sessionStorage.setItem('sismosfera_pwa_dismissed', 'true'); }; } window.addEventListener('appinstalled', () => { console.log('[PWA] Sismosfera App instalada en el sistema.'); if (banner) banner.classList.add('hidden'); if (typeof showToast === 'function') showToast('📲 Sismosfera App instalada correctamente.'); }); } async function initWebPushClient() { const btnToggle = document.getElementById('btn-toggle-web-push'); const badge = document.getElementById('push-status-badge'); if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) { if (btnToggle) { btnToggle.disabled = true; btnToggle.innerText = 'NO SOPORTADO'; btnToggle.classList.add('opacity-40', 'cursor-not-allowed'); } if (badge) badge.innerText = 'NO COMPATIBLE'; wantWebPush = false; return; } try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub && Notification.permission === 'granted') { pushSubscriptionActive = true; wantWebPush = true; updatePushUiState(true); } else if (Notification.permission === 'denied') { pushSubscriptionActive = false; wantWebPush = false; updatePushUiState(false); } else { // Permiso 'default' -> ACTIVADO POR DEFECTO para el usuario (Fase 82.1) pushSubscriptionActive = false; wantWebPush = true; updatePushUiState(true); } } catch (err) { console.warn('[PUSH INIT ERROR]', err); if (typeof Notification !== 'undefined' && Notification.permission !== 'denied') { wantWebPush = true; updatePushUiState(true); } } if (btnToggle) { btnToggle.onclick = handleToggleWebPush; } } function updatePushUiState(isActive) { const btnToggle = document.getElementById('btn-toggle-web-push'); const badge = document.getElementById('push-status-badge'); if (!btnToggle || !badge) return; if (isActive) { badge.innerText = 'ACTIVO (M5.0+)'; badge.className = 'px-2 py-0.5 rounded text-[8px] font-black bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 uppercase'; btnToggle.innerText = 'Desactivar'; btnToggle.className = 'px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-rose-900/60 text-slate-300 hover:text-rose-200 border border-slate-700/60 text-xs font-bold transition-all cursor-pointer flex-shrink-0'; } else { const isBlocked = (typeof Notification !== 'undefined' && Notification.permission === 'denied'); badge.innerText = isBlocked ? 'BLOQUEADO' : 'DESACTIVADO'; badge.className = 'px-2 py-0.5 rounded text-[8px] font-black bg-slate-800 text-slate-400 border border-slate-700/60 uppercase'; btnToggle.innerText = isBlocked ? 'Desbloquear' : 'Activar'; btnToggle.className = 'px-3 py-1.5 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-black transition-all cursor-pointer flex-shrink-0'; } } async function triggerWebPushSubscription(isUserInitiated = false) { if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return false; try { const permission = await Notification.requestPermission(); if (permission !== 'granted') { wantWebPush = false; updatePushUiState(false); if (isUserInitiated && typeof showToast === 'function') showToast('⚠️ Permiso de notificaciones denegado.'); return false; } const keyRes = await fetch('/api/push-public-key'); const keyData = await keyRes.json(); if (!keyData.success || !keyData.publicKey) { throw new Error('No se pudo obtener la clave VAPID'); } const applicationServerKey = urlBase64ToUint8Array(keyData.publicKey); const reg = await navigator.serviceWorker.ready; let sub = await reg.pushManager.getSubscription(); if (!sub) { sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }); } const regRes = await fetch('/api/push-subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subscription: sub, minMag: 5.0, sendWelcome: isUserInitiated }) }); const regData = await regRes.json(); if (regData.success) { pushSubscriptionActive = true; wantWebPush = true; updatePushUiState(true); if (typeof trackGCOEvent === 'function') trackGCOEvent('push_notifications_enabled'); if (isUserInitiated && typeof showToast === 'function') { showToast('🔔 ¡Alertas Push activadas! Recibirás avisos ante sismos M5.0+'); } return true; } } catch (err) { console.warn('[PUSH SUBSCRIBE ERROR]', err); if (isUserInitiated) { alert('No se pudo completar la suscripción Web Push: ' + err.message); } } return false; } window.triggerWebPushSubscription = triggerWebPushSubscription; async function handleToggleWebPush() { const btnToggle = document.getElementById('btn-toggle-web-push'); // Si actualmente está activo (ya sea por push real o por preferencia preactivada), el clic en "Desactivar" lo apaga if (pushSubscriptionActive || wantWebPush) { wantWebPush = false; if (pushSubscriptionActive) { try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { await fetch('/api/push-unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: sub.endpoint }) }); await sub.unsubscribe(); } } catch (err) { console.warn('[PUSH UNSUB WARN]', err); } pushSubscriptionActive = false; } updatePushUiState(false); if (typeof showToast === 'function') showToast('🔕 Notificaciones Web Push desactivadas en este dispositivo.'); return; } // Si está desactivado, el clic en "Activar" solicita permiso e inicia la suscripción if (typeof Notification !== 'undefined' && Notification.permission === 'denied') { alert('Las notificaciones están bloqueadas en la configuración de tu navegador. Para activarlas, haz clic en el icono del candado en la barra de direcciones y permite las notificaciones para Sismosfera.'); return; } wantWebPush = true; if (btnToggle) { btnToggle.disabled = true; btnToggle.innerText = 'Conectando...'; } await triggerWebPushSubscription(true); if (btnToggle) btnToggle.disabled = false; } // ============================================================================== // CAPTADOR INBOUND TÁCTICO: BANNER WEB PUSH 1-CLIC (FASE 291) // ============================================================================== function initQuickPushBanner() { if (window.innerWidth < 768) return; // En móvil se prioriza Vista Dividida limpia y el botón Alertas del dock if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return; if (Notification.permission !== 'default') return; // Ya concedió o bloqueó if (sessionStorage.getItem('push_banner_dismissed') === 'true') return; if (localStorage.getItem('push_subscribed_ok') === 'true') return; // Mostrar tras 4.5 segundos de navegación fluida setTimeout(() => { const banner = document.getElementById('banner-quick-push'); if (banner && Notification.permission === 'default' && sessionStorage.getItem('push_banner_dismissed') !== 'true') { banner.classList.remove('hidden'); } }, 4500); } window.handleQuickPushAccept = async function() { const banner = document.getElementById('banner-quick-push'); if (banner) banner.classList.add('hidden'); if (typeof triggerWebPushSubscription === 'function') { const ok = await triggerWebPushSubscription(true); if (ok) { localStorage.setItem('push_subscribed_ok', 'true'); } } }; window.handleQuickPushDismiss = function() { const banner = document.getElementById('banner-quick-push'); if (banner) banner.classList.add('hidden'); sessionStorage.setItem('push_banner_dismissed', 'true'); }; // Inicializaciones directas (ya estamos dentro del bloque maestro DOMContentLoaded) initPwaInstallExperience(); initWebPushClient(); initQuickPushBanner(); // ======================================================================= // SALA DE PRENSA DIGITAL, PRESS KIT & MODO EMBEBIDO (FASE 64.0) // ======================================================================= window.openPressKitModal = function() { const modal = document.getElementById('modal-press-kit'); if (modal) modal.classList.remove('hidden'); if (typeof trackGCOEvent === 'function') trackGCOEvent('press_kit_opened'); }; window.openBrandKitModal = function() { window.openPressKitModal(); requestAnimationFrame(() => { setTimeout(() => { const brandSec = document.getElementById('brand-kit-section'); const scrollBody = document.getElementById('press-kit-scroll-body'); if (brandSec && scrollBody) { scrollBody.scrollTo({ top: brandSec.offsetTop - 15, behavior: 'smooth' }); } else if (brandSec) { brandSec.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, 150); }); if (typeof trackGCOEvent === 'function') trackGCOEvent('brand_kit_opened'); }; window.closePressKitModal = function() { const modal = document.getElementById('modal-press-kit'); if (modal) modal.classList.add('hidden'); }; // Auto-abrir Brand Kit si la URL incluye hash #brand, #branding o #recursos-marca { const h = (window.location.hash || '').toLowerCase(); if (h === '#brand' || h === '#branding' || h === '#brandkit' || h === '#recursos-marca') { setTimeout(window.openBrandKitModal, 600); } } window.copyEmbedCode = function() { const code = document.getElementById('embed-code-block')?.innerText; if (code) { navigator.clipboard.writeText(code).then(() => { if (typeof showToast === 'function') showToast('✅ Código de Widget embebible copiado al portapapeles.'); if (typeof trackGCOEvent === 'function') trackGCOEvent('embed_code_copied'); }); } }; window.copyBoilerplateShort = function() { const text = document.getElementById('bp-short')?.innerText?.replace(/^"|"$/g, ''); if (text) { navigator.clipboard.writeText(text).then(() => { if (typeof showToast === 'function') showToast('✅ Boilerplate periodístico copiado.'); }); } }; window.copyCitation = function() { const text = document.getElementById('bp-citation')?.innerText; if (text) { navigator.clipboard.writeText(text).then(() => { if (typeof showToast === 'function') showToast('✅ Formato de citación oficial copiado.'); }); } }; // Detección de Modo Embebido (?embed=true, ?embed=map, ?embed=compact, etc.) function initEmbedModeDetection() { const urlParams = new URLSearchParams(window.location.search); const embedVal = urlParams.get('embed'); const isEmbed = embedVal === 'true' || embedVal === 'map' || embedVal === 'compact' || embedVal === 'globe'; const isMap = embedVal === 'map'; const isCompact = embedVal === 'compact'; const isPressKit = urlParams.get('view') === 'press-kit' || window.location.hash === '#prensa'; // Soporte Google Sitelinks Searchbox (?q= o ?search=) const searchQuery = (urlParams.get('q') || urlParams.get('search') || '').trim(); if (searchQuery) { setTimeout(() => { const searchInput = document.getElementById('filter-search'); if (searchInput) { searchInput.value = searchQuery; searchInput.dispatchEvent(new Event('input', { bubbles: true })); } }, 1200); } if (isEmbed) { document.body.classList.add('embed-mode'); // Ocultar elementos no esenciales para el widget de noticias const nav = document.getElementById('main-tactical-navbar'); if (nav) nav.style.display = 'none'; const consentBanner = document.getElementById('cookie-consent-banner'); if (consentBanner) consentBanner.style.display = 'none'; const pwaBanner = document.getElementById('pwa-install-banner'); if (pwaBanner) pwaBanner.style.display = 'none'; const top3Dock = document.getElementById('dock-top3-ranking'); if (top3Dock) top3Dock.style.display = 'none'; // Agregar distintivo sutil de marca en el viewport let badgeLabel = '🔴 SISMOSFERA 3D'; if (isMap) badgeLabel = '🗺️ SISMOSFERA MAPA'; else if (isCompact) badgeLabel = '📊 SISMOSFERA WIDGET'; const badge = document.createElement('a'); badge.href = 'https://sismosfera.com/'; badge.target = '_blank'; badge.rel = 'noopener'; badge.className = 'fixed top-3 right-3 z-40 px-2.5 py-1 rounded-lg bg-slate-950/80 border border-sky-500/40 text-sky-300 font-mono text-[10px] font-black tracking-wider shadow-lg backdrop-blur-md hover:bg-slate-900 transition-all'; badge.innerText = badgeLabel; document.body.appendChild(badge); // Identificar herramienta para el sensor editorial let toolType = 'globe_3d'; if (isMap) toolType = 'map_2d'; else if (isCompact) toolType = 'widget_compact'; const quakeId = urlParams.get('quake') || (window.location.hash || '').replace('#quake-', ''); // Sensor autónomo de adopción editorial (Ping de Referrer para Media Clipping NOC) if (document.referrer) { try { fetch('/api/media-clipping/ping', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ referrer: document.referrer, quakeId: quakeId, tool: toolType, quakeTitle: document.title || 'Monitoreo Sísmico Global' }) }).catch(() => {}); } catch(e) {} } } if (isPressKit) { setTimeout(openPressKitModal, 600); } } initEmbedModeDetection(); window.openLegalTermsModal = function(clauseId) { const modal = document.getElementById('modal-legal-terms'); if (modal) { modal.classList.remove('hidden'); if (clauseId) { setTimeout(() => { const el = document.getElementById('modal-clause-' + clauseId); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, 60); } } }; window.closeLegalTermsModal = function() { const modal = document.getElementById('modal-legal-terms'); if (modal) modal.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') { updateSismosferaDockVisualState('globe'); } }; window.acceptLegalTerms = function() { try { localStorage.setItem('terremoto_legal_accepted_v1', 'true'); } catch (e) { console.warn('[Legal] localStorage no accesible:', e); } closeLegalTermsModal(); if (typeof showToast === 'function') { showToast('Términos de uso y descargo legal aceptados.', 'info'); } }; document.addEventListener('keydown', (e) => { if (e.key === 'Escape' || e.key === 'Esc') { if (window.scrollY > 120 && typeof closeEditorialPortalModal === 'function') { closeEditorialPortalModal(); } const detailsModal = document.getElementById('modal-event-details'); if (detailsModal && !detailsModal.classList.contains('hidden')) { if (typeof handleCloseEventDetails === 'function') { handleCloseEventDetails(); } else { detailsModal.classList.add('hidden'); } } const legalModal = document.getElementById('modal-legal-terms'); if (legalModal && !legalModal.classList.contains('hidden')) { closeLegalTermsModal(); } const pressWireModal = document.getElementById('modal-press-wire'); if (pressWireModal && !pressWireModal.classList.contains('hidden')) { closePressWireModal(); } } }); window.addEventListener('popstate', () => { // 1. Modal de Detalles de Evento (Bottom Sheet) const detailsModal = document.getElementById('modal-event-details'); if (detailsModal && !detailsModal.classList.contains('hidden')) { if (typeof handleCloseEventDetails === 'function') { handleCloseEventDetails(); } else { detailsModal.classList.add('hidden'); } return; } // 2. Modal Ciencia Ciudadana «¡Lo Sentí!» (DYFI Bottom Sheet) const feltModal = document.getElementById('modal-felt-it'); if (feltModal && !feltModal.classList.contains('hidden')) { feltModal.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 3. Modal de Alertas y Notificaciones Push const alertModal = document.getElementById('modal-alert'); if (alertModal && !alertModal.classList.contains('hidden')) { alertModal.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 4. Modal Legal / Términos const legalModal = document.getElementById('modal-legal-terms'); if (legalModal && !legalModal.classList.contains('hidden')) { legalModal.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 5. Contenedor de Búsqueda Geográfica Móvil const searchContainer = document.getElementById('geo-search-container'); if (searchContainer && !searchContainer.classList.contains('hidden')) { searchContainer.classList.add('hidden'); searchContainer.classList.remove('flex'); const panel = document.getElementById('geo-search-results-panel'); if (panel) panel.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 6. Menú de Capas const layerMenu = document.getElementById('layer-menu'); if (layerMenu && !layerMenu.classList.contains('hidden')) { layerMenu.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 7. Menú de Herramientas Científicas const scienceMenu = document.getElementById('dropdown-menu-science'); if (scienceMenu && !scienceMenu.classList.contains('hidden')) { scienceMenu.classList.add('hidden'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 8. Cualquier otro modal genérico visible const activeModals = document.querySelectorAll('.fixed[id^="modal-"]:not(.hidden)'); if (activeModals && activeModals.length > 0) { activeModals.forEach(m => m.classList.add('hidden')); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); return; } // 9. Drawer / Sidebar de Sismos en vista Móvil (< 1024px) if (window.innerWidth < 1024) { const sb = document.getElementById('left-sidebar'); if (sb && !sb.classList.contains('hidden') && !sb.classList.contains('opacity-0') && !sb.classList.contains('-translate-x-[calc(100%+32px)]')) { sb.classList.add('hidden', '-translate-x-[calc(100%+32px)]', 'opacity-0', 'pointer-events-none'); if (typeof updateSismosferaDockVisualState === 'function') updateSismosferaDockVisualState('globe'); if (typeof updateMobileLayout === 'function') updateMobileLayout(false); return; } } }); initPwaAndOfflineHandlers(); });
📖

Guía & Mochila 72h

🌐 Misión Institucional • Sismosfera 3D

Sismosfera es una plataforma científica e independiente de servicio público orientada a la observación, análisis e investigación de la sismicidad global en tiempo real. Integramos telemetría instrumental certificada de las principales redes sismográficas del planeta con el objetivo de brindar información objetiva, veraz e inmediata a la comunidad científica, organismos de respuesta civil, medios periodísticos y a la población general expuesta al riesgo geológico.

🔬

1. Metodología de Telemetría Sísmica & Redes de Sensores Oficiales

La veracidad de los datos es el pilar sagrado de nuestra infraestructura. Sismosfera procesa exclusivamente telemetría instrumental en bruto y soluciones preliminares automáticas emitidas por las redes sismológicas de mayor solvencia internacional bajo el protocolo FDSN (International Federation of Digital Seismograph Networks).

USGS • United States Geological Survey GSN / NEIC

Red sismográfica global de cobertura intercontinental. Determinación hipocentral de alta precisión, tensor de momento sísmico (W-phase moment tensor) y modelos analíticos de impacto demográfico PAGER.

EMSC • Centre Sismologique Euro-Méditerranéen 85+ INSTITUTOS

Consorcio de más de 85 institutos geofísicos nacionales en Europa, el Mediterráneo y Asia Central. Detección temprana transfronteriza y recopilación de macro-sismicidad en tiempo real.

IGN • Instituto Geográfico Nacional de España RSN ESPAÑA

Supervisión sísmica y volcánica integral de la Península Ibérica, el Mar de Alborán y las Islas Canarias, con calibración local en la falla transformante Azores-Gibraltar.

CSN • Centro Sismológico Nacional (Chile) FOSA DE NAZCA

Monitoreo sismológico de alta densidad en el margen de subducción más activo y veloz del planeta, donde converge la Placa de Nazca bajo la Placa Sudamericana a más de 66 mm/año.

Modelos Físicos & Formulación Analítica: Sismosfera incorpora el modelo canónico de velocidades elásticas terrestres ak135 para calcular los tiempos teóricos de viaje de las ondas compresionales P y de cizalla S. Además, implementa la ley empírica de Gutenberg-Richter para analizar el parámetro de valor b en enjambres sísmicos y la ley de Omori-Utsu para evaluar el decaimiento temporal de réplicas.
⚡

2. Física de los Terremotos: ¿Por qué Magnitud no es lo mismo que Intensidad?

Uno de los errores más frecuentes en la comunicación pública de emergencias es confundir la magnitud de un sismo con su intensidad en una localidad particular:

Magnitud de Momento (Mw) • Energía Liberada

La magnitud es un valor único e invariable para cada terremoto. Mide cuantitativamente la energía elástica total liberada durante la ruptura de la falla geológica. Se calcula multiplicando el módulo de rigidez de la roca por el área de la superficie de falla y por el desplazamiento medio registrado:

M₀ = μ • A • D  ➔  Mw = ⅔ log₁₀(M₀) - 10.7

Al tratarse de una escala logarítmica de base 10 con exponente 1.5, cada salto de 1 unidad de magnitud representa aproximadamente 31.6 veces más energía física liberada (y un salto de 2 unidades equivale a 1.000 veces más energía).

Escala de Mercalli Modificada (MMI) • Efecto Sentido

A diferencia de la magnitud, la intensidad sísmica no es única: cambia según la ubicación geográfica del observador, la distancia al epicentro, la profundidad del hipocentro y las condiciones del suelo local (efecto de sitio y amplificación por suelos blandos o aluviales).

I (Imperceptible) ➔ V (Moderado) ➔ VIII (Destructivo) ➔ XII (Catastrófico)

Un sismo superficial de magnitud moderada ($M_w 5.3$) a solo 4 km de profundidad bajo una ciudad puede generar intensidades MMI VII-VIII y graves derrumbes, mientras que un sismo potente de $M_w 7.5$ a 550 km de profundidad en el manto terrestre puede percibirse apenas como MMI III sin provocar daños.

🌋

3. Tectónica Global: El Cinturón de Fuego del Pacífico & Zonas de Subducción

La litosfera terrestre está fragmentada en más de una docena de placas tectónicas mayores que se desplazan sobre la astenosfera viscosa. Más del 81% de los terremotos más potentes del planeta y el 90% de la energía sísmica mundial se concentran en el Cinturón de Fuego del Pacífico (Circum-Pacific Belt), una franja en forma de herradura de 40.000 kilómetros de longitud que rodea la cuenca oceánica del Pacífico.

Límites Convergentes (Subducción)

Una placa oceánica densa se hunde bajo una placa continental o corteza insular (Fosa Chilena, Fosa de Japón, Cascadia). Generan los megaterremotos más devastadores y tsunamis transoceánicos.

Fallas Transformantes (Cizalla)

Dos bloques tectónicos se desplazan lateralmente en fricción horizontal (Falla de San Andrés en California, Falla de Anatolia Septentrional en Turquía). Producen sismos muy superficiales y violentos.

Límites Divergentes (Dorsales)

Las placas se separan permitiendo el ascenso de magma y la formación de nueva corteza oceánica (Dorsal Mesoatlántica en Islandia, Rift de África Oriental). Sismicidad somera asociada a volcanismo.

🚨

Mochila 72h & Autoprotección

🎒 Checklist Mochila 72h
📊 Tu Mochila 72h: 0 de 8 elementos listos (0%) — Marca con [✓] los que ya tienes en casa

Marca los elementos que ya tienes en casa; en los artículos que te falten (1 al 6), al tocar el botón de recomendación te derivamos a Amazon para ver opciones y adquirirlos:

🎒 1. Mochila 72h Equipada (4 Pers.)
Kit base con raciones, mantas térmicas, agua y herramientas de evacuación.
📻 2. Radio Solar + Dínamo + USB
Recibe boletines oficiales y carga tu teléfono a manivela sin electricidad.
💧 3. Agua (3L/día) + Filtro Portátil
Potabiliza agua al instante si se corta o contamina el suministro municipal.
🩹 4. Botiquín Primeros Auxilios IFAK
Vendajes, gasas, antisépticos y manta térmica para atención inmediata.
🔦 5. Linterna LED + Silbato 120 dB
Iluminación táctica de alta autonomía y silbato de localización para rescate.
🔩 6. Correas de Anclaje Antisísmico
Fija televisores, bibliotecas y muebles altos para evitar vuelcos en casa.
💊 7. Medicación Personal y Recetas
Reserva mínima para 7 días de tratamientos crónicos, lentes y copias de recetas.
🏠 Preparar en casa
📄 8. DNI, Llaves y Efectivo en Bolsa
Copias de documentos, duplicado de llaves y efectivo en bolsa hermética.
🏠 Preparar en casa
💡 Transparencia: Al tocar los artículos 1 al 6 te derivamos a nuestra selección recomendada en Amazon. Como Afiliado de Amazon, obtenemos ingresos por las compras adscritas que cumplen los requisitos aplicables.

Los terremotos no se pueden predecir con exactitud determinista, pero sus consecuencias se pueden mitigar radicalmente mediante la educación, el diseño estructural antisísmico y la preparación personal. Conoce las directrices universales homologadas por las agencias internacionales de defensa civil:

A. Qué hacer ANTES de un terremoto (Prevención y Planificación)

  • Identificación de peligros no estructurales: Asegurar con escuadras metálicas y anclajes muebles altos, estanterías, bibliotecas, televisores y calefones de agua.
  • Punto de encuentro y contacto foráneo: Designar un lugar seguro de reunión familiar fuera de edificaciones y acordar el número de un familiar que resida en otra provincia o estado (las redes telefónicas locales se saturan tras un evento severo).
  • Localización de válvulas: Aprender y enseñar a todos los miembros del hogar cómo cortar rápidamente el suministro de gas natural, agua corriente y electricidad.

B. Qué hacer DURANTE un terremoto (Protocolo Universal de Supervivencia)

🧎 1. AGACHARSE Tírate al suelo sobre manos y rodillas antes de que la aceleración te derribe.
🛡️ 2. CUBRIRSE Métete bajo una mesa o escritorio firme y cubre tu cabeza y cuello con los brazos.
✊ 3. SUJETARSE Agárrate con fuerza de la estructura protectora hasta que cesen las sacudidas.
  • Si estás en un edificio: NUNCA uses los ascensores. Aléjate de ventanas, cristales, espejos y fachadas externas. No intentes correr hacia la calle durante la sacudida violenta.
  • Si estás en la vía pública: Aléjate de tendidos eléctricos de media/alta tensión, postes de alumbrado, carteles publicitarios y muros de mampostería.
  • Si estás conduciendo: Disminuye la velocidad gradualmente, oríllate en una zona libre de cables o puentes elevados, pon las luces de baliza y permanece dentro del vehículo.

C. Qué hacer DESPUÉS de un terremoto

  • Verificar fugas de gas: No enciendas fósforos, velas ni interruptores eléctricos hasta confirmar que no existe olor a gas combustible.
  • Evacuar por escaleras: Inspeccionar posibles grietas en escaleras antes de utilizarlas y dirigirse con calma hacia áreas despejadas.
  • Prepararse para réplicas: Tras un sismo mayor se desencadenan réplicas que pueden colapsar estructuras ya resentidas.
  • Alerta de Tsunami en zonas costeras: Si te encuentras cerca de la costa y el temblor te impidió mantenerte de pie o duró más de 20 segundos, o si observas un retiro abrupto de la marea, evacúa inmediatamente a pie hacia cotas superiores a 30 metros de altitud sin esperar avisos sirenas oficiales.
❓

5. Preguntas Frecuentes sobre Terremotos (FAQ)

¿Se pueden predecir los terremotos con día y hora exacta?

No. Ninguna institución científica ni sismólogo en el mundo tiene la capacidad tecnológica de predecir la hora, día o lugar exacto de un terremoto futuro. La sismología trabaja con pronósticos probabilísticos de amenaza y mapas de peligrosidad a mediano y largo plazo, no con predicciones deterministas a corto plazo. Toda persona que afirme predecir terremotos carece de sustento científico.

¿Qué es un enjambre sísmico y en qué se diferencia de una réplica?

Un enjambre sísmico es una secuencia de numerosos eventos telúricos concentrados en una región espacial y temporal determinada, sin que exista un evento principal de magnitud desproporcionadamente mayor. Suelen estar vinculados a movimiento de fluidos hidrotermales o ascenso de magma. Una réplica, en cambio, es un sismo posterior de menor magnitud que ocurre sobre la misma área de falla donde se produjo el terremoto principal (mainshock).

¿Qué es el reporte ciudadano "¿Lo sentiste?" (DYFI)?

El sistema Did You Feel It? (DYFI) permite a cualquier ciudadano que haya percibido una sacudida telúrica reportar sus observaciones (si los objetos vibraron, si despertó personas o provocó grietas). Estos reportes se agregan algorítmicamente para construir mapas de intensidad comunitaria en tiempo real que complementan la red instrumental de acelerógrafos.

🌎

6. Directorio Internacional de Monitoreo Sísmico por Países

Accede a las páginas de monitoreo regional especializado con telemetría geofísica calibrada, fallas activas y sismicidad histórica para cada nación:

Sismosfera 3D