Initial commit - Proyecto de mapa
This commit is contained in:
commit
de542421c2
158
config.php
Executable file
158
config.php
Executable 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
67
data.php
Executable 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
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
125
import_csv_to_sqlite.php
Normal 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
1
import_success.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Importación completada: 3310843 registros
|
||||||
368
index.php
Executable file
368
index.php
Executable 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: '© <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
692
index2.php
Normal 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
837
index3.php
Normal 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
876
index4.php
Normal 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
336
index5.php
Normal 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
369
index6.php
Normal 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>
|
||||||
1
ip_cache/0001ff814036856198c8ffdfff0ca435.json
Normal file
1
ip_cache/0001ff814036856198c8ffdfff0ca435.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.144.169","country":"Havana, CU","lat":23.1252,"lon":-82.3007,"timestamp":1751593453}
|
||||||
1
ip_cache/000a9dc06e13c2cddf5cfe320a61bd2e.json
Normal file
1
ip_cache/000a9dc06e13c2cddf5cfe320a61bd2e.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"146.70.182.219","country":"Piedmont, IT","lat":45.8459,"lon":8.41527,"timestamp":1750793829}
|
||||||
1
ip_cache/000b00a3a218ba042b2254c3da8b8293.json
Normal file
1
ip_cache/000b00a3a218ba042b2254c3da8b8293.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"213.87.138.231","country":"Moscow Oblast, RU","lat":55.7017,"lon":36.1932,"timestamp":1751205413}
|
||||||
1
ip_cache/000b05f832d7e482c16284ad7ff910db.json
Normal file
1
ip_cache/000b05f832d7e482c16284ad7ff910db.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"96.126.104.130","country":"New Jersey, US","lat":40.8218,"lon":-74.45,"timestamp":1751155466}
|
||||||
1
ip_cache/000c60976c3c0b1c8219a187c9d5f1ba.json
Normal file
1
ip_cache/000c60976c3c0b1c8219a187c9d5f1ba.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.192.72","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751590265}
|
||||||
1
ip_cache/000cbfe4d271aa944bd190f1971b3b64.json
Normal file
1
ip_cache/000cbfe4d271aa944bd190f1971b3b64.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"129.222.1.213","country":"Florida, US","lat":25.7617,"lon":-80.1918,"timestamp":1751042073}
|
||||||
1
ip_cache/0016622470187916ecb76ce5877de5d5.json
Normal file
1
ip_cache/0016622470187916ecb76ce5877de5d5.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.150.204","country":"Havana, CU","lat":23.0954,"lon":-82.3267,"timestamp":1751025097}
|
||||||
1
ip_cache/0017309d0a1d0ec46c94b40129f57e38.json
Normal file
1
ip_cache/0017309d0a1d0ec46c94b40129f57e38.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"191.156.49.37","country":"SA","lat":4.60971,"lon":-74.0817,"timestamp":1751638898}
|
||||||
1
ip_cache/001b9aff5ac620cb0fbda7727d3922e0.json
Normal file
1
ip_cache/001b9aff5ac620cb0fbda7727d3922e0.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"190.86.107.218","country":"San Miguel Department, SV","lat":13.5,"lon":-88.35,"timestamp":1751149040}
|
||||||
1
ip_cache/001c396fbfd36551884600b4baadd746.json
Normal file
1
ip_cache/001c396fbfd36551884600b4baadd746.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"172.56.2.168","country":"Virginia, US","lat":38.7318,"lon":-77.4311,"timestamp":1750887933}
|
||||||
1
ip_cache/001caa5908b13ad87ef8d72e0bf3094d.json
Normal file
1
ip_cache/001caa5908b13ad87ef8d72e0bf3094d.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"172.236.103.4","country":"Illinois, US","lat":41.8781,"lon":-87.6298,"timestamp":1751036709}
|
||||||
1
ip_cache/001e2e0fba42dde8835442aab45b6131.json
Normal file
1
ip_cache/001e2e0fba42dde8835442aab45b6131.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.156.173.246","country":"SA","lat":-32.3703,"lon":-54.1675,"timestamp":1751112881}
|
||||||
1
ip_cache/0023160d586cffbe63b9ae19bd968cf9.json
Normal file
1
ip_cache/0023160d586cffbe63b9ae19bd968cf9.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.149.203","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1751577316}
|
||||||
1
ip_cache/0026bf947493094ab46b4497ee773af9.json
Normal file
1
ip_cache/0026bf947493094ab46b4497ee773af9.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.146.165","country":"Cienfuegos Province, CU","lat":22.4148,"lon":-80.2931,"timestamp":1751041891}
|
||||||
1
ip_cache/002e72e408b9a899e0bbf92d59e9192d.json
Normal file
1
ip_cache/002e72e408b9a899e0bbf92d59e9192d.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"176.33.60.170","country":"AS","lat":41.0605,"lon":28.9872,"timestamp":1750884121}
|
||||||
1
ip_cache/0033dd19e890d81705bd811f967cf751.json
Normal file
1
ip_cache/0033dd19e890d81705bd811f967cf751.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.148.61","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751205173}
|
||||||
1
ip_cache/0035585fa8db25e0c4435b2b7f01dc3e.json
Normal file
1
ip_cache/0035585fa8db25e0c4435b2b7f01dc3e.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.193.104","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751578842}
|
||||||
1
ip_cache/00367a0b245197133279365557ed74a0.json
Normal file
1
ip_cache/00367a0b245197133279365557ed74a0.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.146.211","country":"Cienfuegos Province, CU","lat":22.4148,"lon":-80.2931,"timestamp":1751046609}
|
||||||
1
ip_cache/00376bfdc61bb4833f8c3da5b2501449.json
Normal file
1
ip_cache/00376bfdc61bb4833f8c3da5b2501449.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.211.219","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751031943}
|
||||||
1
ip_cache/0037a740467cce61e75f988b0388e62c.json
Normal file
1
ip_cache/0037a740467cce61e75f988b0388e62c.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"186.167.160.221","country":"SA","lat":10.488,"lon":-66.8792,"timestamp":1751140690}
|
||||||
1
ip_cache/00443311cac5f59065e7d3f61b0e13f9.json
Normal file
1
ip_cache/00443311cac5f59065e7d3f61b0e13f9.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"188.70.57.237","country":"AS","lat":29.1908,"lon":48.1135,"timestamp":1750870225}
|
||||||
1
ip_cache/004a449fcb0a2d04e47cd2ec0976a545.json
Normal file
1
ip_cache/004a449fcb0a2d04e47cd2ec0976a545.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.133.11","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751054335}
|
||||||
1
ip_cache/004eacaddc2d97230ae2ab26b20e86af.json
Normal file
1
ip_cache/004eacaddc2d97230ae2ab26b20e86af.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"201.238.32.231","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1750887635}
|
||||||
1
ip_cache/0059dbc84d0037cec16daa17b32f7e1c.json
Normal file
1
ip_cache/0059dbc84d0037cec16daa17b32f7e1c.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.59.150","country":"Santiago de Cuba Province, CU","lat":20.0208,"lon":-75.8267,"timestamp":1751208293}
|
||||||
1
ip_cache/0059f1bf3cf7654d215e09b12ccf556f.json
Normal file
1
ip_cache/0059f1bf3cf7654d215e09b12ccf556f.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"106.219.231.195","country":"AS","lat":28.4601,"lon":77.0264,"timestamp":1750818207}
|
||||||
1
ip_cache/005c1eb726a0ba8a8947fb5c171da777.json
Normal file
1
ip_cache/005c1eb726a0ba8a8947fb5c171da777.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.184.148","country":"Holgu\u00edn Province, CU","lat":20.8872,"lon":-76.2631,"timestamp":1751130299}
|
||||||
1
ip_cache/0060c335239cc44be62364e79ccf5383.json
Normal file
1
ip_cache/0060c335239cc44be62364e79ccf5383.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"66.228.38.146","country":"New Jersey, US","lat":40.8218,"lon":-74.45,"timestamp":1750890389}
|
||||||
1
ip_cache/00640e72341c21181722144f655b1b5a.json
Normal file
1
ip_cache/00640e72341c21181722144f655b1b5a.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.211.201","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750816584}
|
||||||
1
ip_cache/0064dc002d949c7ec2fdc2ea0d0c7f99.json
Normal file
1
ip_cache/0064dc002d949c7ec2fdc2ea0d0c7f99.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.20.93","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751590626}
|
||||||
1
ip_cache/0065156a89f2830683759d84cf12f306.json
Normal file
1
ip_cache/0065156a89f2830683759d84cf12f306.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.211.93","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750903003}
|
||||||
1
ip_cache/00674ee37084958c676f2630e9c1f211.json
Normal file
1
ip_cache/00674ee37084958c676f2630e9c1f211.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.10.228","country":"Havana, CU","lat":23.1231,"lon":-82.4197,"timestamp":1750960602}
|
||||||
1
ip_cache/006d7d2165e69597e6f4f007bbfb7c16.json
Normal file
1
ip_cache/006d7d2165e69597e6f4f007bbfb7c16.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.184.114","country":"Holgu\u00edn Province, CU","lat":20.8872,"lon":-76.2631,"timestamp":1751108738}
|
||||||
1
ip_cache/006d9244587c9dafbac22b2a1ea46b99.json
Normal file
1
ip_cache/006d9244587c9dafbac22b2a1ea46b99.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"135.129.119.20","country":"Florida, US","lat":25.7617,"lon":-80.1918,"timestamp":1750919463}
|
||||||
1
ip_cache/00715cb55ab06437cb1a1c773bb64038.json
Normal file
1
ip_cache/00715cb55ab06437cb1a1c773bb64038.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"212.5.158.96","country":"Sofia-grad, BG","lat":42.6975,"lon":23.3242,"timestamp":1750854745}
|
||||||
1
ip_cache/007177855a6535911cff0590aab972da.json
Normal file
1
ip_cache/007177855a6535911cff0590aab972da.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"201.238.1.120","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1751210100}
|
||||||
1
ip_cache/0076d06910ebbb17edfc4480353382d0.json
Normal file
1
ip_cache/0076d06910ebbb17edfc4480353382d0.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"151.18.108.38","country":"Piedmont, IT","lat":45.0445,"lon":7.61408,"timestamp":1751120868}
|
||||||
1
ip_cache/0077dd0dd3b779934f644363dfc6c1d5.json
Normal file
1
ip_cache/0077dd0dd3b779934f644363dfc6c1d5.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.120.151","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1750782297}
|
||||||
1
ip_cache/0078ef5f4f325c9523c46eb17a10eb7a.json
Normal file
1
ip_cache/0078ef5f4f325c9523c46eb17a10eb7a.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.210.28","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1750961023}
|
||||||
1
ip_cache/007af3c3ae5fc6aa27568a00eddc9e24.json
Normal file
1
ip_cache/007af3c3ae5fc6aa27568a00eddc9e24.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"191.156.125.111","country":"SA","lat":4.71638,"lon":-74.212,"timestamp":1751572002}
|
||||||
1
ip_cache/007cf7f0ab1d7162fffa719356b9b8a5.json
Normal file
1
ip_cache/007cf7f0ab1d7162fffa719356b9b8a5.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.144.14","country":"Havana, CU","lat":23.1252,"lon":-82.3007,"timestamp":1751052288}
|
||||||
1
ip_cache/007e59dbe13cc529848d64ee54457a30.json
Normal file
1
ip_cache/007e59dbe13cc529848d64ee54457a30.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"212.30.33.227","country":"Madrid, ES","lat":40.4167,"lon":-3.70329,"timestamp":1751092050}
|
||||||
1
ip_cache/007ec03693aa46e4f3396d200f877c15.json
Normal file
1
ip_cache/007ec03693aa46e4f3396d200f877c15.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"191.156.233.16","country":"SA","lat":5.53528,"lon":-73.3678,"timestamp":1750812005}
|
||||||
1
ip_cache/00802fd33bb359bcb6f0c43afb74e451.json
Normal file
1
ip_cache/00802fd33bb359bcb6f0c43afb74e451.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.34.179","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751040751}
|
||||||
1
ip_cache/00811c528c5d6e130607a5e1a3301274.json
Normal file
1
ip_cache/00811c528c5d6e130607a5e1a3301274.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"94.109.171.166","country":"Wallonia, BE","lat":50.5541,"lon":4.10082,"timestamp":1751606013}
|
||||||
1
ip_cache/0084b36d5ea60d2207cf450e1c5e6a31.json
Normal file
1
ip_cache/0084b36d5ea60d2207cf450e1c5e6a31.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.27.53","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751608115}
|
||||||
1
ip_cache/00859740e953f06df1864ad289f2304d.json
Normal file
1
ip_cache/00859740e953f06df1864ad289f2304d.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.191.101","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751104537}
|
||||||
1
ip_cache/008cdb9a0fb80fb28c3ad4fd9482a5be.json
Normal file
1
ip_cache/008cdb9a0fb80fb28c3ad4fd9482a5be.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.132.120","country":"Camag\u00fcey, CU","lat":21.5254,"lon":-78.2258,"timestamp":1751572002}
|
||||||
1
ip_cache/008eb7832ef00c5f7c82746e1d1597c5.json
Normal file
1
ip_cache/008eb7832ef00c5f7c82746e1d1597c5.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"177.174.212.235","country":"SA","lat":-15.7797,"lon":-47.9297,"timestamp":1750825232}
|
||||||
1
ip_cache/0091a014532ec966fa6a5672acab539c.json
Normal file
1
ip_cache/0091a014532ec966fa6a5672acab539c.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.193.206","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1751646166}
|
||||||
1
ip_cache/009282b6561fbf29de71be02cb8ec543.json
Normal file
1
ip_cache/009282b6561fbf29de71be02cb8ec543.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"186.13.200.171","country":"SA","lat":-25.3468,"lon":-57.6065,"timestamp":1751008229}
|
||||||
1
ip_cache/00937d5a00dd69e348e077dabbc1d643.json
Normal file
1
ip_cache/00937d5a00dd69e348e077dabbc1d643.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"212.227.226.143","country":"Madrid, ES","lat":40.4167,"lon":-3.70329,"timestamp":1750869211}
|
||||||
1
ip_cache/00952982da619e806f3c198a58b2920f.json
Normal file
1
ip_cache/00952982da619e806f3c198a58b2920f.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"105.232.128.39","country":"AF","lat":-22.5594,"lon":17.0832,"timestamp":1750783675}
|
||||||
1
ip_cache/0095e73e25d51998cc0dd181829cd250.json
Normal file
1
ip_cache/0095e73e25d51998cc0dd181829cd250.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"186.13.203.101","country":"SA","lat":-25.5097,"lon":-54.6111,"timestamp":1750891741}
|
||||||
1
ip_cache/00967a0ec94fd06eea1bc7575dca5510.json
Normal file
1
ip_cache/00967a0ec94fd06eea1bc7575dca5510.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"192.235.61.124","country":"Saint Lucy, BB","lat":13.2844,"lon":-59.6422,"timestamp":1750793889}
|
||||||
1
ip_cache/00974acf86314a913abc4dd0b3215597.json
Normal file
1
ip_cache/00974acf86314a913abc4dd0b3215597.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"177.174.209.244","country":"SA","lat":-7.19111,"lon":-48.2072,"timestamp":1751572002}
|
||||||
1
ip_cache/00975e9b11b5c361e54bd9c50f41141c.json
Normal file
1
ip_cache/00975e9b11b5c361e54bd9c50f41141c.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.192.67","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751051386}
|
||||||
1
ip_cache/009934153a61ae02ec11d74e7936ff68.json
Normal file
1
ip_cache/009934153a61ae02ec11d74e7936ff68.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"46.216.225.50","country":"Minsk, BY","lat":54.1554,"lon":27.2412,"timestamp":1750990703}
|
||||||
1
ip_cache/00a313f94799617d86a6e864d7991f4b.json
Normal file
1
ip_cache/00a313f94799617d86a6e864d7991f4b.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"191.156.59.212","country":"SA","lat":4.60971,"lon":-74.0817,"timestamp":1751590867}
|
||||||
1
ip_cache/00a480b7237ffe00b2c9d05e5c2e992f.json
Normal file
1
ip_cache/00a480b7237ffe00b2c9d05e5c2e992f.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"5.91.62.206","country":"Lombardy, IT","lat":45.4642,"lon":9.18998,"timestamp":1751101354}
|
||||||
1
ip_cache/00a5f6e5c043406c3b0e92ad4279fc83.json
Normal file
1
ip_cache/00a5f6e5c043406c3b0e92ad4279fc83.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"10.232.191.48","country":"ZZ","lat":0,"lon":0,"timestamp":1751025759}
|
||||||
1
ip_cache/00aa6017f1ba365c0da9331c15ec62af.json
Normal file
1
ip_cache/00aa6017f1ba365c0da9331c15ec62af.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.240.30","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1751032184}
|
||||||
1
ip_cache/00b0594f519eafcb47cd3fabe3cf278a.json
Normal file
1
ip_cache/00b0594f519eafcb47cd3fabe3cf278a.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"177.26.74.178","country":"SA","lat":-22.9984,"lon":-43.3655,"timestamp":1751593753}
|
||||||
1
ip_cache/00b336d5323e263e1b5c53cd13469f2b.json
Normal file
1
ip_cache/00b336d5323e263e1b5c53cd13469f2b.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"149.22.84.152","country":"California, US","lat":37.3387,"lon":-121.885,"timestamp":1751572002}
|
||||||
1
ip_cache/00b3737ab0338bea6259f27effce7e43.json
Normal file
1
ip_cache/00b3737ab0338bea6259f27effce7e43.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.165.108","country":"Havana, CU","lat":23.133,"lon":-82.383,"timestamp":1750813150}
|
||||||
1
ip_cache/00ba0d857a5c5a7cc7418153f8e4ba48.json
Normal file
1
ip_cache/00ba0d857a5c5a7cc7418153f8e4ba48.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.149.0","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1750988482}
|
||||||
1
ip_cache/00ba8bad5330b8f07bb93636ecea2851.json
Normal file
1
ip_cache/00ba8bad5330b8f07bb93636ecea2851.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.190.255","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751577759}
|
||||||
1
ip_cache/00bc10eb8559b6f6367ce9149f545d74.json
Normal file
1
ip_cache/00bc10eb8559b6f6367ce9149f545d74.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.57.200","country":"Villa Clara Province, CU","lat":22.4069,"lon":-79.9647,"timestamp":1750975562}
|
||||||
1
ip_cache/00c4fcfe3eee467106dae82c815ddb57.json
Normal file
1
ip_cache/00c4fcfe3eee467106dae82c815ddb57.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.192.9","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751137147}
|
||||||
1
ip_cache/00cda0df459b8e87cc0fa94361978638.json
Normal file
1
ip_cache/00cda0df459b8e87cc0fa94361978638.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"10.226.130.196","country":"ZZ","lat":0,"lon":0,"timestamp":1751582330}
|
||||||
1
ip_cache/00d29108d1554d9f990c615c8beb48bc.json
Normal file
1
ip_cache/00d29108d1554d9f990c615c8beb48bc.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.186.58","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751152404}
|
||||||
1
ip_cache/00d2a2b9301f519955be96316b71c94d.json
Normal file
1
ip_cache/00d2a2b9301f519955be96316b71c94d.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"46.216.173.39","country":"Minsk City, BY","lat":53.9,"lon":27.5667,"timestamp":1751019634}
|
||||||
1
ip_cache/00d6490c6d32905109bc0a9a742e6809.json
Normal file
1
ip_cache/00d6490c6d32905109bc0a9a742e6809.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"186.77.138.137","country":"South Caribbean Coast, NI","lat":12.0137,"lon":-83.7635,"timestamp":1750942487}
|
||||||
1
ip_cache/00ddca505635c23888e9ca957b68a787.json
Normal file
1
ip_cache/00ddca505635c23888e9ca957b68a787.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.8.112","country":"Havana, CU","lat":23.1231,"lon":-82.4197,"timestamp":1751172339}
|
||||||
1
ip_cache/00e1f8af9496c09bc0673651c3dbc891.json
Normal file
1
ip_cache/00e1f8af9496c09bc0673651c3dbc891.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.121.214","country":"Camag\u00fcey, CU","lat":21.3808,"lon":-77.9169,"timestamp":1751151984}
|
||||||
1
ip_cache/00e2e769111dc9c2e17e6c6303cd47a3.json
Normal file
1
ip_cache/00e2e769111dc9c2e17e6c6303cd47a3.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"191.156.60.99","country":"SA","lat":4.93658,"lon":-73.8331,"timestamp":1751125192}
|
||||||
1
ip_cache/00e48cd88059f182d03d2fa360a8695f.json
Normal file
1
ip_cache/00e48cd88059f182d03d2fa360a8695f.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.192.139","country":"Havana, CU","lat":23.1648,"lon":-82.3012,"timestamp":1751587802}
|
||||||
1
ip_cache/00e73bcf215bef12206248e04ba58cda.json
Normal file
1
ip_cache/00e73bcf215bef12206248e04ba58cda.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.187.156","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751205413}
|
||||||
1
ip_cache/00ea1f977681408137dd41e4c7763800.json
Normal file
1
ip_cache/00ea1f977681408137dd41e4c7763800.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.231.24","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1750920243}
|
||||||
1
ip_cache/00eb77664e5edbe58053ce189831b424.json
Normal file
1
ip_cache/00eb77664e5edbe58053ce189831b424.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.147.119","country":"Havana, CU","lat":23.0954,"lon":-82.3267,"timestamp":1751646516}
|
||||||
1
ip_cache/00efde7e67dee13305ccd93c73cc1f84.json
Normal file
1
ip_cache/00efde7e67dee13305ccd93c73cc1f84.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.27.216","country":"Havana, CU","lat":23.0739,"lon":-82.4189,"timestamp":1751107958}
|
||||||
1
ip_cache/00f193f6a87ecaa58294a24e6433a96e.json
Normal file
1
ip_cache/00f193f6a87ecaa58294a24e6433a96e.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.208.182","country":"Matanzas Province, CU","lat":23.0366,"lon":-81.206,"timestamp":1751627705}
|
||||||
1
ip_cache/01006307f65e16bbd630a3c6103588f4.json
Normal file
1
ip_cache/01006307f65e16bbd630a3c6103588f4.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.223.76","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751192753}
|
||||||
1
ip_cache/010537f9f3d3f3e726c40909eb4b20de.json
Normal file
1
ip_cache/010537f9f3d3f3e726c40909eb4b20de.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"169.158.83.60","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751625061}
|
||||||
1
ip_cache/01071d2c23fa5f200c5a2c2fc145fe2d.json
Normal file
1
ip_cache/01071d2c23fa5f200c5a2c2fc145fe2d.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.206.20.84","country":"Havana, CU","lat":23.1136,"lon":-82.3666,"timestamp":1751594174}
|
||||||
1
ip_cache/01077cfa817f8097afd9dd5f63a626ff.json
Normal file
1
ip_cache/01077cfa817f8097afd9dd5f63a626ff.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"198.71.52.179","country":"Texas, US","lat":33.1384,"lon":-95.6011,"timestamp":1751034718}
|
||||||
1
ip_cache/010918762e944fead788b9a974859b7f.json
Normal file
1
ip_cache/010918762e944fead788b9a974859b7f.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.133.138","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751595977}
|
||||||
1
ip_cache/010930089e69117d99583ce3f5e7a82c.json
Normal file
1
ip_cache/010930089e69117d99583ce3f5e7a82c.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"152.207.223.100","country":"Havana, CU","lat":23.1081,"lon":-82.3866,"timestamp":1751589063}
|
||||||
1
ip_cache/01108febccc151152849e0c9a1dd1536.json
Normal file
1
ip_cache/01108febccc151152849e0c9a1dd1536.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"ip":"201.238.32.144","country":"SA","lat":10.162,"lon":-68.0077,"timestamp":1751104537}
|
||||||
1
ip_cache/01146408c9b2cbddb8d72dcadfb11e77.json
Normal file
1
ip_cache/01146408c9b2cbddb8d72dcadfb11e77.json
Normal 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
Loading…
x
Reference in New Issue
Block a user