Spaces:
Running
Running
| <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: '© <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> | |