370 lines
13 KiB
PHP
370 lines
13 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 Conexiones Global en 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>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #0c1445, #1a237e, #283593);
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#globe-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.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; }
|
|
}
|
|
|
|
.loading {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
background: rgba(0, 10, 30, 0.7);
|
|
z-index: 100;
|
|
color: white;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
.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); }
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 1.2rem;
|
|
text-align: center;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.timestamp {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 0.9rem;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
z-index: 10;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Fondo de estrellas -->
|
|
<div class="stars" id="stars"></div>
|
|
|
|
<!-- Contenedor del globo -->
|
|
<div id="globe-container">
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<div class="status-text">Cargando datos de la API...</div>
|
|
</div>
|
|
<div class="timestamp" id="last-update"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Variables globales
|
|
let scene, camera, renderer, controls;
|
|
let globe, connectionBoxes = [];
|
|
let currentData = [];
|
|
let autoRotate = true;
|
|
let refreshInterval;
|
|
|
|
// Variables para normalización de tamaños
|
|
let minConnections = Infinity;
|
|
let maxConnections = 0;
|
|
|
|
// 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-container').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.9;
|
|
|
|
// 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 minuto
|
|
refreshInterval = setInterval(fetchData, 60000);
|
|
}
|
|
|
|
// 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: 50
|
|
});
|
|
|
|
// 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.8
|
|
});
|
|
|
|
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) {
|
|
// Calcular min y max de conexiones para normalización
|
|
minConnections = Infinity;
|
|
maxConnections = 0;
|
|
data.forEach(item => {
|
|
if (item.connections < minConnections) minConnections = item.connections;
|
|
if (item.connections > maxConnections) maxConnections = item.connections;
|
|
});
|
|
|
|
// Eliminar rectángulos antiguos
|
|
connectionBoxes.forEach(box => scene.remove(box));
|
|
connectionBoxes = [];
|
|
|
|
// Filtrar puntos con coordenadas válidas (excluir (0,0))
|
|
const filteredData = data.filter(item => item.lat !== 0 && item.lon !== 0);
|
|
|
|
// Crear nuevos rectángulos 3D
|
|
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);
|
|
|
|
// Calcular tamaño basado en conexiones (normalizado)
|
|
const baseSize = 0.1;
|
|
const normalizedConnections = (item.connections - minConnections) /
|
|
(maxConnections - minConnections || 1);
|
|
|
|
// Dimensiones del rectángulo
|
|
const width = baseSize * (0.5 + normalizedConnections * 1.5);
|
|
const height = baseSize * (0.5 + normalizedConnections * 1.5);
|
|
const depth = baseSize * (1.5 + normalizedConnections * 3); // Profundidad proporcional
|
|
|
|
// Crear geometría del rectángulo
|
|
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
|
|
// 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 if (item.connections <= 9) color = new THREE.Color(0xf5f542);
|
|
else color = new THREE.Color(0xf54242);
|
|
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: color,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
shininess: 60,
|
|
specular: new THREE.Color(0xffffff)
|
|
});
|
|
|
|
// Crear el rectángulo
|
|
const box = new THREE.Mesh(geometry, material);
|
|
box.position.set(x, y, z);
|
|
|
|
// Orientar el rectángulo hacia la cámara (apuntando radialmente hacia afuera)
|
|
const vector = new THREE.Vector3(x, y, z).normalize();
|
|
box.quaternion.setFromUnitVectors(
|
|
new THREE.Vector3(0, 0, 1),
|
|
vector
|
|
);
|
|
|
|
// Rotar para que quede vertical
|
|
box.rotateX(Math.PI / 2);
|
|
|
|
// Añadir a la escena y a la lista
|
|
scene.add(box);
|
|
connectionBoxes.push(box);
|
|
});
|
|
}
|
|
|
|
// Obtener datos de la API
|
|
async function fetchData() {
|
|
try {
|
|
document.getElementById('loading').style.display = 'flex';
|
|
document.querySelector('.status-text').textContent = "Conectando a la API...";
|
|
|
|
// 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);
|
|
|
|
// Actualizar marca de tiempo
|
|
const now = new Date();
|
|
document.getElementById('last-update').textContent =
|
|
`Última actualización: ${now.toLocaleTimeString()}`;
|
|
|
|
// Actualizar estado
|
|
document.querySelector('.status-text').textContent = "Datos cargados correctamente";
|
|
|
|
// Ocultar spinner después de un breve retraso
|
|
setTimeout(() => {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}, 1000);
|
|
} catch (error) {
|
|
console.error('Error al obtener datos:', error);
|
|
document.querySelector('.status-text').textContent = "Error al conectar con la API";
|
|
|
|
// Ocultar spinner después de un breve retraso
|
|
setTimeout(() => {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|