mapa/index3.php
2025-07-04 12:34:51 -04:00

838 lines
29 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%;
}
* {
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 {
position: absolute;
top: 100px;
right: 30px;
width: 350px;
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: 20px;
border: 1px solid rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
z-index: 30;
}
.stats-card {
background: rgba(0, 30, 60, 0.65);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(0, 200, 255, 0.25);
}
.stats-card h2 {
font-size: 1.4rem;
margin-bottom: 15px;
color: #64c8ff;
display: flex;
align-items: center;
gap: 10px;
}
.stats-card h2 i {
font-size: 1.6rem;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.stat-item {
background: rgba(0, 50, 100, 0.45);
padding: 12px;
border-radius: 10px;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 150, 255, 0.3);
}
.stat-value {
font-size: 2.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.9rem;
opacity: 0.8;
}
.connection-list {
max-height: 300px;
overflow-y: auto;
background: rgba(0, 20, 40, 0.55);
border-radius: 10px;
padding: 15px;
}
.connection-item {
padding: 10px;
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;
}
.connection-location {
font-size: 0.9rem;
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;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.control-btn {
flex: 1;
padding: 12px;
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);
}
.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.9rem;
opacity: 0.7;
margin-top: 10px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
gap: 20px;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(100, 200, 255, 0.3);
border-top: 5px solid #64c8ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.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.9rem;
}
.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;
}
.map-overlay {
position: absolute;
bottom: 20px;
left: 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;
}
.legend-item {
display: flex;
align-items: center;
margin: 8px 0;
}
.legend-color {
width: 25px;
height: 15px;
margin-right: 10px;
border-radius: 3px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 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;
}
.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 {
width: calc(100% - 40px);
max-height: 40vh;
top: auto;
bottom: 20px;
left: 20px;
right: 20px;
}
.globe-container {
margin-bottom: 300px;
}
}
</style>
</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>
<div class="panel">
<div class="top-bar">
<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 class="stats-card">
<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="stats-card">
<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>
<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>