Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>India Climate Map</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | |
| <link rel="stylesheet" type="text/css" href="/static/styles.css"> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; background: #000; color: #e5e7eb; } | |
| #map { height: 90vh; border-radius: 0.75rem; } | |
| .loader { | |
| border: 4px solid #222; | |
| border-top: 4px solid #10b981; | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} } | |
| </style> | |
| </head> | |
| <body class="bg-black text-gray-200 antialiased"> | |
| <nav class="nav"> | |
| <img class="nav-img" src="{{ url_for('static', filename='assets/NRSC.png') }}" alt="NRSC-ISRO logo"> | |
| <ul> | |
| <li class="active"><a href="/" style="padding: 12px;">Home</a></li><br> | |
| <li class="about"><a href="/about">About</a></li><br> | |
| <li class="info"><a href="/info">Reservoirs Info</a></li><br> | |
| <li class="qr"><a href="/qr">QR Scanner</a></li><br> | |
| <li class="command"><a href="/command">Face Recognition</a></li><br> | |
| <li class="tutorial"><a href="/voiceassistant">Voice Assistant</a></li><br> | |
| <li class="climatemap"><a href="/climatemap">Climate Map</a></li> | |
| </ul> | |
| </nav> | |
| <div class="center-container"> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-3xl md:text-4xl font-bold text-emerald-400">India Climate Map</h1> | |
| <p class="text-md text-gray-400 mt-2">Enter a city or click on the map to get live weather updates.</p> | |
| </header> | |
| <main> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8"> | |
| <div class="lg:col-span-1 bg-gray-900 p-6 rounded-xl shadow-lg"> | |
| <div class="mb-6"> | |
| <label for="city-input" class="block text-sm font-medium text-gray-300 mb-2">Search for a City</label> | |
| <div class="flex items-center space-x-2"> | |
| <input type="text" id="city-input" placeholder="e.g., Mumbai" | |
| class="w-full px-4 py-2 border border-gray-700 bg-black text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-400"/> | |
| <button id="search-button" | |
| class="px-4 py-2 bg-emerald-600 text-white font-semibold rounded-lg hover:bg-emerald-700 focus:ring-2 focus:ring-emerald-400"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"> | |
| <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <button id="radar-btn" class="w-full bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700"> | |
| Show Radar | |
| </button> | |
| </div> | |
| <div id="weather-info" class="space-y-4"></div> | |
| </div> | |
| <div class="lg:col-span-2"> | |
| <div id="map"></div> | |
| </div> | |
| </div> | |
| <div id="forecast-container" class="bg-gray-900 p-6 rounded-xl shadow-lg"> | |
| </div> | |
| </main> | |
| </div> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const apiKey = "683bae9848bec16b6e7db75f8bf0bc0e"; // your OpenWeatherMap key | |
| const initialCity = "Hyderabad"; | |
| const cityInput = document.getElementById('city-input'); | |
| const searchButton = document.getElementById('search-button'); | |
| const weatherInfoDiv = document.getElementById('weather-info'); | |
| const forecastContainer = document.getElementById('forecast-container'); | |
| const radarBtn = document.getElementById('radar-btn'); | |
| const map = L.map('map').setView([22.5937, 78.9629], 5); | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); | |
| let mapMarker; | |
| // Radar Layer | |
| const radarLayer = L.tileLayer(`https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=${apiKey}`, {opacity: 0.5}); | |
| let radarVisible = false; | |
| // ✅ Add tile recoloring logic here | |
| radarLayer.on('tileload', function (event) { | |
| let img = event.tile; | |
| let canvas = document.createElement("canvas"); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| let ctx = canvas.getContext("2d"); | |
| ctx.drawImage(img, 0, 0); | |
| let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| let data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| let alpha = data[i + 3] / 255; | |
| let percent = alpha * 100; | |
| let grey; | |
| if (percent >= 90) { | |
| grey = 0x4d; // Dark Gray | |
| } else if (percent >= 75) { | |
| grey = 0x66; // Medium Dark Gray | |
| } else if (percent >= 40) { | |
| grey = 0x99; // Medium Gray | |
| } else if (percent >= 10) { | |
| grey = 0xcc; // Light Gray | |
| } else { | |
| grey = 0xf2; // Very Light Gray | |
| } | |
| data[i] = grey; // Red | |
| data[i + 1] = grey; // Green | |
| data[i + 2] = grey; // Blue | |
| data[i + 3] = 255; // Opaque | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| img.src = canvas.toDataURL(); | |
| }); | |
| radarBtn.addEventListener('click', () => { | |
| if(!radarVisible) { | |
| radarLayer.addTo(map); | |
| radarBtn.textContent = "Hide Radar"; | |
| } else { | |
| map.removeLayer(radarLayer); | |
| radarBtn.textContent = "Show Radar"; | |
| } | |
| radarVisible = !radarVisible; | |
| }); | |
| // Fetch weather by city | |
| const getWeatherByCity = (city) => { | |
| showLoading(); | |
| fetch(`https://api.openweathermap.org/geo/1.0/direct?q=${city}&limit=1&appid=${apiKey}`) | |
| .then(res => res.json()) | |
| .then(geo => { | |
| if(!geo || geo.length===0) throw new Error("City not found"); | |
| const {lat, lon, name, state, country} = geo[0]; | |
| fetchWeatherByCoords(lat, lon, name, state, country); | |
| }).catch(err => showError(err.message)); | |
| }; | |
| // Fetch weather by coords | |
| const getWeatherByCoords = (lat, lon) => { | |
| showLoading(); | |
| fetch(`https://api.openweathermap.org/geo/1.0/reverse?lat=${lat}&lon=${lon}&limit=1&appid=${apiKey}`) | |
| .then(res => res.json()) | |
| .then(geo => { | |
| let name="", state="", country=""; | |
| if(geo && geo.length>0){ | |
| name = geo[0].name; | |
| state = geo[0].state; | |
| country = geo[0].country; | |
| } | |
| fetchWeatherByCoords(lat, lon, name, state, country); | |
| }).catch(err => showError(err.message)); | |
| }; | |
| const fetchWeatherByCoords = (lat, lon, name=null, state=null, country=null) => { | |
| fetch(`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`) | |
| .then(res => { if(!res.ok) throw new Error("Weather data not found"); return res.json(); }) | |
| .then(async data => { | |
| const cityName = name || data.name; | |
| const cityState = state || ""; | |
| let cityCountry = country || data.sys.country; | |
| try { | |
| const countryRes = await fetch(`https://restcountries.com/v3.1/alpha/${cityCountry}`); | |
| const countryJson = await countryRes.json(); | |
| cityCountry = countryJson[0]?.name?.common || cityCountry; | |
| } catch(e) { console.error("Country fetch failed:", e); } | |
| updateWeatherUI(data, cityName, cityState, cityCountry); | |
| updateMap(lat, lon, cityName, cityState, cityCountry); | |
| applyRainShadow(data); | |
| fetchForecast(lat, lon); | |
| }).catch(err => showError(err.message)); | |
| }; | |
| // Update UI | |
| const updateWeatherUI = (data, cityName, cityState, cityCountry) => { | |
| const rainChance = data.clouds ? data.clouds.all : 0; | |
| const rainVolume = data.rain ? (data.rain["1h"] || 0) : 0; | |
| weatherInfoDiv.innerHTML = ` | |
| <h2 class="text-2xl font-semibold text-emerald-400 mb-0">${cityName}</h2> | |
| <p class="text-xs text-gray-400 mb-2">${cityState ? cityState + ", " : ""}${cityCountry}</p> | |
| <p class="capitalize text-gray-400 mb-2">${data.weather[0].description}</p> | |
| <img src="https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png" class="mx-auto"> | |
| <p class="text-3xl font-bold text-white mb-4">${Math.round(data.main.temp)}°C</p> | |
| <div class="grid grid-cols-2 gap-4 text-sm"> | |
| <div class="flex items-center bg-gray-800 p-3 rounded-lg"> | |
| <i data-lucide="droplet" class="w-5 h-5 text-emerald-400 mr-2"></i> | |
| <span>Humidity: <span class="text-emerald-400">${data.main.humidity}%</span></span> | |
| </div> | |
| <div class="flex items-center bg-gray-800 p-3 rounded-lg"> | |
| <i data-lucide="wind" class="w-5 h-5 text-emerald-400 mr-2"></i> | |
| <span>Wind: <span class="text-emerald-400">${data.wind.speed} m/s</span></span> | |
| </div> | |
| <div class="flex items-center bg-gray-800 p-3 rounded-lg"> | |
| <i data-lucide="cloud-rain" class="w-5 h-5 text-emerald-400 mr-2"></i> | |
| <span>Rain Chance: <span class="text-emerald-400">${rainChance}%</span></span> | |
| </div> | |
| <div class="flex items-center bg-gray-800 p-3 rounded-lg"> | |
| <i data-lucide="umbrella" class="w-5 h-5 text-emerald-400 mr-2"></i> | |
| <span>Rain Volume: <span class="text-emerald-400">${rainVolume} mm</span></span> | |
| </div> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| }; | |
| // Update map | |
| const updateMap = (lat, lon, cityName, state, country) => { | |
| map.setView([lat, lon], 10); | |
| if(mapMarker) map.removeLayer(mapMarker); | |
| mapMarker = L.marker([lat, lon]).addTo(map) | |
| .bindPopup(`<b>${cityName}</b><br>${state ? state + ", " : ""}${country}`).openPopup(); | |
| }; | |
| const showLoading = () => { | |
| weatherInfoDiv.innerHTML = ` | |
| <div class="flex flex-col items-center justify-center h-full space-y-3"> | |
| <div class="loader"></div> | |
| <p class="text-gray-400">Fetching weather data...</p> | |
| </div>`; | |
| forecastContainer.innerHTML = ` | |
| <div class="flex items-center justify-center space-x-3"> | |
| <div class="loader"></div> | |
| <p class="text-gray-400">Fetching forecast data...</p> | |
| </div>`; | |
| }; | |
| const showError = (msg) => { | |
| weatherInfoDiv.innerHTML = ` | |
| <div class="bg-red-900 text-red-200 p-4 rounded-lg text-center"> | |
| <p class="font-semibold">Error</p> | |
| <p class="text-sm">${msg}</p> | |
| </div>`; | |
| forecastContainer.innerHTML = ` | |
| <div class="text-center text-red-400"> | |
| <p>Could not load forecast.</p> | |
| </div>`; | |
| }; | |
| const applyRainShadow = (data) => { | |
| let rainChance = data.clouds ? data.clouds.all : 0; | |
| let opacity = 0; | |
| if(rainChance >= 80) opacity = 1.0; | |
| else if(rainChance >= 60) opacity = 0.8; | |
| else if(rainChance >= 40) opacity = 0.5; | |
| else if(rainChance >= 20) opacity = 0.2; | |
| else opacity = 0.0; | |
| radarLayer.setOpacity(opacity); | |
| }; | |
| const fetchForecast = (lat, lon) => { | |
| fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if (!data.list) return; | |
| const daily = []; | |
| data.list.forEach(item => { | |
| if (item.dt_txt.includes("12:00:00")) { | |
| daily.push(item); | |
| } | |
| }); | |
| updateForecastUI(daily.slice(0, 5)); | |
| }) | |
| .catch(err => { | |
| console.error("Forecast error:", err); | |
| forecastContainer.innerHTML = `<div class="text-center text-red-400"><p>Could not load forecast data.</p></div>`; | |
| }); | |
| }; | |
| const updateForecastUI = (daily) => { | |
| forecastContainer.innerHTML = ` | |
| <h3 class="text-xl font-semibold text-emerald-400 mb-4 text-center lg:text-left">5-Day Forecast</h3> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4"> | |
| ${daily.map(day => { | |
| const date = new Date(day.dt * 1000); | |
| const options = { weekday: 'short' }; | |
| const weekday = date.toLocaleDateString(undefined, options); | |
| return ` | |
| <div class="bg-gray-800 p-3 rounded-lg flex flex-col items-center text-center"> | |
| <p class="text-sm font-semibold text-gray-300">${weekday}</p> | |
| <img src="https://openweathermap.org/img/wn/${day.weather[0].icon}.png" alt="${day.weather[0].main}" class="w-12 h-12"/> | |
| <p class="font-bold text-white">${Math.round(day.main.temp)}°C</p> | |
| <p class="text-xs text-gray-400 capitalize">${day.weather[0].description}</p> | |
| </div> | |
| `; | |
| }).join('')} | |
| </div> | |
| `; | |
| }; | |
| searchButton.addEventListener('click', () => { | |
| const city = cityInput.value.trim(); | |
| if(city) getWeatherByCity(city); | |
| }); | |
| cityInput.addEventListener('keydown', e => { if(e.key==="Enter") searchButton.click(); }); | |
| map.on('click', e => getWeatherByCoords(e.latlng.lat, e.latlng.lng)); | |
| getWeatherByCity(initialCity); | |
| }); | |
| </script> | |
| </body> | |
| </html> |