// pg4-debug.js - Modo debug e calibração visual para pg4
// Renderiza o campo KDE real sobre a matriz + marcadores de picos + painel de sliders.
// Independente de pg4.js: reimplementa o pipeline com parâmetros mutáveis.
(function () {
'use strict';
// =========================================================================
// CONFIG INICIAL
// =========================================================================
const DEBUG_CONFIG = {
bandwidth: 10,
massMin: 0.15,
dominanceRatio: 0.50,
prominenceMin: 0.25,
maxPGs: 3,
minSep: 18
};
const GRID_SIZE = 100;
const DEBUG_CANVAS_SIZE = 490;
const DEBUG_CANVAS_TOP = '43%';
const DEBUG_CANVAS_LEFT = '50%';
const DEBUG_CANVAS_TRANSFORM = 'translate(-50%, -50%)';
// =========================================================================
// ESTADO
// =========================================================================
const debugState = {
active: false,
pontos: [],
grid: null,
maxGlobal: 0,
picosMarcados: [],
pGsFinais: [],
canvas: null,
panel: null
};
// =========================================================================
// BOOTSTRAP
// =========================================================================
console.log('pg4-debug: script carregado');
function init() {
injectStyles();
tentarInjetarBotao(10);
}
function tentarInjetarBotao(tentativas) {
if (document.getElementById('pg4DebugBtn')) return;
const sucesso = injectButton();
if (!sucesso && tentativas > 0) {
setTimeout(() => tentarInjetarBotao(tentativas - 1), 400);
}
}
function injectButton() {
const btn = document.createElement('button');
btn.className = 'button';
btn.id = 'pg4DebugBtn';
btn.title = 'Debug KDE (pg4)';
btn.innerHTML = '';
btn.onclick = toggleDebug;
const inputArea = document.getElementById('inputArea');
if (inputArea) {
const containers = inputArea.querySelectorAll('.button-container');
if (containers.length > 0) {
const novo = document.createElement('div');
novo.className = 'button-container';
novo.appendChild(btn);
const ultimo = containers[containers.length - 1];
ultimo.parentNode.insertBefore(novo, ultimo.nextSibling);
console.log('pg4-debug: botão injetado no inputArea');
return true;
}
}
btn.style.position = 'fixed';
btn.style.bottom = '20px';
btn.style.left = '20px';
btn.style.zIndex = '500';
btn.style.backgroundColor = 'rgba(30, 20, 70, 0.95)';
btn.style.borderColor = '#ff0066';
document.body.appendChild(btn);
console.log('pg4-debug: botão injetado como fallback fixo');
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// =========================================================================
// ABRIR / FECHAR
// =========================================================================
function toggleDebug() {
if (debugState.active) {
fecharDebug();
} else {
abrirDebug();
}
}
function abrirDebug() {
const hidden = document.getElementById('hiddenCoordinates');
if (!hidden) {
alert("pg4-debug: campo 'hiddenCoordinates' não encontrado.");
return;
}
debugState.pontos = extrairPontos(hidden.value);
if (debugState.pontos.length < 5) {
alert('pg4-debug: pontos insuficientes para análise (mínimo 5).');
return;
}
criarCanvas();
criarPainel();
rodarAnalise();
debugState.active = true;
}
function fecharDebug() {
const antigoCanvas = document.getElementById('pg4DebugCanvas');
const antigoPainel = document.getElementById('pg4DebugPanel');
if (antigoCanvas) antigoCanvas.remove();
if (antigoPainel) antigoPainel.remove();
debugState.canvas = null;
debugState.panel = null;
debugState.active = false;
}
function criarCanvas() {
const matrix = document.getElementById('matrix');
if (!matrix) return;
const antigo = document.getElementById('pg4DebugCanvas');
if (antigo) antigo.remove();
const canvas = document.createElement('canvas');
canvas.id = 'pg4DebugCanvas';
canvas.width = DEBUG_CANVAS_SIZE;
canvas.height = DEBUG_CANVAS_SIZE;
canvas.style.position = 'absolute';
canvas.style.top = DEBUG_CANVAS_TOP;
canvas.style.left = DEBUG_CANVAS_LEFT;
canvas.style.transform = DEBUG_CANVAS_TRANSFORM;
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '50';
matrix.appendChild(canvas);
debugState.canvas = canvas;
}
// =========================================================================
// PAINEL DE CONTROLES
// =========================================================================
function criarPainel() {
const antigo = document.getElementById('pg4DebugPanel');
if (antigo) antigo.remove();
const panel = document.createElement('div');
panel.id = 'pg4DebugPanel';
panel.innerHTML = `
${sliderHtml('bw', 'BANDWIDTH', 5, 20, 0.5, DEBUG_CONFIG.bandwidth, 'Largura do kernel gaussiano.')}
${sliderHtml('mm', 'MASS_MIN', 0.05, 0.40, 0.01, DEBUG_CONFIG.massMin, 'Massa mínima do pG primário.')}
${sliderHtml('dr', 'DOMINANCE', 0.20, 0.90, 0.05, DEBUG_CONFIG.dominanceRatio, 'Fração da massa do primário para secundários.')}
${sliderHtml('pm', 'PROMINENCE', 0.05, 0.60, 0.05, DEBUG_CONFIG.prominenceMin, 'Proeminência mínima do pico.')}
pG aprovado
candidato (filtrado)
pico sem proeminência
`;
document.body.appendChild(panel);
debugState.panel = panel;
panel.querySelector('.pg4d-close').addEventListener('click', fecharDebug);
panel.querySelector('#pg4d-copy').addEventListener('click', copiarConfig);
['bw', 'mm', 'dr', 'pm'].forEach(k => {
const input = document.getElementById(`pg4d-${k}`);
const val = document.getElementById(`pg4d-${k}-val`);
input.addEventListener('input', () => {
val.textContent = input.value;
onParamChange();
});
});
}
function sliderHtml(key, label, min, max, step, val, tip) {
return `
`;
}
function onParamChange() {
const bwNew = parseFloat(document.getElementById('pg4d-bw').value);
const bwChanged = bwNew !== DEBUG_CONFIG.bandwidth;
DEBUG_CONFIG.bandwidth = bwNew;
DEBUG_CONFIG.massMin = parseFloat(document.getElementById('pg4d-mm').value);
DEBUG_CONFIG.dominanceRatio = parseFloat(document.getElementById('pg4d-dr').value);
DEBUG_CONFIG.prominenceMin = parseFloat(document.getElementById('pg4d-pm').value);
if (bwChanged) {
rodarAnalise();
} else {
debugState.picosMarcados = enriquecer(
detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth),
debugState.pontos,
debugState.grid,
DEBUG_CONFIG.bandwidth
);
const sel = selecionar(debugState.picosMarcados, DEBUG_CONFIG);
debugState.picosMarcados = sel.marcados;
debugState.pGsFinais = sel.selecionados;
renderizar();
atualizarStats();
}
}
// =========================================================================
// PIPELINE
// =========================================================================
function rodarAnalise() {
debugState.grid = construirGrid(debugState.pontos, DEBUG_CONFIG.bandwidth);
debugState.maxGlobal = maximoGlobal(debugState.grid);
const picos = detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth);
const enriquecidos = enriquecer(picos, debugState.pontos, debugState.grid, DEBUG_CONFIG.bandwidth);
const sel = selecionar(enriquecidos, DEBUG_CONFIG);
debugState.picosMarcados = sel.marcados;
debugState.pGsFinais = sel.selecionados;
renderizar();
atualizarStats();
}
function extrairPontos(input) {
return input.split('\n').map(linha => {
if (/^[a-zA-Z]+[a-zA-Z0-9_-]*-/.test(linha)) return null;
const pura = linha.split('>')[0].split('(r=')[0];
const partes = pura.split(',');
if (partes.length === 4 && partes.every(p => !isNaN(parseFloat(p)))) {
return {
x: parseFloat(partes[3]),
y: parseFloat(partes[1])
};
}
return null;
}).filter(p => p !== null);
}
function construirGrid(pontos, bw) {
const grid = Array.from({ length: GRID_SIZE }, () => new Float64Array(GRID_SIZE));
const h2 = 2 * bw * bw;
const raio = Math.ceil(bw * 3);
pontos.forEach(p => {
const xMin = Math.max(0, Math.floor(p.x - raio));
const xMax = Math.min(GRID_SIZE - 1, Math.ceil(p.x + raio));
const yMin = Math.max(0, Math.floor(p.y - raio));
const yMax = Math.min(GRID_SIZE - 1, Math.ceil(p.y + raio));
for (let i = xMin; i <= xMax; i++) {
for (let j = yMin; j <= yMax; j++) {
const dx = i - p.x;
const dy = j - p.y;
grid[i][j] += Math.exp(-(dx * dx + dy * dy) / h2);
}
}
});
return grid;
}
function maximoGlobal(grid) {
let max = 0;
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] > max) max = grid[i][j];
}
}
return max;
}
function detectarPicos(grid, bw) {
const picos = [];
const raio = Math.max(2, Math.floor(bw / 2));
const threshold = maximoGlobal(grid) * 0.05;
for (let i = raio; i < GRID_SIZE - raio; i++) {
for (let j = raio; j < GRID_SIZE - raio; j++) {
const v = grid[i][j];
if (v < threshold) continue;
let ehMax = true;
for (let di = -raio; di <= raio && ehMax; di++) {
for (let dj = -raio; dj <= raio && ehMax; dj++) {
if (di === 0 && dj === 0) continue;
if (grid[i + di][j + dj] > v) ehMax = false;
}
}
if (ehMax) picos.push({ x: i, y: j, densidade: v });
}
}
return picos;
}
function enriquecer(picos, pontos, grid, bw) {
const total = pontos.length;
const raioMassa = bw * 1.5;
return picos.map(pico => {
const massa = pontos.filter(p =>
Math.sqrt((p.x - pico.x) ** 2 + (p.y - pico.y) ** 2) <= raioMassa
).length / total;
const raio = Math.ceil(bw * 2);
let vale = pico.densidade;
for (let di = -raio; di <= raio; di++) {
for (let dj = -raio; dj <= raio; dj++) {
const i = pico.x + di;
const j = pico.y + dj;
if (i < 0 || i >= GRID_SIZE || j < 0 || j >= GRID_SIZE) continue;
if (di * di + dj * dj > raio * raio) continue;
if (grid[i][j] < vale) vale = grid[i][j];
}
}
const prom = (pico.densidade - vale) / pico.densidade;
return { ...pico, massa, proeminencia: prom };
});
}
function selecionar(picos, cfg) {
const marcados = picos.map(p => ({ ...p, status: null, rank: null }));
marcados.forEach(p => {
if (p.proeminencia < cfg.prominenceMin) p.status = 'sem_proeminencia';
});
const candidatos = marcados
.filter(p => !p.status)
.sort((a, b) => b.massa - a.massa);
if (candidatos.length === 0) {
return { selecionados: [], marcados };
}
if (candidatos[0].massa < cfg.massMin) {
candidatos.forEach(p => { p.status = 'primario_fraco'; });
return { selecionados: [], marcados };
}
const selecionados = [];
const massaPrim = candidatos[0].massa;
candidatos[0].status = 'aprovado';
candidatos[0].rank = 0;
selecionados.push(candidatos[0]);
for (let i = 1; i < candidatos.length; i++) {
const c = candidatos[i];
if (selecionados.length >= cfg.maxPGs) {
c.status = 'teto';
continue;
}
if (c.massa < massaPrim * cfg.dominanceRatio) {
c.status = 'dominancia';
continue;
}
const longe = selecionados.every(s =>
Math.sqrt((s.x - c.x) ** 2 + (s.y - c.y) ** 2) >= cfg.minSep
);
if (!longe) {
c.status = 'separacao';
continue;
}
c.status = 'aprovado';
c.rank = selecionados.length;
selecionados.push(c);
}
return { selecionados, marcados };
}
// =========================================================================
// RENDERIZAÇÃO
// =========================================================================
function renderizar() {
if (!debugState.canvas) return;
const ctx = debugState.canvas.getContext('2d');
const W = debugState.canvas.width;
const H = debugState.canvas.height;
ctx.clearRect(0, 0, W, H);
const grid = debugState.grid;
const maxV = debugState.maxGlobal || 1;
const cellW = W / GRID_SIZE;
const cellH = H / GRID_SIZE;
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
const v = grid[i][j] / maxV;
if (v < 0.05) continue;
const cx = (1 - i / 100) * W;
const cy = (j / 100) * H;
ctx.fillStyle = corParaDensidade(v);
ctx.fillRect(cx - cellW / 2, cy - cellH / 2, cellW + 1, cellH + 1);
}
}
debugState.picosMarcados.forEach(pico => {
const px = (1 - pico.x / 100) * W;
const py = (pico.y / 100) * H;
desenharMarcador(ctx, px, py, pico);
});
}
function desenharMarcador(ctx, x, y, pico) {
let cor, raio, fill;
switch (pico.status) {
case 'aprovado':
cor = '#00ff88';
raio = 13;
fill = true;
break;
case 'sem_proeminencia':
cor = '#888';
raio = 5;
fill = false;
break;
default:
cor = '#ffcc00';
raio = 9;
fill = false;
}
ctx.beginPath();
ctx.arc(x, y, raio, 0, Math.PI * 2);
ctx.strokeStyle = cor;
ctx.lineWidth = 2.5;
ctx.stroke();
if (fill) {
ctx.fillStyle = cor;
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
ctx.fill();
}
const label = `${(pico.massa * 100).toFixed(0)}% · ${(pico.proeminencia * 100).toFixed(0)}%`;
ctx.font = 'bold 10px Arial';
const textW = ctx.measureText(label).width;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(x + raio + 3, y - 7, textW + 6, 13);
ctx.fillStyle = cor;
ctx.fillText(label, x + raio + 6, y + 3);
}
function corParaDensidade(v) {
const stops = [
[0.00, [30, 50, 140, 0]],
[0.15, [30, 100, 200, 80]],
[0.35, [0, 200, 220, 130]],
[0.55, [100, 220, 100, 160]],
[0.75, [255, 200, 50, 180]],
[0.90, [255, 80, 100, 200]],
[1.00, [255, 0, 102, 220]]
];
for (let i = 1; i < stops.length; i++) {
if (v <= stops[i][0]) {
const [v0, c0] = stops[i - 1];
const [v1, c1] = stops[i];
const t = (v - v0) / (v1 - v0);
const r = Math.round(c0[0] + t * (c1[0] - c0[0]));
const g = Math.round(c0[1] + t * (c1[1] - c0[1]));
const b = Math.round(c0[2] + t * (c1[2] - c0[2]));
const a = (c0[3] + t * (c1[3] - c0[3])) / 255;
return `rgba(${r},${g},${b},${a.toFixed(2)})`;
}
}
const c = stops[stops.length - 1][1];
return `rgba(${c[0]},${c[1]},${c[2]},${c[3] / 255})`;
}
// =========================================================================
// ESTATÍSTICAS
// =========================================================================
function atualizarStats() {
const stats = document.getElementById('pg4d-stats');
if (!stats) return;
const pontos = debugState.pontos.length;
const picos = debugState.picosMarcados.length;
const pGs = debugState.pGsFinais.length;
let html = `
Pontos: ${pontos}
Picos brutos: ${picos}
pGs aprovados: ${pGs}
`;
if (pGs === 0 && picos > 0) {
html += `Nenhum pG aprovado (dispersão ou filtros estritos).
`;
}
html += ``;
const sorted = [...debugState.picosMarcados].sort((a, b) => b.massa - a.massa);
sorted.forEach((p, i) => {
const cor = p.status === 'aprovado'
? '#00ff88'
: p.status === 'sem_proeminencia'
? '#888'
: '#ffcc00';
const rotuloStatus = traduzirStatus(p.status);
html += `
pico ${i + 1} @ (${p.x}, ${p.y})
massa: ${(p.massa * 100).toFixed(1)}% · prom: ${(p.proeminencia * 100).toFixed(0)}%
${rotuloStatus}
`;
});
stats.innerHTML = html;
}
function traduzirStatus(s) {
return {
aprovado: '✓ aprovado como pG',
sem_proeminencia: `✗ proeminência < ${(DEBUG_CONFIG.prominenceMin * 100).toFixed(0)}%`,
primario_fraco: `✗ massa < ${(DEBUG_CONFIG.massMin * 100).toFixed(0)}% (dispersão)`,
dominancia: `✗ massa < ${(DEBUG_CONFIG.dominanceRatio * 100).toFixed(0)}% do primário`,
separacao: '✗ próximo demais de outro pG',
teto: `✗ já há ${DEBUG_CONFIG.maxPGs} pGs`
}[s] || '?';
}
// =========================================================================
// AÇÕES
// =========================================================================
function copiarConfig() {
const config = `// Cole em pg4.js substituindo as constantes:
const BANDWIDTH = ${DEBUG_CONFIG.bandwidth};
const MASS_MIN_PRIMARY = ${DEBUG_CONFIG.massMin};
const DOMINANCE_RATIO = ${DEBUG_CONFIG.dominanceRatio};
const PROMINENCE_MIN = ${DEBUG_CONFIG.prominenceMin};`;
navigator.clipboard.writeText(config).then(() => {
const btn = document.getElementById('pg4d-copy');
const orig = btn.textContent;
btn.textContent = 'Copiado!';
setTimeout(() => {
btn.textContent = orig;
}, 1500);
}).catch(() => {
prompt('Copie manualmente:', config);
});
}
// =========================================================================
// ESTILOS
// =========================================================================
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
#pg4DebugPanel {
position: fixed;
top: 60px;
right: 20px;
width: 290px;
max-height: calc(100vh - 80px);
overflow-y: auto;
background: rgba(25, 18, 60, 0.96);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
padding: 14px;
color: white;
font-family: Arial, sans-serif;
font-size: 11px;
z-index: 300;
backdrop-filter: blur(10px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.pg4d-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 12px;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pg4d-close {
background: transparent;
border: none;
color: white;
cursor: pointer;
font-size: 20px;
padding: 0 6px;
line-height: 1;
}
.pg4d-close:hover {
color: #ff0066;
}
.pg4d-sliders {
margin-bottom: 6px;
}
.pg4d-slider {
display: block;
margin-bottom: 12px;
cursor: help;
}
.pg4d-slider-top {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 10px;
}
.pg4d-label {
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.5px;
}
.pg4d-val {
color: #ff0066;
font-weight: bold;
font-family: monospace;
}
.pg4d-slider input[type=range] {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.12);
outline: none;
-webkit-appearance: none;
border-radius: 2px;
}
.pg4d-slider input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #ff0066;
border-radius: 50%;
cursor: pointer;
}
.pg4d-slider input[type=range]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #ff0066;
border-radius: 50%;
cursor: pointer;
border: none;
}
.pg4d-stats {
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.pg4d-row {
margin-bottom: 4px;
font-size: 11px;
}
.pg4d-warn {
color: #ffcc00;
font-style: italic;
margin-top: 6px;
}
.pg4d-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 10px 0;
}
.pg4d-peak {
background: rgba(255, 255, 255, 0.04);
padding: 7px 9px;
margin-bottom: 6px;
border-radius: 5px;
border-left: 3px solid #888;
font-size: 10px;
line-height: 1.4;
}
.pg4d-peak-head {
font-weight: bold;
margin-bottom: 2px;
color: rgba(255,255,255,0.9);
}
.pg4d-status {
margin-top: 3px;
opacity: 0.75;
font-size: 9.5px;
}
.pg4d-legend {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
font-size: 10px;
}
.pg4d-legend > div {
margin: 4px 0;
display: flex;
align-items: center;
}
.pg4d-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
flex-shrink: 0;
}
.pg4d-dot-pg {
background: #00ff88;
border: 2px solid #00ff88;
}
.pg4d-dot-cand {
border: 2px solid #ffcc00;
}
.pg4d-dot-weak {
border: 2px solid #888;
}
.pg4d-footer {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.pg4d-action {
background: rgba(255, 0, 102, 0.15);
border: 1px solid #ff0066;
color: #ff0066;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 10px;
width: 100%;
font-weight: bold;
letter-spacing: 0.5px;
text-transform: uppercase;
transition: background 0.2s;
}
.pg4d-action:hover {
background: rgba(255, 0, 102, 0.3);
}
`;
document.head.appendChild(style);
}
})();