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

693 lines
24 KiB
PHP

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapa de Calor - 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>