877 lines
30 KiB
PHP
877 lines
30 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mapa de Calor Global en Esfera 3D</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
.map-layout {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
|
|
.map-sidebar {
|
|
width: 300px;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
#map-container {
|
|
flex: 1;
|
|
height: 100%;
|
|
}
|
|
|
|
.map-layout {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
|
|
.map-sidebar {
|
|
width: 300px;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
#map-container {
|
|
flex: 1;
|
|
height: 100%;
|
|
}
|
|
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #0c1445, #1a237e, #283593);
|
|
color: #fff;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.container {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 1400px;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 20px;
|
|
z-index: 10;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
padding: 15px 0;
|
|
margin-bottom: 20px;
|
|
z-index: 20;
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 8px;
|
|
color: #fff;
|
|
text-shadow: 0 0 15px rgba(100, 200, 255, 0.7);
|
|
animation: glow 2s ease-in-out infinite alternate;
|
|
}
|
|
|
|
@keyframes glow {
|
|
from { text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #64c8ff, 0 0 20px #64c8ff; }
|
|
to { text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #2196f3, 0 0 40px #2196f3; }
|
|
}
|
|
|
|
header p {
|
|
font-size: 1.1rem;
|
|
opacity: 0.9;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.globe-container {
|
|
position: relative;
|
|
flex: 1;
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
|
background: rgba(0, 10, 30, 0.2);
|
|
border: 1px solid rgba(100, 200, 255, 0.2);
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
|
|
#globe {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.panel-container {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
z-index: 30;
|
|
width: 350px;
|
|
}
|
|
|
|
.panel {
|
|
background: rgba(0, 10, 20, 0.8);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
font-size: 1.4rem;
|
|
color: #64c8ff;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.connection-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 0.9rem;
|
|
background: rgba(100, 200, 255, 0.2);
|
|
padding: 5px 10px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.stats-card {
|
|
background: rgba(0, 30, 60, 0.65);
|
|
border-radius: 12px;
|
|
padding: 15px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
border: 1px solid rgba(0, 200, 255, 0.25);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-item {
|
|
background: rgba(0, 50, 100, 0.45);
|
|
padding: 10px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
}
|
|
|
|
.stat-item:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 5px 15px rgba(0, 150, 255, 0.3);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #64c8ff;
|
|
margin: 5px 0;
|
|
text-shadow: 0 0 10px rgba(100, 200, 255, 0.5);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.85rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.connection-list {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
background: rgba(0, 20, 40, 0.55);
|
|
border-radius: 10px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.connection-item {
|
|
padding: 8px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.connection-item:hover {
|
|
background: rgba(100, 200, 255, 0.1);
|
|
}
|
|
|
|
.connection-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.connection-ip {
|
|
font-weight: bold;
|
|
color: #64c8ff;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.connection-location {
|
|
font-size: 0.85rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.connection-count {
|
|
background: rgba(100, 200, 255, 0.25);
|
|
padding: 2px 10px;
|
|
border-radius: 20px;
|
|
font-weight: bold;
|
|
min-width: 30px;
|
|
text-align: center;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-btn {
|
|
flex: 1;
|
|
padding: 10px;
|
|
background: linear-gradient(to right, #1a2980, #26d0ce);
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.control-btn:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.control-btn.active {
|
|
background: linear-gradient(to right, #11998e, #38ef7d);
|
|
box-shadow: 0 0 15px rgba(56, 239, 125, 0.5);
|
|
}
|
|
|
|
.timestamp {
|
|
text-align: center;
|
|
font-size: 0.85rem;
|
|
opacity: 0.7;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100%;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid rgba(100, 200, 255, 0.3);
|
|
border-top: 4px solid #64c8ff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.map-overlay {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 10, 20, 0.85);
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
max-width: 250px;
|
|
z-index: 15;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
|
|
.map-overlay h3 {
|
|
color: #64c8ff;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 25px;
|
|
height: 15px;
|
|
margin-right: 10px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 10px;
|
|
padding: 8px;
|
|
background: rgba(0, 30, 60, 0.65);
|
|
border-radius: 8px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background-color: #4CAF50;
|
|
}
|
|
|
|
.status-dot.active {
|
|
background-color: #4CAF50;
|
|
box-shadow: 0 0 10px #4CAF50;
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 0.7; }
|
|
50% { opacity: 1; }
|
|
100% { opacity: 0.7; }
|
|
}
|
|
|
|
.status-dot.inactive {
|
|
background-color: #f44336;
|
|
}
|
|
|
|
.stars {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.star {
|
|
position: absolute;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
animation: twinkle var(--duration, 5s) infinite var(--delay, 0s);
|
|
}
|
|
|
|
@keyframes twinkle {
|
|
0%, 100% { opacity: 0.2; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.panel-container {
|
|
width: calc(100% - 40px);
|
|
max-height: 40vh;
|
|
top: auto;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
right: 20px;
|
|
}
|
|
|
|
.globe-container {
|
|
margin-bottom: 300px;
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.panel {
|
|
padding: 15px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Fondo de estrellas -->
|
|
<div class="stars" id="stars"></div>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1><i class="fas fa-globe-americas"></i> Mapa de Calor Global en 3D</h1>
|
|
<p>Visualización en tiempo real de conexiones en una esfera interactiva - Actualizando cada 5 segundos</p>
|
|
</header>
|
|
|
|
<div class="globe-container">
|
|
<div id="globe"></div>
|
|
|
|
<!-- Panel de Actividad Global en el lado izquierdo -->
|
|
<div class="panel-container">
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2><i class="fas fa-fire"></i> Actividad Global</h2>
|
|
<div class="connection-badge">
|
|
<i class="fas fa-sync-alt fa-spin"></i>
|
|
<span>Actualizando...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-card">
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="total-connections">0</div>
|
|
<div class="stat-label">Conexiones totales</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="unique-countries">0</div>
|
|
<div class="stat-label">Países</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="max-connections">0</div>
|
|
<div class="stat-label">Máx. conexiones</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="active-ips">0</div>
|
|
<div class="stat-label">IPs activas</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2><i class="fas fa-list"></i> Últimas conexiones</h2>
|
|
<div class="connection-list" id="connection-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<div>Cargando datos...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2><i class="fas fa-sliders-h"></i> Controles</h2>
|
|
<div class="controls">
|
|
<button id="auto-rotate-btn" class="control-btn active">
|
|
<i class="fas fa-sync"></i> Rotar
|
|
</button>
|
|
<button id="heat-btn" class="control-btn active">
|
|
<i class="fas fa-fire"></i> Calor
|
|
</button>
|
|
<button id="refresh-btn" class="control-btn">
|
|
<i class="fas fa-sync-alt"></i> Actualizar
|
|
</button>
|
|
</div>
|
|
<div class="status-indicator">
|
|
<div class="status-dot active" id="status-dot"></div>
|
|
<span id="status-text">Conectando a la API...</span>
|
|
</div>
|
|
<div class="timestamp">
|
|
Última actualización: <span id="last-update">--:--:--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leyenda en el lado derecho -->
|
|
<div class="map-overlay">
|
|
<h3><i class="fas fa-layer-group"></i> Leyenda</h3>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: rgba(0, 0, 255, 0.5);"></div>
|
|
<span>Baja actividad</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: rgba(0, 255, 255, 0.5);"></div>
|
|
<span>Actividad media</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: rgba(0, 255, 0, 0.5);"></div>
|
|
<span>Alta actividad</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: rgba(255, 255, 0, 0.5);"></div>
|
|
<span>Muy alta actividad</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: rgba(255, 0, 0, 0.5);"></div>
|
|
<span>Máxima actividad</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Variables globales
|
|
let scene, camera, renderer, controls;
|
|
let globe, heatPoints = [];
|
|
let currentData = [];
|
|
let autoRotate = true;
|
|
let showHeat = true;
|
|
let refreshInterval;
|
|
|
|
// Inicializar la escena Three.js
|
|
function init() {
|
|
// Crear la escena
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0c1445);
|
|
scene.fog = new THREE.Fog(0x0c1445, 10, 30);
|
|
|
|
// Crear la cámara
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.z = 25;
|
|
|
|
// Crear el renderizador
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
document.getElementById('globe').appendChild(renderer.domElement);
|
|
|
|
// Añadir controles de órbita
|
|
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.autoRotate = autoRotate;
|
|
controls.autoRotateSpeed = 0.5;
|
|
|
|
// Añadir iluminación
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(5, 3, 5);
|
|
scene.add(directionalLight);
|
|
|
|
const pointLight = new THREE.PointLight(0x64c8ff, 1, 50);
|
|
pointLight.position.set(5, 5, 5);
|
|
scene.add(pointLight);
|
|
|
|
// Crear la esfera (globo terráqueo)
|
|
createGlobe();
|
|
|
|
// Crear estrellas de fondo
|
|
createStars();
|
|
|
|
// Iniciar animación
|
|
animate();
|
|
|
|
// Manejar redimensionamiento
|
|
window.addEventListener('resize', onWindowResize, false);
|
|
|
|
// Cargar datos iniciales
|
|
fetchData();
|
|
|
|
// Configurar intervalo para actualizar cada 5 segundos
|
|
refreshInterval = setInterval(fetchData, 5000);
|
|
}
|
|
|
|
// Crear el globo terráqueo
|
|
function createGlobe() {
|
|
// Crear la geometría de la esfera
|
|
const geometry = new THREE.SphereGeometry(10, 64, 64);
|
|
|
|
// Cargar textura de la Tierra
|
|
const textureLoader = new THREE.TextureLoader();
|
|
const texture = textureLoader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg');
|
|
const bumpMap = textureLoader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_normal_2048.jpg');
|
|
const specularMap = textureLoader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_specular_2048.jpg');
|
|
|
|
// Crear material
|
|
const material = new THREE.MeshPhongMaterial({
|
|
map: texture,
|
|
bumpMap: bumpMap,
|
|
bumpScale: 0.05,
|
|
specularMap: specularMap,
|
|
specular: new THREE.Color(0x333333),
|
|
shininess: 5
|
|
});
|
|
|
|
// Crear la esfera
|
|
globe = new THREE.Mesh(geometry, material);
|
|
scene.add(globe);
|
|
|
|
// Añadir nubes
|
|
const cloudsGeometry = new THREE.SphereGeometry(10.05, 64, 64);
|
|
const cloudsMaterial = new THREE.MeshPhongMaterial({
|
|
map: textureLoader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_clouds_1024.png'),
|
|
transparent: true,
|
|
opacity: 0.4
|
|
});
|
|
|
|
const clouds = new THREE.Mesh(cloudsGeometry, cloudsMaterial);
|
|
scene.add(clouds);
|
|
}
|
|
|
|
// Crear estrellas de fondo
|
|
function createStars() {
|
|
const starsContainer = document.getElementById('stars');
|
|
for (let i = 0; i < 200; i++) {
|
|
const star = document.createElement('div');
|
|
star.classList.add('star');
|
|
star.style.width = `${Math.random() * 3}px`;
|
|
star.style.height = star.style.width;
|
|
star.style.left = `${Math.random() * 100}%`;
|
|
star.style.top = `${Math.random() * 100}%`;
|
|
star.style.setProperty('--duration', `${Math.random() * 5 + 3}s`);
|
|
star.style.setProperty('--delay', `${Math.random() * 5}s`);
|
|
starsContainer.appendChild(star);
|
|
}
|
|
}
|
|
|
|
// Actualizar el mapa con nuevos datos
|
|
function updateMap(data) {
|
|
// Eliminar puntos antiguos
|
|
heatPoints.forEach(point => scene.remove(point));
|
|
heatPoints = [];
|
|
|
|
// Filtrar puntos con coordenadas válidas (excluir (0,0))
|
|
const filteredData = data.filter(item => item.lat !== 0 && item.lon !== 0);
|
|
|
|
// Crear nuevos puntos
|
|
filteredData.forEach(item => {
|
|
// Convertir lat/lon a coordenadas 3D
|
|
const lat = item.lat * Math.PI / 180;
|
|
const lon = -item.lon * Math.PI / 180;
|
|
const radius = 10.1;
|
|
|
|
// Calcular posición en la esfera
|
|
const x = radius * Math.cos(lat) * Math.cos(lon);
|
|
const y = radius * Math.sin(lat);
|
|
const z = radius * Math.cos(lat) * Math.sin(lon);
|
|
|
|
// Crear geometría del punto
|
|
const pointSize = Math.min(0.1 + item.connections * 0.05, 0.5);
|
|
const geometry = new THREE.SphereGeometry(pointSize, 16, 16);
|
|
|
|
// Asignar color basado en conexiones
|
|
let color;
|
|
if (item.connections === 1) color = new THREE.Color(0x4287f5);
|
|
else if (item.connections <= 3) color = new THREE.Color(0x42f5ef);
|
|
else if (item.connections <= 6) color = new THREE.Color(0x42f56e);
|
|
else color = new THREE.Color(0xf54242);
|
|
|
|
const material = new THREE.MeshBasicMaterial({
|
|
color: color,
|
|
transparent: true,
|
|
opacity: showHeat ? 0.8 : 0
|
|
});
|
|
|
|
// Crear el punto
|
|
const point = new THREE.Mesh(geometry, material);
|
|
point.position.set(x, y, z);
|
|
|
|
// Añadir a la escena y a la lista
|
|
scene.add(point);
|
|
heatPoints.push(point);
|
|
});
|
|
}
|
|
|
|
// Obtener datos de la API
|
|
async function fetchData() {
|
|
try {
|
|
updateStatus('Conectando a la API...', 'connecting');
|
|
|
|
// Usar tu API real
|
|
const response = await fetch('http://192.168.6.63/mapa/data.php');
|
|
if (!response.ok) throw new Error('Error en la respuesta de la API');
|
|
|
|
const data = await response.json();
|
|
currentData = data;
|
|
|
|
updateMap(currentData);
|
|
updateStats(currentData);
|
|
updateConnectionList(currentData);
|
|
updateStatus('Datos actualizados', 'success');
|
|
|
|
// Actualizar marca de tiempo
|
|
const now = new Date();
|
|
document.getElementById('last-update').textContent =
|
|
now.toLocaleTimeString();
|
|
} catch (error) {
|
|
console.error('Error al obtener datos:', error);
|
|
updateStatus('Error al obtener datos', 'error');
|
|
}
|
|
}
|
|
|
|
// Actualizar estadísticas
|
|
function updateStats(data) {
|
|
// Total de conexiones
|
|
const totalConnections = data.reduce((sum, item) => sum + item.connections, 0);
|
|
document.getElementById('total-connections').textContent = totalConnections;
|
|
|
|
// Países únicos
|
|
const uniqueCountries = new Set(data.map(item => {
|
|
// Extraer código de país si está disponible
|
|
const parts = item.country.split(', ');
|
|
return parts.length > 1 ? parts[1] : item.country;
|
|
}).filter(country => country.length === 2)); // Filtrar solo códigos de 2 letras
|
|
|
|
document.getElementById('unique-countries').textContent = uniqueCountries.size;
|
|
|
|
// Máximo de conexiones
|
|
const maxConnections = Math.max(...data.map(item => item.connections));
|
|
document.getElementById('max-connections').textContent = maxConnections;
|
|
|
|
// IPs activas
|
|
document.getElementById('active-ips').textContent = data.length;
|
|
}
|
|
|
|
// Actualizar lista de conexiones
|
|
function updateConnectionList(data) {
|
|
const listElement = document.getElementById('connection-list');
|
|
listElement.innerHTML = '';
|
|
|
|
// Ordenar por timestamp (más recientes primero)
|
|
const sortedData = [...data].sort((a, b) => b.timestamp - a.timestamp);
|
|
|
|
// Tomar los últimos 8 elementos
|
|
sortedData.slice(0, 8).forEach(item => {
|
|
const connectionItem = document.createElement('div');
|
|
connectionItem.className = 'connection-item';
|
|
|
|
// Convertir timestamp a hora legible
|
|
const date = new Date(item.timestamp * 1000);
|
|
const timeString = date.toLocaleTimeString();
|
|
|
|
// Extraer el código de país si está disponible
|
|
let countryCode = item.country;
|
|
const countryParts = item.country.split(', ');
|
|
if (countryParts.length > 1 && countryParts[1].length === 2) {
|
|
countryCode = countryParts[1];
|
|
}
|
|
|
|
connectionItem.innerHTML = `
|
|
<div>
|
|
<div class="connection-ip">${item.ip}</div>
|
|
<div class="connection-location">${countryCode} • ${timeString}</div>
|
|
</div>
|
|
<div class="connection-count">${item.connections}</div>
|
|
`;
|
|
|
|
listElement.appendChild(connectionItem);
|
|
});
|
|
}
|
|
|
|
// Actualizar estado de conexión
|
|
function updateStatus(message, status) {
|
|
const statusText = document.getElementById('status-text');
|
|
const statusDot = document.getElementById('status-dot');
|
|
|
|
statusText.innerHTML = message;
|
|
|
|
// Limpiar clases anteriores
|
|
statusDot.className = 'status-dot';
|
|
|
|
if (status === 'success') {
|
|
statusDot.classList.add('active');
|
|
} else if (status === 'error') {
|
|
statusDot.classList.add('inactive');
|
|
} else if (status === 'connecting') {
|
|
statusDot.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// Inicializar controles
|
|
function initControls() {
|
|
// Botón de rotación automática
|
|
document.getElementById('auto-rotate-btn').addEventListener('click', () => {
|
|
autoRotate = !autoRotate;
|
|
controls.autoRotate = autoRotate;
|
|
|
|
const btn = document.getElementById('auto-rotate-btn');
|
|
btn.classList.toggle('active', autoRotate);
|
|
btn.innerHTML = autoRotate ?
|
|
'<i class="fas fa-sync"></i> Rotar' :
|
|
'<i class="fas fa-ban"></i> Rotar';
|
|
});
|
|
|
|
// Botón de mostrar calor
|
|
document.getElementById('heat-btn').addEventListener('click', () => {
|
|
showHeat = !showHeat;
|
|
|
|
// Actualizar opacidad de todos los puntos
|
|
heatPoints.forEach(point => {
|
|
point.material.opacity = showHeat ? 0.8 : 0;
|
|
});
|
|
|
|
const btn = document.getElementById('heat-btn');
|
|
btn.classList.toggle('active', showHeat);
|
|
btn.innerHTML = showHeat ?
|
|
'<i class="fas fa-fire"></i> Calor' :
|
|
'<i class="fas fa-fire"></i> Calor';
|
|
});
|
|
|
|
// Botón de actualización
|
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
|
fetchData();
|
|
});
|
|
}
|
|
|
|
// Manejar redimensionamiento de ventana
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
// Función de animación
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Iniciar la aplicación cuando el DOM esté listo
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init();
|
|
initControls();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|