Initial commit - Proyecto de mapa

This commit is contained in:
Kevin 2025-07-04 12:34:51 -04:00
commit de542421c2
22182 changed files with 25998 additions and 0 deletions

158
config.php Executable file
View File

@ -0,0 +1,158 @@
<?php
// config.php - Configuración principal corregida
error_reporting(E_ALL);
ini_set('display_errors', 1);
define('EJABBERD_API_URL', 'https://comunica.presidencia.gob.cu:5443/api');
define('EJABBERD_USER', 'admin@comunica.presidencia.gob.cu');
define('EJABBERD_PASSWORD', '**4fgTK85pb');
define('IP_DATABASE_SQLITE', __DIR__ . '/ip_db.sqlite');
define('IP_CACHE_DIR', __DIR__ . '/ip_cache');
// Crear directorio de caché si no existe
if (!is_dir(IP_CACHE_DIR)) {
mkdir(IP_CACHE_DIR, 0755, true);
}
// Función para conectar a la base de datos SQLite
function get_ip_database() {
static $db = null;
if ($db === null) {
if (!file_exists(IP_DATABASE_SQLITE)) {
die("Base de datos SQLite no encontrada. Ejecuta import_csv_to_sqlite.php primero.");
}
$db = new SQLite3(IP_DATABASE_SQLITE, SQLITE3_OPEN_READONLY);
$db->busyTimeout(5000);
}
return $db;
}
// Obtener IPs de usuarios conectados
function get_connected_ips() {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => EJABBERD_API_URL . '/connected_users_info',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => EJABBERD_USER . ':' . EJABBERD_PASSWORD,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POST => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_POSTFIELDS => json_encode([]),
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
error_log("CURL Error: " . curl_error($ch));
curl_close($ch);
return [];
}
curl_close($ch);
if ($httpCode !== 200) {
error_log("Error en API ejabberd: HTTP $httpCode - $response");
return [];
}
$data = json_decode($response, true);
if (!is_array($data)) {
error_log("Respuesta inválida de API: " . $response);
return [];
}
return $data;
}
// Convertir IP a número entero sin signo
function ip_to_int($ip) {
$ipLong = ip2long($ip);
if ($ipLong === false) {
return null;
}
return sprintf('%u', $ipLong);
}
// Buscar ubicación en la base de datos SQLite
function ip_to_location($ip) {
// Verificar caché
$cache_file = IP_CACHE_DIR . '/' . md5($ip) . '.json';
if (file_exists($cache_file)) {
$cache = json_decode(file_get_contents($cache_file), true);
if ($cache && time() - $cache['timestamp'] < 86400) {
return $cache;
}
}
// Convertir IP
$ip_num = ip_to_int($ip);
if ($ip_num === null) {
return null;
}
// Buscar en SQLite
$db = get_ip_database();
$stmt = $db->prepare('SELECT country, city, region, lat, lon FROM ip_ranges WHERE :ip >= start AND :ip <= end LIMIT 1');
if (!$stmt) {
error_log("Error preparando consulta: " . $db->lastErrorMsg());
return null;
}
$stmt->bindValue(':ip', $ip_num, SQLITE3_INTEGER);
$result = $stmt->execute();
if (!$result) {
error_log("Error ejecutando consulta: " . $db->lastErrorMsg());
return null;
}
$data = $result->fetchArray(SQLITE3_ASSOC);
if ($data) {
// Validar y convertir coordenadas
$lat = is_numeric($data['lat']) ? (float)$data['lat'] : null;
$lon = is_numeric($data['lon']) ? (float)$data['lon'] : null;
// Si las coordenadas no son válidas, no retornar ubicación
if ($lat === null || $lon === null) {
error_log("Coordenadas inválidas para IP: $ip - Lat: {$data['lat']}, Lon: {$data['lon']}");
return null;
}
// Determinar el mejor nombre para la ubicación
$locationName = $data['country'];
if ($locationName === 'NA' || $locationName === 'EU' || empty($locationName)) {
if (!empty($data['city']) && !empty($data['region'])) {
$locationName = $data['city'] . ', ' . $data['region'];
} elseif (!empty($data['city'])) {
$locationName = $data['city'];
} elseif (!empty($data['region'])) {
$locationName = $data['region'];
} else {
$locationName = 'Desconocido';
}
}
$location = [
'ip' => $ip,
'country' => $locationName,
'lat' => $lat,
'lon' => $lon,
'timestamp' => time()
];
// Guardar en caché
file_put_contents($cache_file, json_encode($location));
return $location;
} else {
error_log("No se encontró ubicación para IP: $ip ($ip_num)");
}
return null;
}
?>

67
data.php Executable file
View File

@ -0,0 +1,67 @@
<?php
// data.php - Servicio de datos corregido
require_once 'config.php';
// Configurar tiempo de ejecución
set_time_limit(10);
// Cabecera JSON
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Access-Control-Allow-Origin: *');
// Usar caché de resultados completos (5 segundos)
$cache_file = IP_CACHE_DIR . '/last_result.json';
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < 5) {
readfile($cache_file);
exit;
}
// Obtener conexiones y procesar
$connections = get_connected_ips();
$locations = [];
$ip_counts = [];
// Procesar cada conexión
foreach ($connections as $conn) {
if (!empty($conn['ip'])) {
$ip = filter_var($conn['ip'], FILTER_VALIDATE_IP);
// Solo procesar IPs válidas
if ($ip) {
// Contar IPs duplicadas
if (!isset($ip_counts[$ip])) {
$ip_counts[$ip] = 0;
}
$ip_counts[$ip]++;
// Solo procesar cada IP una vez
if ($ip_counts[$ip] === 1) {
$location = ip_to_location($ip);
if ($location) {
// Validar coordenadas
if ($location['lat'] !== null && $location['lon'] !== null) {
$locations[] = $location;
}
}
}
}
}
}
// Agregar conteo de conexiones a cada ubicación
foreach ($locations as &$loc) {
$loc['connections'] = $ip_counts[$loc['ip']] ?? 1;
}
// Si no hay ubicaciones, devolver un array vacío
if (empty($locations)) {
$locations = [];
}
// Guardar y enviar resultado
$result = json_encode($locations);
file_put_contents($cache_file, $result);
echo $result;
?>

BIN
dbip-city-lite.csv Executable file

Binary file not shown.
Can't render this file because it is too large.

125
import_csv_to_sqlite.php Normal file
View File

@ -0,0 +1,125 @@
<?php
// import_csv_to_sqlite.php - Importador de base de datos corregido
define('CSV_FILE', __DIR__ . '/dbip-city-lite.csv');
define('SQLITE_DB', __DIR__ . '/ip_db.sqlite');
define('BATCH_SIZE', 50000);
// Verificar si el archivo CSV existe
if (!file_exists(CSV_FILE)) {
die("Archivo CSV no encontrado: " . CSV_FILE);
}
// Eliminar base de datos existente
if (file_exists(SQLITE_DB)) {
unlink(SQLITE_DB);
}
// Crear nueva base de datos
$db = new SQLite3(SQLITE_DB);
if (!$db) {
die("No se pudo crear la base de datos SQLite");
}
// Crear tabla con estructura mejorada
$db->exec('CREATE TABLE ip_ranges (
start INTEGER UNSIGNED,
end INTEGER UNSIGNED,
country TEXT,
region TEXT,
city TEXT,
lat REAL,
lon REAL
)');
// Crear índices
$db->exec('CREATE INDEX idx_start ON ip_ranges (start)');
$db->exec('CREATE INDEX idx_end ON ip_ranges (end)');
// Abrir archivo CSV
$file = fopen(CSV_FILE, 'r');
if (!$file) {
die("No se pudo abrir el archivo CSV");
}
// Preparar statement
$stmt = $db->prepare('INSERT INTO ip_ranges (start, end, country, region, city, lat, lon)
VALUES (:start, :end, :country, :region, :city, :lat, :lon)');
if (!$stmt) {
die("Error preparando statement: " . $db->lastErrorMsg());
}
echo "Iniciando importación...\n";
$startTime = microtime(true);
$count = 0;
$batchCount = 0;
$db->exec('BEGIN TRANSACTION');
while (($data = fgetcsv($file)) !== false) {
// Asegurarse de que hay suficientes columnas
if (count($data) < 8) {
continue;
}
// Obtener rangos de IP
$start = ip2long($data[0]);
$end = ip2long($data[1]);
if ($start === false || $end === false) {
continue;
}
// Convertir a enteros sin signo
$start = sprintf('%u', $start);
$end = sprintf('%u', $end);
// Obtener datos de ubicación
$country = $data[2];
$region = $data[3] ?? '';
$city = $data[4] ?? '';
// Latitud y Longitud - índices corregidos según estructura estándar
$lat = is_numeric($data[6]) ? (float)$data[6] : 0.0;
$lon = is_numeric($data[7]) ? (float)$data[7] : 0.0;
// Insertar datos
$stmt->bindValue(':start', $start, SQLITE3_INTEGER);
$stmt->bindValue(':end', $end, SQLITE3_INTEGER);
$stmt->bindValue(':country', $country, SQLITE3_TEXT);
$stmt->bindValue(':region', $region, SQLITE3_TEXT);
$stmt->bindValue(':city', $city, SQLITE3_TEXT);
$stmt->bindValue(':lat', $lat, SQLITE3_FLOAT);
$stmt->bindValue(':lon', $lon, SQLITE3_FLOAT);
if (!$stmt->execute()) {
echo "Error insertando fila: " . $db->lastErrorMsg() . "\n";
}
$count++;
$batchCount++;
// Commit periódico
if ($batchCount >= BATCH_SIZE) {
$db->exec('COMMIT');
$db->exec('BEGIN TRANSACTION');
echo "Importadas $count filas...\n";
$batchCount = 0;
}
}
// Finalizar transacción
$db->exec('COMMIT');
fclose($file);
// Optimizar base de datos
$db->exec('VACUUM');
$db->close();
$endTime = microtime(true);
$timeTaken = round($endTime - $startTime, 2);
echo "Importación completada!\n";
echo "Total filas: $count\n";
echo "Tiempo tomado: {$timeTaken} segundos\n";
// Crear archivo de verificación
file_put_contents(__DIR__ . '/import_success.txt', "Importación completada: $count registros");
?>

1
import_success.txt Normal file
View File

@ -0,0 +1 @@
Importación completada: 3310843 registros

368
index.php Executable file
View File

@ -0,0 +1,368 @@
<!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 IComunica</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.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; }
body, html { height: 100%; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
#map { height: 100vh; width: 100%; }
.info-panel {
position: absolute;
top: 15px;
right: 15px;
z-index: 1000;
background: rgba(255, 255, 255, 0.93);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
width: 300px;
backdrop-filter: blur(5px);
}
.panel-title {
font-size: 1.4rem;
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
}
.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 15px;
}
.stat-box {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
color: #2980b9;
}
.stat-label {
font-size: 0.85rem;
color: #7f8c8d;
margin-top: 3px;
}
.refresh-info {
background: #e1f5fe;
padding: 12px;
border-radius: 8px;
text-align: center;
font-size: 1.1rem;
color: #0288d1;
}
.refresh-counter {
font-weight: 700;
font-size: 1.3rem;
color: #d35400;
}
.last-updated {
margin-top: 15px;
text-align: center;
font-size: 0.9rem;
color: #7f8c8d;
}
.connection-marker {
position: relative;
width: 16px !important;
height: 24px !important;
}
.marker-pin {
width: 100%;
height: 100%;
background: #3498db; /* Azul */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg) scale(0.65); /* Más pequeño */
position: absolute;
top: 0;
left: 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.2); /* Opcional: sombra suave */
}
.marker-count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(45deg);
color: white;
font-weight: bold;
font-size: 8px; /* Texto más pequeño */
text-align: center;
z-index: 10;
text-shadow: 0 0 2px #000; /* Mejor contraste */
}
.custom-connection-marker {
position: relative !important;
width: 16px !important;
height: 24px !important;
z-index: 1000;
}
.custom-marker-pin {
width: 100% !important;
height: 100% !important;
background: #3498db !important; /* Azul */
border-radius: 50% 50% 50% 0 !important;
transform: rotate(-45deg) scale(0.65) !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
box-shadow: none !important;
}
.custom-marker-count {
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) rotate(45deg) !important;
color: white !important;
font-weight: bold !important;
font-size: 8px !important;
text-align: center !important;
z-index: 10 !important;
text-shadow: 0 0 2px #000 !important;
margin: 0 !important;
padding: 0 !important;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="info-panel">
<div class="panel-title">Conexiones IComunica</div>
<div class="stats-container">
<div class="stat-box">
<div class="stat-value" id="connection-count">0</div>
<div class="stat-label">Conexiones</div>
</div>
<div class="stat-box">
<div class="stat-value" id="location-count">0</div>
<div class="stat-label">Ubicaciones</div>
</div>
</div>
<div class="refresh-info">
Actualizando en <span class="refresh-counter" id="refresh-counter">5</span>s
</div>
<div class="last-updated">
Última actualización: <span id="last-updated">--:--:--</span>
</div>
</div>
<script
//src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js">
src="https://cdn.jsdelivr.net/gh/Leaflet/Leaflet.heat@gh-pages/dist/leaflet-heat.js">
</script>
<script>
// Configuración inicial del mapa
const map = L.map('map').setView([20, 0], 2);
// Capa de OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// Capa para marcadores
const markersLayer = L.layerGroup().addTo(map);
// Variables de estado
let connectionCount = 0;
let refreshTimer = 5;
let updateTimer;
// Icono personalizado
function createCustomIcon(count) {
return L.divIcon({
className: 'force-styles',
html: `
<div style="
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #3498db;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg) scale(0.65);
"></div>
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(45deg);
color: white;
font-weight: bold;
font-size: 8px;
text-align: center;
z-index: 10;
text-shadow: 0 0 2px #000;
">${count}</div>
`,
iconSize: [16, 24],
iconAnchor: [8, 24],
popupAnchor: [0, -18]
});
}
// Crear contenido para popup
function createPopupContent(conn) {
return `
<div class="popup-content">
<div style="font-weight:bold; margin-bottom:8px; font-size:16px;">
${conn.country}
</div>
<div style="margin-bottom:5px;">
<strong>IP:</strong> ${conn.ip}
</div>
<div>
<strong>Conexiones:</strong> ${conn.connections}
</div>
</div>
`;
}
// Actualizar datos del mapa
async function updateMapData() {
try {
const response = await fetch('data.php?t=' + Date.now());
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
const connections = await response.json();
// Limpiar marcadores anteriores
markersLayer.clearLayers();
// Contadores
let totalConnections = 0;
let validLocations = 0;
// Agregar nuevos marcadores
connections.forEach(conn => {
totalConnections += conn.connections || 1;
// Validar coordenadas
if (typeof conn.lat === 'number' &&
typeof conn.lon === 'number' &&
!isNaN(conn.lat) &&
!isNaN(conn.lon)) {
validLocations++;
// Crear marcador con conteo de conexiones
const marker = L.marker([conn.lat, conn.lon], {
icon: createCustomIcon(conn.connections || 1)
}).bindPopup(createPopupContent(conn));
markersLayer.addLayer(marker);
}
});
// Actualizar estadísticas
document.getElementById('connection-count').textContent = totalConnections;
document.getElementById('location-count').textContent = validLocations;
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
// Actualizar vista si hay marcadores
if (validLocations > 0) {
map.fitBounds(markersLayer.getBounds(), {
padding: [50, 50],
maxZoom: 15
});
}
} catch (error) {
console.error('Error al obtener datos:', error);
}
}
// Iniciar temporizador de actualización
function startRefreshTimer() {
refreshTimer = 5;
clearInterval(updateTimer);
updateTimer = setInterval(() => {
refreshTimer--;
document.getElementById('refresh-counter').textContent = refreshTimer;
if (refreshTimer <= 0) {
clearInterval(updateTimer);
updateMapData();
startRefreshTimer();
}
}, 1000);
}
// Estilos dinámicos para marcadores
const style = document.createElement('style');
style.textContent = `
.connection-marker {
position: relative;
width: 30px;
height: 42px;
}
.marker-pin {
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
background: #e74c3c;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.marker-count {
position: absolute;
top: 4px;
left: 0;
width: 100%;
text-align: center;
color: white;
font-weight: bold;
font-size: 14px;
transform: rotate(45deg);
}
.leaflet-popup-content {
margin: 12px;
font-size: 14px;
}
`;
document.head.appendChild(style);
// Inicializar
updateMapData();
startRefreshTimer();
</script>
</body>
</html>

692
index2.php Normal file
View File

@ -0,0 +1,692 @@
<!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 - Conexiones en Tiempo Real</title>
<!-- MapLibre GL JS -->
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css" rel="stylesheet">
<!-- Font Awesome para iconos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
color: #fff;
min-height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20px;
}
header {
text-align: center;
padding: 15px 0;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
header h1 {
font-size: 2.2rem;
margin-bottom: 8px;
color: #fff;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.7);
}
header p {
font-size: 1.1rem;
opacity: 0.9;
max-width: 800px;
margin: 0 auto;
}
.content {
display: flex;
flex: 1;
gap: 20px;
height: calc(100% - 130px);
}
#map {
flex: 3;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel {
flex: 1;
background: rgba(0, 10, 20, 0.7);
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
max-width: 350px;
overflow-y: auto;
}
.stats-card {
background: rgba(0, 30, 60, 0.6);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 200, 255, 0.2);
}
.stats-card h2 {
font-size: 1.4rem;
margin-bottom: 15px;
color: #4fc3f7;
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.4);
padding: 12px;
border-radius: 10px;
text-align: center;
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
color: #4fc3f7;
margin: 5px 0;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.connection-list {
max-height: 300px;
overflow-y: auto;
background: rgba(0, 20, 40, 0.5);
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;
}
.connection-item:last-child {
border-bottom: none;
}
.connection-ip {
font-weight: bold;
color: #4fc3f7;
}
.connection-location {
font-size: 0.9rem;
opacity: 0.9;
}
.connection-count {
background: rgba(79, 195, 247, 0.2);
padding: 2px 10px;
border-radius: 20px;
font-weight: bold;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.control-btn {
flex: 1;
padding: 12px;
background: rgba(0, 100, 200, 0.6);
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;
}
.control-btn:hover {
background: rgba(0, 150, 255, 0.8);
transform: translateY(-2px);
}
.control-btn:active {
transform: translateY(1px);
}
.control-btn.active {
background: rgba(0, 200, 255, 0.8);
box-shadow: 0 0 15px rgba(79, 195, 247, 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(79, 195, 247, 0.3);
border-top: 5px solid #4fc3f7;
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.8);
border-radius: 10px;
padding: 15px;
max-width: 250px;
z-index: 1;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
}
.map-overlay h3 {
color: #4fc3f7;
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;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 8px;
background: rgba(0, 30, 60, 0.6);
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;
}
.status-dot.inactive {
background-color: #f44336;
}
@media (max-width: 900px) {
.content {
flex-direction: column;
}
.panel {
max-width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-fire"></i> Mapa de Calor de Conexiones</h1>
<p>Visualización en tiempo real de conexiones alrededor del mundo - Actualizando cada 5 segundos</p>
</header>
<div class="content">
<div id="map"></div>
<div class="panel">
<div class="stats-card">
<h2><i class="fas fa-chart-bar"></i> Estadísticas</h2>
<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-ips">0</div>
<div class="stat-label">IPs únicas</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="cuba-connections">0</div>
<div class="stat-label">Conexiones en Cuba</div>
</div>
</div>
</div>
<div class="stats-card">
<h2><i class="fas fa-globe-americas"></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="heatmap-btn" class="control-btn active">
<i class="fas fa-fire"></i> Calor
</button>
<button id="points-btn" class="control-btn">
<i class="fas fa-map-marker-alt"></i> Puntos
</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">Conectado 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 del Mapa</h3>
<div class="legend-item">
<div class="legend-color" style="background: rgba(0, 0, 255, 0.5);"></div>
<span>Baja densidad</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(0, 255, 255, 0.5);"></div>
<span>Media densidad</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(0, 255, 0, 0.5);"></div>
<span>Alta densidad</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(255, 255, 0, 0.5);"></div>
<span>Muy alta densidad</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(255, 0, 0, 0.5);"></div>
<span>Máxima densidad</span>
</div>
</div>
<script>
// Variables globales
let map;
let refreshInterval;
let currentData = [];
let isHeatmapActive = true;
let isPointsActive = false;
// Inicializar el mapa
function initMap() {
map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-79.5, 21.5], // Centro en Cuba
zoom: 6,
attributionControl: false
});
map.addControl(new maplibregl.AttributionControl({
compact: true
}));
map.addControl(new maplibregl.NavigationControl(), 'top-right');
map.on('load', () => {
// Cargar datos iniciales
fetchData();
// Configurar intervalo para actualizar cada 5 segundos
refreshInterval = setInterval(fetchData, 5000);
});
}
// Obtener datos de la API
async function fetchData() {
try {
updateStatus('Conectando a la API...', 'connecting');
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(data);
updateStats(data);
updateConnectionList(data);
updateStatus('Datos actualizados correctamente', '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 el mapa con nuevos datos
function updateMap(data) {
// Filtrar puntos con coordenadas válidas (excluir (0,0))
const filteredData = data.filter(item => item.lat !== 0 && item.lon !== 0);
// Convertir a GeoJSON
const geojson = {
type: 'FeatureCollection',
features: filteredData.map(item => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [item.lon, item.lat]
},
properties: {
weight: item.connections,
ip: item.ip,
country: item.country,
connections: item.connections
}
}))
};
// Si la fuente ya existe, actualizar los datos
if (map.getSource('heatmap-source')) {
map.getSource('heatmap-source').setData(geojson);
} else {
// Crear la fuente de datos
map.addSource('heatmap-source', {
type: 'geojson',
data: geojson
});
// Añadir capa de calor
map.addLayer({
id: 'heatmap',
type: 'heatmap',
source: 'heatmap-source',
paint: {
'heatmap-weight': [
'interpolate',
['linear'],
['get', 'weight'],
0, 0.1,
8, 1.0
],
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0, 'rgba(0, 0, 255, 0)',
0.2, 'rgba(0, 0, 255, 0.5)',
0.4, 'rgba(0, 255, 255, 0.5)',
0.6, 'rgba(0, 255, 0, 0.5)',
0.8, 'rgba(255, 255, 0, 0.5)',
1, 'rgba(255, 0, 0, 0.5)'
],
'heatmap-radius': [
'interpolate',
['linear'],
['zoom'],
0, 5,
9, 20
],
'heatmap-opacity': 0.7
}
});
// Añadir capa de puntos (inicialmente oculta)
map.addLayer({
id: 'points',
type: 'circle',
source: 'heatmap-source',
paint: {
'circle-radius': [
'interpolate',
['linear'],
['get', 'connections'],
1, 4,
8, 12
],
'circle-color': [
'interpolate',
['linear'],
['get', 'connections'],
1, '#4287f5',
4, '#42f5ef',
8, '#f54242'
],
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
'circle-opacity': 0.7
},
filter: ['==', '$type', 'Point']
});
// Ocultar capa de puntos inicialmente
map.setLayoutProperty('points', 'visibility', 'none');
// Añadir popups al hacer clic en los puntos
map.on('click', 'points', (e) => {
const ip = e.features[0].properties.ip;
const country = e.features[0].properties.country;
const connections = e.features[0].properties.connections;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(`
<div class="popup-content">
<strong>${ip}</strong><br>
${country}<br>
<span style="color: #f54242">${connections} conexión${connections > 1 ? 'es' : ''}</span>
</div>
`)
.addTo(map);
});
// Cambiar el cursor al pasar sobre puntos
map.on('mouseenter', 'points', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'points', () => {
map.getCanvas().style.cursor = '';
});
}
}
// 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;
// IPs únicas
const uniqueIps = new Set(data.map(item => item.ip)).size;
document.getElementById('unique-ips').textContent = uniqueIps;
// Máximo de conexiones
const maxConnections = Math.max(...data.map(item => item.connections));
document.getElementById('max-connections').textContent = maxConnections;
// Conexiones en Cuba
const cubaConnections = data
.filter(item => item.country.includes('CU'))
.reduce((sum, item) => sum + item.connections, 0);
document.getElementById('cuba-connections').textContent = cubaConnections;
}
// 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 10 elementos
sortedData.slice(0, 10).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();
connectionItem.innerHTML = `
<div>
<div class="connection-ip">${item.ip}</div>
<div class="connection-location">${item.country} ${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.textContent = 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');
statusDot.style.animation = 'pulse 1.5s infinite';
}
}
// Inicializar controles
function initControls() {
// Botón de mapa de calor
document.getElementById('heatmap-btn').addEventListener('click', () => {
isHeatmapActive = !isHeatmapActive;
map.setLayoutProperty('heatmap', 'visibility', isHeatmapActive ? 'visible' : 'none');
const btn = document.getElementById('heatmap-btn');
btn.classList.toggle('active', isHeatmapActive);
});
// Botón de puntos
document.getElementById('points-btn').addEventListener('click', () => {
isPointsActive = !isPointsActive;
map.setLayoutProperty('points', 'visibility', isPointsActive ? 'visible' : 'none');
const btn = document.getElementById('points-btn');
btn.classList.toggle('active', isPointsActive);
});
// Botón de actualización
document.getElementById('refresh-btn').addEventListener('click', () => {
fetchData();
});
}
// Iniciar la aplicación cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
initMap();
initControls();
});
</script>
</body>
</html>

837
index3.php Normal file
View File

@ -0,0 +1,837 @@
<!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>

876
index4.php Normal file
View File

@ -0,0 +1,876 @@
<!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>

336
index5.php Normal file
View File

@ -0,0 +1,336 @@
<!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>
<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, heatPoints = [];
let currentData = [];
let autoRotate = 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-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 5 segundos
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: 1
});
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 if (item.connections <= 9) color = new THREE.Color(0xf5f542);
else color = new THREE.Color(0xf54242);
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.8
});
// 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 {
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>

369
index6.php Normal file
View File

@ -0,0 +1,369 @@
<!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>

View File

@ -0,0 +1 @@
{"ip":"152.207.144.169","country":"Havana, CU","lat":23.1252,"lon":-82.3007,"timestamp":1751593453}

View File

@ -0,0 +1 @@
{"ip":"146.70.182.219","country":"Piedmont, IT","lat":45.8459,"lon":8.41527,"timestamp":1750793829}

View File

@ -0,0 +1 @@
{"ip":"213.87.138.231","country":"Moscow Oblast, RU","lat":55.7017,"lon":36.1932,"timestamp":1751205413}

View File

@ -0,0 +1 @@
{"ip":"96.126.104.130","country":"New Jersey, US","lat":40.8218,"lon":-74.45,"timestamp":1751155466}

View File

@ -0,0 +1 @@
{"ip":"152.206.192.72","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751590265}

View File

@ -0,0 +1 @@
{"ip":"129.222.1.213","country":"Florida, US","lat":25.7617,"lon":-80.1918,"timestamp":1751042073}

View File

@ -0,0 +1 @@
{"ip":"152.207.150.204","country":"Havana, CU","lat":23.0954,"lon":-82.3267,"timestamp":1751025097}

View File

@ -0,0 +1 @@
{"ip":"191.156.49.37","country":"SA","lat":4.60971,"lon":-74.0817,"timestamp":1751638898}

View File

@ -0,0 +1 @@
{"ip":"190.86.107.218","country":"San Miguel Department, SV","lat":13.5,"lon":-88.35,"timestamp":1751149040}

View File

@ -0,0 +1 @@
{"ip":"172.56.2.168","country":"Virginia, US","lat":38.7318,"lon":-77.4311,"timestamp":1750887933}

View File

@ -0,0 +1 @@
{"ip":"172.236.103.4","country":"Illinois, US","lat":41.8781,"lon":-87.6298,"timestamp":1751036709}

View File

@ -0,0 +1 @@
{"ip":"152.156.173.246","country":"SA","lat":-32.3703,"lon":-54.1675,"timestamp":1751112881}

View File

@ -0,0 +1 @@
{"ip":"152.207.149.203","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1751577316}

View File

@ -0,0 +1 @@
{"ip":"152.207.146.165","country":"Cienfuegos Province, CU","lat":22.4148,"lon":-80.2931,"timestamp":1751041891}

View File

@ -0,0 +1 @@
{"ip":"176.33.60.170","country":"AS","lat":41.0605,"lon":28.9872,"timestamp":1750884121}

View File

@ -0,0 +1 @@
{"ip":"152.207.148.61","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751205173}

View File

@ -0,0 +1 @@
{"ip":"152.206.193.104","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751578842}

View File

@ -0,0 +1 @@
{"ip":"152.207.146.211","country":"Cienfuegos Province, CU","lat":22.4148,"lon":-80.2931,"timestamp":1751046609}

View File

@ -0,0 +1 @@
{"ip":"152.206.211.219","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751031943}

View File

@ -0,0 +1 @@
{"ip":"186.167.160.221","country":"SA","lat":10.488,"lon":-66.8792,"timestamp":1751140690}

View File

@ -0,0 +1 @@
{"ip":"188.70.57.237","country":"AS","lat":29.1908,"lon":48.1135,"timestamp":1750870225}

View File

@ -0,0 +1 @@
{"ip":"152.207.133.11","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751054335}

View File

@ -0,0 +1 @@
{"ip":"201.238.32.231","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1750887635}

View File

@ -0,0 +1 @@
{"ip":"152.207.59.150","country":"Santiago de Cuba Province, CU","lat":20.0208,"lon":-75.8267,"timestamp":1751208293}

View File

@ -0,0 +1 @@
{"ip":"106.219.231.195","country":"AS","lat":28.4601,"lon":77.0264,"timestamp":1750818207}

View File

@ -0,0 +1 @@
{"ip":"152.206.184.148","country":"Holgu\u00edn Province, CU","lat":20.8872,"lon":-76.2631,"timestamp":1751130299}

View File

@ -0,0 +1 @@
{"ip":"66.228.38.146","country":"New Jersey, US","lat":40.8218,"lon":-74.45,"timestamp":1750890389}

View File

@ -0,0 +1 @@
{"ip":"152.207.211.201","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750816584}

View File

@ -0,0 +1 @@
{"ip":"152.206.20.93","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751590626}

View File

@ -0,0 +1 @@
{"ip":"152.207.211.93","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750903003}

View File

@ -0,0 +1 @@
{"ip":"152.206.10.228","country":"Havana, CU","lat":23.1231,"lon":-82.4197,"timestamp":1750960602}

View File

@ -0,0 +1 @@
{"ip":"152.206.184.114","country":"Holgu\u00edn Province, CU","lat":20.8872,"lon":-76.2631,"timestamp":1751108738}

View File

@ -0,0 +1 @@
{"ip":"135.129.119.20","country":"Florida, US","lat":25.7617,"lon":-80.1918,"timestamp":1750919463}

View File

@ -0,0 +1 @@
{"ip":"212.5.158.96","country":"Sofia-grad, BG","lat":42.6975,"lon":23.3242,"timestamp":1750854745}

View File

@ -0,0 +1 @@
{"ip":"201.238.1.120","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1751210100}

View File

@ -0,0 +1 @@
{"ip":"151.18.108.38","country":"Piedmont, IT","lat":45.0445,"lon":7.61408,"timestamp":1751120868}

View File

@ -0,0 +1 @@
{"ip":"152.206.120.151","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1750782297}

View File

@ -0,0 +1 @@
{"ip":"152.206.210.28","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1750961023}

View File

@ -0,0 +1 @@
{"ip":"191.156.125.111","country":"SA","lat":4.71638,"lon":-74.212,"timestamp":1751572002}

View File

@ -0,0 +1 @@
{"ip":"152.207.144.14","country":"Havana, CU","lat":23.1252,"lon":-82.3007,"timestamp":1751052288}

View File

@ -0,0 +1 @@
{"ip":"212.30.33.227","country":"Madrid, ES","lat":40.4167,"lon":-3.70329,"timestamp":1751092050}

View File

@ -0,0 +1 @@
{"ip":"191.156.233.16","country":"SA","lat":5.53528,"lon":-73.3678,"timestamp":1750812005}

View File

@ -0,0 +1 @@
{"ip":"152.206.34.179","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751040751}

View File

@ -0,0 +1 @@
{"ip":"94.109.171.166","country":"Wallonia, BE","lat":50.5541,"lon":4.10082,"timestamp":1751606013}

View File

@ -0,0 +1 @@
{"ip":"152.207.27.53","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751608115}

View File

@ -0,0 +1 @@
{"ip":"152.206.191.101","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751104537}

View File

@ -0,0 +1 @@
{"ip":"152.207.132.120","country":"Camag\u00fcey, CU","lat":21.5254,"lon":-78.2258,"timestamp":1751572002}

View File

@ -0,0 +1 @@
{"ip":"177.174.212.235","country":"SA","lat":-15.7797,"lon":-47.9297,"timestamp":1750825232}

View File

@ -0,0 +1 @@
{"ip":"152.206.193.206","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751646166}

View File

@ -0,0 +1 @@
{"ip":"186.13.200.171","country":"SA","lat":-25.3468,"lon":-57.6065,"timestamp":1751008229}

View File

@ -0,0 +1 @@
{"ip":"212.227.226.143","country":"Madrid, ES","lat":40.4167,"lon":-3.70329,"timestamp":1750869211}

View File

@ -0,0 +1 @@
{"ip":"105.232.128.39","country":"AF","lat":-22.5594,"lon":17.0832,"timestamp":1750783675}

View File

@ -0,0 +1 @@
{"ip":"186.13.203.101","country":"SA","lat":-25.5097,"lon":-54.6111,"timestamp":1750891741}

View File

@ -0,0 +1 @@
{"ip":"192.235.61.124","country":"Saint Lucy, BB","lat":13.2844,"lon":-59.6422,"timestamp":1750793889}

View File

@ -0,0 +1 @@
{"ip":"177.174.209.244","country":"SA","lat":-7.19111,"lon":-48.2072,"timestamp":1751572002}

View File

@ -0,0 +1 @@
{"ip":"152.206.192.67","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751051386}

View File

@ -0,0 +1 @@
{"ip":"46.216.225.50","country":"Minsk, BY","lat":54.1554,"lon":27.2412,"timestamp":1750990703}

View File

@ -0,0 +1 @@
{"ip":"191.156.59.212","country":"SA","lat":4.60971,"lon":-74.0817,"timestamp":1751590867}

View File

@ -0,0 +1 @@
{"ip":"5.91.62.206","country":"Lombardy, IT","lat":45.4642,"lon":9.18998,"timestamp":1751101354}

View File

@ -0,0 +1 @@
{"ip":"10.232.191.48","country":"ZZ","lat":0,"lon":0,"timestamp":1751025759}

View File

@ -0,0 +1 @@
{"ip":"152.207.240.30","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1751032184}

View File

@ -0,0 +1 @@
{"ip":"177.26.74.178","country":"SA","lat":-22.9984,"lon":-43.3655,"timestamp":1751593753}

View File

@ -0,0 +1 @@
{"ip":"149.22.84.152","country":"California, US","lat":37.3387,"lon":-121.885,"timestamp":1751572002}

View File

@ -0,0 +1 @@
{"ip":"152.207.165.108","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1750813150}

View File

@ -0,0 +1 @@
{"ip":"152.207.149.0","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1750988482}

View File

@ -0,0 +1 @@
{"ip":"152.206.190.255","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751577759}

View File

@ -0,0 +1 @@
{"ip":"152.207.57.200","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750975562}

View File

@ -0,0 +1 @@
{"ip":"152.206.192.9","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751137147}

View File

@ -0,0 +1 @@
{"ip":"10.226.130.196","country":"ZZ","lat":0,"lon":0,"timestamp":1751582330}

View File

@ -0,0 +1 @@
{"ip":"152.206.186.58","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751152404}

View File

@ -0,0 +1 @@
{"ip":"46.216.173.39","country":"Minsk City, BY","lat":53.9,"lon":27.5667,"timestamp":1751019634}

View File

@ -0,0 +1 @@
{"ip":"186.77.138.137","country":"South Caribbean Coast, NI","lat":12.0137,"lon":-83.7635,"timestamp":1750942487}

View File

@ -0,0 +1 @@
{"ip":"152.206.8.112","country":"Havana, CU","lat":23.1231,"lon":-82.4197,"timestamp":1751172339}

View File

@ -0,0 +1 @@
{"ip":"152.206.121.214","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1751151984}

View File

@ -0,0 +1 @@
{"ip":"191.156.60.99","country":"SA","lat":4.93658,"lon":-73.8331,"timestamp":1751125192}

View File

@ -0,0 +1 @@
{"ip":"152.206.192.139","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751587802}

View File

@ -0,0 +1 @@
{"ip":"152.206.187.156","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751205413}

View File

@ -0,0 +1 @@
{"ip":"152.206.231.24","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1750920243}

View File

@ -0,0 +1 @@
{"ip":"152.207.147.119","country":"Havana, CU","lat":23.0954,"lon":-82.3267,"timestamp":1751646516}

View File

@ -0,0 +1 @@
{"ip":"152.207.27.216","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751107958}

View File

@ -0,0 +1 @@
{"ip":"152.206.208.182","country":"Matanzas Province, CU","lat":23.0366,"lon":-81.206,"timestamp":1751627705}

View File

@ -0,0 +1 @@
{"ip":"152.207.223.76","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751192753}

View File

@ -0,0 +1 @@
{"ip":"169.158.83.60","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751625061}

View File

@ -0,0 +1 @@
{"ip":"152.206.20.84","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751594174}

View File

@ -0,0 +1 @@
{"ip":"198.71.52.179","country":"Texas, US","lat":33.1384,"lon":-95.6011,"timestamp":1751034718}

View File

@ -0,0 +1 @@
{"ip":"152.207.133.138","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751595977}

View File

@ -0,0 +1 @@
{"ip":"152.207.223.100","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751589063}

View File

@ -0,0 +1 @@
{"ip":"201.238.32.144","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1751104537}

View File

@ -0,0 +1 @@
{"ip":"152.207.135.67","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751052348}

Some files were not shown because too many files have changed in this diff Show More