// pg4.js - Detecção de pontos gravitacionais via KDE + análise de picos // Substitui pg3.js (K-Means fixo k=4) por abordagem baseada em densidade real. // // Fluxo: // 1. Constrói um campo de densidade 2D via Kernel Density Estimation (gaussiano) // 2. Identifica máximos locais (picos) nesse campo // 3. Enriquece cada pico com massa real (% de pontos em raio de captura) e proeminência // 4. Aplica portões de existência, dominância, proeminência e separação // 5. Marca de 0 a 3 pGs conforme o que os dados justificam // ========================================================================= // PARÂMETROS AJUSTÁVEIS // ========================================================================= const GRID_SIZE = 100; // resolução do campo (100x100, 1 célula = 1 unidade da matriz) const BANDWIDTH = 10; // largura do kernel gaussiano (em unidades 0-100 da matriz) const MASS_MIN_PRIMARY = 0.15; // massa mínima para existir um pG primário (15% dos respondentes) const DOMINANCE_RATIO = 0.50; // pGs secundários precisam ter >=50% da massa do primário const PROMINENCE_MIN = 0.25; // pico precisa destacar >=25% acima do vale circundante const MAX_PGS = 3; // teto absoluto (acima de 3 é dispersão, não cultura) const MIN_PEAK_SEPARATION = 18; // distância mínima entre pGs (unidades da matriz) const MIN_POINTS_FOR_ANALYSIS = 8; // amostra mínima para rodar análise // ========================================================================= // BOOTSTRAP // ========================================================================= document.addEventListener('DOMContentLoaded', function () { const computeButton = document.getElementById('computeCentroid'); if (computeButton) { computeButton.addEventListener('click', processarCoordenadas); } else { console.warn("pg4: botão #computeCentroid não encontrado."); } }); // ========================================================================= // PIPELINE PRINCIPAL // ========================================================================= function processarCoordenadas() { const input = document.getElementById('hiddenCoordinates').value; // Guarda: se já há pGs no campo oculto, não recalcular if (/\bpG\d*\b/.test(input)) { console.log("pg4: pGs já presentes no campo oculto. Cálculo ignorado."); return; } const pontos = extrairPontos(input); if (pontos.length < MIN_POINTS_FOR_ANALYSIS) { console.log(`pg4: amostra pequena (${pontos.length} pontos). Análise ignorada.`); return; } const campo = construirCampoKDE(pontos); const picos = detectarPicosLocais(campo); const picosEnriquecidos = enriquecerComMassaEProeminencia(picos, pontos, campo); const pGsFinais = selecionarPGs(picosEnriquecidos); if (pGsFinais.length === 0) { console.log("pg4: nenhum aglomerado significativo. Padrão de dispersão cultural detectado."); return; } pGsFinais.forEach((pg, index) => { const label = index === 0 ? "pG" : `pG${index + 1}`; marcarPG(pg, label); }); window.densityData = pGsFinais; } // ========================================================================= // EXTRAÇÃO DE PONTOS // ========================================================================= function extrairPontos(input) { return input.split('\n').map(linha => { // Ignora linhas de labels já existentes (pG-..., PG-..., etc.) if (/^[a-zA-Z]+[a-zA-Z0-9_-]*-/.test(linha)) return null; const linhaPura = linha.split('>')[0].split('(r=')[0]; const partes = linhaPura.split(','); if (partes.length === 4 && partes.every(p => !isNaN(parseFloat(p)))) { // Formato da matriz Qore: (100-y), y, (100-x), x return { x: parseFloat(partes[3]), y: parseFloat(partes[1]) }; } return null; }).filter(p => p !== null); } // ========================================================================= // KDE: CAMPO DE DENSIDADE GAUSSIANO // ========================================================================= function construirCampoKDE(pontos) { const grid = Array.from({ length: GRID_SIZE }, () => new Float64Array(GRID_SIZE)); const h2 = 2 * BANDWIDTH * BANDWIDTH; const raioKernel = Math.ceil(BANDWIDTH * 3); // cobre ~99.7% da massa gaussiana pontos.forEach(p => { const xMin = Math.max(0, Math.floor(p.x - raioKernel)); const xMax = Math.min(GRID_SIZE - 1, Math.ceil(p.x + raioKernel)); const yMin = Math.max(0, Math.floor(p.y - raioKernel)); const yMax = Math.min(GRID_SIZE - 1, Math.ceil(p.y + raioKernel)); 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; } // ========================================================================= // DETECÇÃO DE MÁXIMOS LOCAIS // ========================================================================= function detectarPicosLocais(grid) { const picos = []; const raio = Math.max(2, Math.floor(BANDWIDTH / 2)); const maxGlobal = encontrarMaximoGlobal(grid); const thresholdRuido = maxGlobal * 0.05; // ignora regiões praticamente vazias 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 < thresholdRuido) continue; let ehMaximo = true; for (let di = -raio; di <= raio && ehMaximo; di++) { for (let dj = -raio; dj <= raio && ehMaximo; dj++) { if (di === 0 && dj === 0) continue; if (grid[i + di][j + dj] > v) ehMaximo = false; } } if (ehMaximo) picos.push({ x: i, y: j, densidade: v }); } } return picos; } function encontrarMaximoGlobal(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; } // ========================================================================= // MASSA REAL E PROEMINÊNCIA // ========================================================================= function enriquecerComMassaEProeminencia(picos, pontos, grid) { const total = pontos.length; const raioMassa = BANDWIDTH * 1.5; return picos.map(pico => { // Massa: % de pontos reais dentro do raio de captura const massa = pontos.filter(p => Math.sqrt((p.x - pico.x) ** 2 + (p.y - pico.y) ** 2) <= raioMassa ).length / total; // Proeminência: quanto o pico se destaca do vale ao redor const proeminencia = calcularProeminencia(pico, grid); return { ...pico, massa, proeminencia }; }); } function calcularProeminencia(pico, grid) { const raio = Math.ceil(BANDWIDTH * 2); let valeMinimo = 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] < valeMinimo) valeMinimo = grid[i][j]; } } return (pico.densidade - valeMinimo) / pico.densidade; } // ========================================================================= // SELEÇÃO FINAL DOS pGs // ========================================================================= function selecionarPGs(picos) { if (picos.length === 0) return []; // Filtro de proeminência: picos sem destaque são só saliências num planalto const candidatos = picos .filter(p => p.proeminencia >= PROMINENCE_MIN) .sort((a, b) => b.massa - a.massa); if (candidatos.length === 0) return []; // Portão de existência: pico primário precisa de massa mínima if (candidatos[0].massa < MASS_MIN_PRIMARY) { console.log( `pg4: pico mais denso tem apenas ${(candidatos[0].massa * 100).toFixed(1)}% de massa ` + `(mínimo ${(MASS_MIN_PRIMARY * 100).toFixed(0)}%). Padrão disperso.` ); return []; } const massaPrimario = candidatos[0].massa; const selecionados = [candidatos[0]]; for (let i = 1; i < candidatos.length && selecionados.length < MAX_PGS; i++) { const c = candidatos[i]; // Portão de dominância relativa if (c.massa < massaPrimario * DOMINANCE_RATIO) continue; // Portão de separação mínima const distanteDosOutros = selecionados.every(s => Math.sqrt((s.x - c.x) ** 2 + (s.y - c.y) ** 2) >= MIN_PEAK_SEPARATION ); if (!distanteDosOutros) continue; selecionados.push(c); } return selecionados; } // ========================================================================= // INTEGRAÇÃO COM O RESTO DO SISTEMA // ========================================================================= function marcarPG(pg, label) { // Converte coordenada lógica (0-100) para pixel da matriz const mappedX = (100 - pg.x) / 100 * width; const mappedY = height - ((100 - pg.y) / 100 * height); data.push({ x: mappedX, y: mappedY, label: label, isSpecial: true }); if (!activeLabels.hasOwnProperty(label)) { activeLabels[label] = false; } // Intensidade agora vem da massa real em vez de array fixo [20, 12, 7, 2] // Faixa prática: 5 (massa ~5%) a 25 (massa ~45%) const intensidade = Math.max(5, Math.min(25, Math.round(pg.massa * 50) + 3)); const f = (100 - pg.y).toFixed(2); const e = pg.y.toFixed(2); const c = (100 - pg.x).toFixed(2); const a = pg.x.toFixed(2); const intensidadeCoord = `(PG-${intensidade})${f},${e},${c},${a}`; const pGCoord = `${label}-${f},${e},${c},${a}`; const hiddenField = document.getElementById('hiddenCoordinates'); if (hiddenField) { hiddenField.value += `${intensidadeCoord}\n${pGCoord}\n`; console.log( `pg4 ${label}: massa=${(pg.massa * 100).toFixed(1)}%, ` + `proeminência=${(pg.proeminencia * 100).toFixed(0)}%, ` + `intensidade=${intensidade}, posição=(${pg.x},${pg.y})` ); } drawHeatmap(); updateLabelTable(); }