lima-wind-whisperer / index.html
quangchi's picture
Objetivo
08c2b86 verified
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vientos de Lima | Wind Whisperer</title>
<link rel="icon" type="image/x-icon" href="https://static.photos/blue/200x200/42">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://cdn.jsdelivr.net/npm/chroma-js@2.4.2/chroma.min.js"></script>
<style>
#map {
height: 100vh;
width: 100%;
position: relative;
}
.wind-arrow {
position: absolute;
transform-origin: center;
stroke-linecap: round;
}
.legend {
background: rgba(255, 255, 255, 0.9);
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.legend-gradient {
height: 20px;
width: 100%;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="flex flex-col h-screen">
<header class="bg-gradient-to-r from-blue-500 to-cyan-400 text-white p-4 shadow-md">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-2xl font-bold flex items-center">
<i data-feather="wind" class="mr-2"></i> Lima Wind Whisperer
</h1>
<div class="flex items-center space-x-4">
<div class="flex items-center">
<label for="unit-select" class="mr-2 text-sm font-medium">Unidad:</label>
<select id="unit-select" class="bg-white/20 border border-white/30 rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-white">
<option value="kmh">km/h</option>
<option value="ms">m/s</option>
<option value="knots">nudos</option>
</select>
</div>
<button id="refresh-btn" class="flex items-center bg-white/20 hover:bg-white/30 px-3 py-1 rounded transition">
<i data-feather="refresh-cw" class="mr-1" width="16"></i>
<span class="text-sm">Actualizar</span>
</button>
</div>
</div>
</header>
<div id="map"></div>
<div id="legend" class="absolute bottom-4 right-4 z-[1000] legend">
<h3 class="font-bold text-gray-800 mb-2">Velocidad del viento</h3>
<div id="legend-gradient" class="legend-gradient"></div>
<div class="flex justify-between text-xs text-gray-600">
<span>0</span>
<span>20</span>
<span>40 km/h</span>
</div>
</div>
<div id="tooltip" class="absolute bg-white p-3 rounded shadow-lg z-[1000] hidden">
<div class="flex items-center mb-1">
<i data-feather="map-pin" class="text-blue-500 mr-2" width="16"></i>
<span id="location" class="font-medium"></span>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="flex items-center">
<i data-feather="compass" class="text-gray-500 mr-2" width="14"></i>
<span id="direction"></span>
</div>
<div class="flex items-center">
<i data-feather="wind" class="text-gray-500 mr-2" width="14"></i>
<span id="speed"></span>
</div>
</div>
</div>
</div>
<script>
// Configuración inicial
const config = {
center: [-12.0464, -77.0428], // Lima
zoom: 11,
minZoom: 8,
maxZoom: 18,
windDataUrl: 'https://api.open-meteo.com/v1/gfs?latitude=-12.0464&longitude=-77.0428&hourly=windspeed_10m,winddirection_10m',
updateInterval: 600000 // 10 minutos
};
// Inicializar mapa
const map = L.map('map', {
center: config.center,
zoom: config.zoom,
minZoom: config.minZoom,
maxZoom: config.maxZoom
});
// Añadir capa base
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Escala de colores para la velocidad del viento
const colorScale = chroma.scale(['#4cc9f0', '#4361ee', '#3a0ca3', '#7209b7', '#f72585']).domain([0, 40]);
// Actualizar gradiente de leyenda
function updateLegendGradient() {
const gradient = document.getElementById('legend-gradient');
gradient.style.background = `linear-gradient(to right, ${colorScale(0).hex()}, ${colorScale(10).hex()}, ${colorScale(20).hex()}, ${colorScale(30).hex()}, ${colorScale(40).hex()})`;
}
// Convertir dirección meteorológica a cardinal
function degreesToCardinal(degrees) {
const cardinals = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const index = Math.round((degrees % 360) / 22.5);
return cardinals[(index % 16)];
}
// Dibujar flecha de viento
function drawWindArrow(latlng, speed, direction, unit = 'kmh') {
// Normalizar velocidad según unidad
let displaySpeed;
let displayUnit;
switch(unit) {
case 'ms':
displaySpeed = speed / 3.6;
displayUnit = 'm/s';
break;
case 'knots':
displaySpeed = speed / 1.852;
displayUnit = 'kt';
break;
default:
displaySpeed = speed;
displayUnit = 'km/h';
}
// Convertir dirección meteorológica (de dónde viene) a dirección gráfica (hacia dónde va)
const graphicDirection = (direction + 180) % 360;
// Calcular tamaño proporcional a la velocidad (con límites)
const minSize = 10;
const maxSize = 30;
const size = Math.min(maxSize, Math.max(minSize, speed / 2));
// Crear elemento SVG para la flecha
const arrow = document.createElementNS("http://www.w3.org/2000/svg", "svg");
arrow.setAttribute("width", size * 2);
arrow.setAttribute("height", size * 2);
arrow.setAttribute("viewBox", "0 0 24 24");
arrow.className = "wind-arrow";
arrow.setAttribute("data-speed", speed);
arrow.setAttribute("data-direction", direction);
arrow.setAttribute("data-lat", latlng.lat);
arrow.setAttribute("data-lng", latlng.lng);
arrow.setAttribute("data-unit", unit);
// Definir la flecha como un path SVG
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M12 2L4 12L8 12L8 22L16 22L16 12L20 12L12 2Z");
path.setAttribute("fill", colorScale(speed).hex());
path.setAttribute("stroke", "#fff");
path.setAttribute("stroke-width", "0.5");
arrow.appendChild(path);
arrow.style.transform = `rotate(${graphicDirection}deg)`;
// Convertir a capa Leaflet
const icon = L.divIcon({
html: arrow.outerHTML,
className: '',
iconSize: [size, size]
});
const marker = L.marker(latlng, {
icon: icon,
interactive: true
}).addTo(map);
// Eventos para tooltip
marker.on('mouseover', function(e) {
const tooltip = document.getElementById('tooltip');
tooltip.style.left = `${e.originalEvent.clientX + 10}px`;
tooltip.style.top = `${e.originalEvent.clientY + 10}px`;
tooltip.classList.remove('hidden');
document.getElementById('location').textContent = `${latlng.lat.toFixed(4)}, ${latlng.lng.toFixed(4)}`;
document.getElementById('direction').textContent = `${direction}° ${degreesToCardinal(direction)}`;
document.getElementById('speed').textContent = `${displaySpeed.toFixed(1)} ${displayUnit}`;
});
marker.on('mouseout', function() {
document.getElementById('tooltip').classList.add('hidden');
});
return marker;
}
// Generar datos de viento simulados (en una aplicación real, esto vendría de una API)
function generateWindData() {
const data = [];
const gridSize = 0.1;
for(let lat = -12.2; lat <= -11.9; lat += gridSize) {
for(let lng = -77.2; lng <= -76.8; lng += gridSize) {
// Simular variaciones de velocidad y dirección
const baseSpeed = 5 + Math.random() * 20;
const speed = baseSpeed + Math.sin(lat * 10) * 5 + Math.cos(lng * 10) * 5;
const direction = (270 + (lat + 12) * 100 + (lng + 77) * 50) % 360;
data.push({
latlng: [lat, lng],
speed: Math.max(0, speed),
direction: direction
});
}
}
return data;
}
// Actualizar visualización con nuevos datos
function updateWindData(unit = 'kmh') {
// Limpiar marcadores existentes
map.eachLayer(layer => {
if (layer instanceof L.Marker) {
map.removeLayer(layer);
}
});
// Generar o cargar datos
const windData = generateWindData();
// Dibujar flechas según el nivel de zoom
const currentZoom = map.getZoom();
let sampleRate = 1;
if (currentZoom < 10) sampleRate = 4;
else if (currentZoom < 12) sampleRate = 2;
for(let i = 0; i < windData.length; i += sampleRate) {
const point = windData[i];
drawWindArrow(
L.latLng(point.latlng[0], point.latlng[1]),
point.speed,
point.direction,
unit
);
}
}
// Manejar cambios en la unidad
document.getElementById('unit-select').addEventListener('change', function() {
updateWindData(this.value);
});
// Manejar actualización manual
document.getElementById('refresh-btn').addEventListener('click', function() {
updateWindData(document.getElementById('unit-select').value);
});
// Manejar cambios de zoom
map.on('zoomend', function() {
updateWindData(document.getElementById('unit-select').value);
});
// Actualizar automáticamente
function startAutoRefresh() {
updateWindData();
updateLegendGradient();
setInterval(() => {
updateWindData(document.getElementById('unit-select').value);
}, config.updateInterval);
}
// Inicializar
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
startAutoRefresh();
});
</script>
</body>
</html>