Spaces:
Sleeping
Sleeping
Commit
·
808378f
1
Parent(s):
4f0125c
No Emojii
Browse files- cams_downloader.py +58 -143
- constants.py +14 -0
- data_processor.py +3 -20
- deploy.sh +0 -24
- diagnose_cams.py +0 -131
- startup.py +0 -118
- templates/aurora_predict.html +8 -8
- templates/aurora_prediction_plot.html +14 -14
- templates/aurora_variables.html +15 -15
- templates/gallery.html +31 -32
- templates/index.html +21 -21
- templates/interactive_plot.html +34 -34
- templates/plot.html +10 -10
- templates/prediction_runs.html +9 -13
- templates/variables.html +14 -14
- templates/view_interactive.html +11 -11
cams_downloader.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
| 1 |
-
|
| 2 |
-
# Download CAMS atmospheric composition data
|
| 3 |
-
|
| 4 |
import cdsapi
|
| 5 |
import zipfile
|
| 6 |
-
import
|
|
|
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
from datetime import datetime, timedelta
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class CAMSDownloader:
|
| 12 |
-
def __init__(self, download_dir=
|
| 13 |
"""
|
| 14 |
Initialize CAMS downloader
|
| 15 |
|
|
@@ -28,23 +31,22 @@ class CAMSDownloader:
|
|
| 28 |
|
| 29 |
def _init_client(self):
|
| 30 |
"""Initialize CDS API client"""
|
|
|
|
| 31 |
try:
|
| 32 |
-
# First, try environment variables (preferred for cloud deployments)
|
| 33 |
cdsapi_url = os.getenv('CDSAPI_URL')
|
| 34 |
cdsapi_key = os.getenv('CDSAPI_KEY')
|
| 35 |
|
| 36 |
if cdsapi_url and cdsapi_key:
|
| 37 |
self.client = cdsapi.Client(key=cdsapi_key, url=cdsapi_url)
|
| 38 |
-
|
| 39 |
return
|
| 40 |
|
| 41 |
-
#
|
| 42 |
cdsapirc_path = Path.cwd() / ".cdsapirc"
|
| 43 |
if not cdsapirc_path.exists():
|
| 44 |
cdsapirc_path = Path.home() / ".cdsapirc"
|
| 45 |
|
| 46 |
if cdsapirc_path.exists():
|
| 47 |
-
# Parse credentials from .cdsapirc
|
| 48 |
with open(cdsapirc_path, 'r') as f:
|
| 49 |
lines = f.readlines()
|
| 50 |
|
|
@@ -59,21 +61,21 @@ class CAMSDownloader:
|
|
| 59 |
|
| 60 |
if url and key:
|
| 61 |
self.client = cdsapi.Client(key=key, url=url)
|
| 62 |
-
|
| 63 |
return
|
| 64 |
else:
|
| 65 |
raise ValueError("Could not parse URL or key from .cdsapirc file")
|
| 66 |
|
| 67 |
# Last resort: Try default initialization
|
| 68 |
self.client = cdsapi.Client()
|
| 69 |
-
|
| 70 |
|
| 71 |
except Exception as e:
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
self.client = None
|
| 78 |
|
| 79 |
def is_client_ready(self):
|
|
@@ -81,45 +83,31 @@ class CAMSDownloader:
|
|
| 81 |
return self.client is not None
|
| 82 |
|
| 83 |
def download_cams_data(self, date_str, variables=None, pressure_levels=None):
|
| 84 |
-
"""
|
| 85 |
-
Download CAMS atmospheric composition data for a specific date
|
| 86 |
-
|
| 87 |
-
Parameters:
|
| 88 |
-
date_str (str): Date in YYYY-MM-DD format
|
| 89 |
-
variables (list): List of variables to download (default: common air pollution variables)
|
| 90 |
-
pressure_levels (list): List of pressure levels (default: standard levels)
|
| 91 |
-
|
| 92 |
-
Returns:
|
| 93 |
-
str: Path to downloaded ZIP file
|
| 94 |
-
"""
|
| 95 |
if not self.is_client_ready():
|
| 96 |
raise Exception("CDS API client not initialized. Please check your credentials.")
|
| 97 |
|
| 98 |
-
# Validate date
|
| 99 |
try:
|
| 100 |
target_date = pd.to_datetime(date_str)
|
| 101 |
date_str = target_date.strftime('%Y-%m-%d')
|
| 102 |
except:
|
| 103 |
raise ValueError(f"Invalid date format: {date_str}. Use YYYY-MM-DD format.")
|
| 104 |
|
| 105 |
-
# Check if data already exists
|
| 106 |
filename = f"{date_str}-cams.nc.zip"
|
| 107 |
filepath = self.download_dir / filename
|
| 108 |
|
| 109 |
if filepath.exists():
|
| 110 |
-
|
| 111 |
return str(filepath)
|
| 112 |
|
| 113 |
-
# Default variables (common air pollution variables)
|
| 114 |
if variables is None:
|
| 115 |
variables = [
|
| 116 |
-
# Meteorological surface-level variables
|
| 117 |
"10m_u_component_of_wind",
|
| 118 |
"10m_v_component_of_wind",
|
| 119 |
"2m_temperature",
|
| 120 |
"mean_sea_level_pressure",
|
| 121 |
-
|
| 122 |
-
# Pollution surface-level variables
|
| 123 |
"particulate_matter_1um",
|
| 124 |
"particulate_matter_2.5um",
|
| 125 |
"particulate_matter_10um",
|
|
@@ -128,15 +116,13 @@ class CAMSDownloader:
|
|
| 128 |
"total_column_nitrogen_dioxide",
|
| 129 |
"total_column_ozone",
|
| 130 |
"total_column_sulphur_dioxide",
|
| 131 |
-
|
| 132 |
-
# Meteorological atmospheric variables
|
| 133 |
"u_component_of_wind",
|
| 134 |
"v_component_of_wind",
|
| 135 |
"temperature",
|
| 136 |
"geopotential",
|
| 137 |
"specific_humidity",
|
| 138 |
-
|
| 139 |
-
# Pollution atmospheric variables
|
| 140 |
"carbon_monoxide",
|
| 141 |
"nitrogen_dioxide",
|
| 142 |
"nitrogen_monoxide",
|
|
@@ -144,20 +130,18 @@ class CAMSDownloader:
|
|
| 144 |
"sulphur_dioxide",
|
| 145 |
]
|
| 146 |
|
| 147 |
-
# Default pressure levels
|
| 148 |
if pressure_levels is None:
|
| 149 |
pressure_levels = [
|
| 150 |
"50", "100", "150", "200", "250", "300", "400",
|
| 151 |
"500", "600", "700", "850", "925", "1000",
|
| 152 |
]
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
|
| 158 |
try:
|
| 159 |
-
|
| 160 |
-
print("📡 Requesting data from CAMS API...")
|
| 161 |
self.client.retrieve(
|
| 162 |
"cams-global-atmospheric-composition-forecasts",
|
| 163 |
{
|
|
@@ -166,7 +150,7 @@ class CAMSDownloader:
|
|
| 166 |
"variable": variables,
|
| 167 |
"pressure_level": pressure_levels,
|
| 168 |
"date": date_str,
|
| 169 |
-
"time": ["00:00", "12:00"],
|
| 170 |
"format": "netcdf_zip",
|
| 171 |
},
|
| 172 |
str(filepath),
|
|
@@ -175,74 +159,53 @@ class CAMSDownloader:
|
|
| 175 |
# Validate the downloaded file
|
| 176 |
if filepath.exists():
|
| 177 |
file_size = filepath.stat().st_size
|
| 178 |
-
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
print(f"⚠️ Warning: Downloaded file is very small ({file_size} bytes)")
|
| 183 |
-
# Read first few bytes to check for error messages
|
| 184 |
with open(filepath, 'rb') as f:
|
| 185 |
header = f.read(200)
|
| 186 |
if b'error' in header.lower() or b'html' in header.lower():
|
| 187 |
filepath.unlink()
|
| 188 |
raise Exception("CAMS API returned an error response instead of data")
|
| 189 |
|
| 190 |
-
|
| 191 |
return str(filepath)
|
| 192 |
else:
|
| 193 |
raise Exception("Download completed but file was not created")
|
| 194 |
|
| 195 |
except Exception as e:
|
| 196 |
-
# Clean up partial download
|
| 197 |
if filepath.exists():
|
| 198 |
-
|
| 199 |
filepath.unlink()
|
| 200 |
raise Exception(f"Error downloading CAMS data: {str(e)}")
|
| 201 |
|
| 202 |
def extract_cams_files(self, zip_path):
|
| 203 |
-
"""
|
| 204 |
-
Extract surface and atmospheric data from CAMS ZIP file
|
| 205 |
-
|
| 206 |
-
Parameters:
|
| 207 |
-
zip_path (str): Path to CAMS ZIP file
|
| 208 |
-
|
| 209 |
-
Returns:
|
| 210 |
-
dict: Paths to extracted files
|
| 211 |
-
"""
|
| 212 |
zip_path = Path(zip_path)
|
| 213 |
if not zip_path.exists():
|
| 214 |
raise FileNotFoundError(f"ZIP file not found: {zip_path}")
|
| 215 |
|
| 216 |
-
# Validate file is actually a ZIP file
|
| 217 |
try:
|
| 218 |
-
# Check file size first
|
| 219 |
file_size = zip_path.stat().st_size
|
| 220 |
-
if file_size < 1000:
|
| 221 |
-
|
| 222 |
-
# Try to read first few bytes to see what we got
|
| 223 |
with open(zip_path, 'rb') as f:
|
| 224 |
header = f.read(100)
|
| 225 |
if b'html' in header.lower() or b'error' in header.lower():
|
| 226 |
raise Exception("Downloaded file appears to be an HTML error page, not ZIP data")
|
| 227 |
|
| 228 |
-
# Test if it's a valid ZIP file
|
| 229 |
if not zipfile.is_zipfile(zip_path):
|
| 230 |
-
|
| 231 |
-
# Try to read first few lines to diagnose
|
| 232 |
with open(zip_path, 'r', errors='ignore') as f:
|
| 233 |
first_lines = f.read(200)
|
| 234 |
-
|
| 235 |
raise Exception(f"Downloaded file is not a valid ZIP archive. File size: {file_size} bytes")
|
| 236 |
|
| 237 |
except Exception as e:
|
| 238 |
-
|
| 239 |
-
raise e
|
| 240 |
-
else:
|
| 241 |
-
raise Exception(f"Error validating ZIP file: {str(e)}")
|
| 242 |
|
| 243 |
-
# Extract date from filename
|
| 244 |
date_str = zip_path.stem.replace("-cams.nc", "")
|
| 245 |
-
|
| 246 |
surface_path = self.extracted_dir / f"{date_str}-cams-surface.nc"
|
| 247 |
atmospheric_path = self.extracted_dir / f"{date_str}-cams-atmospheric.nc"
|
| 248 |
|
|
@@ -252,37 +215,24 @@ class CAMSDownloader:
|
|
| 252 |
with zipfile.ZipFile(zip_path, "r") as zf:
|
| 253 |
zip_contents = zf.namelist()
|
| 254 |
|
| 255 |
-
|
| 256 |
-
surface_file = None
|
| 257 |
-
for file in zip_contents:
|
| 258 |
-
if 'sfc' in file.lower() or file.endswith('_sfc.nc'):
|
| 259 |
-
surface_file = file
|
| 260 |
-
break
|
| 261 |
-
|
| 262 |
if surface_file and not surface_path.exists():
|
| 263 |
with open(surface_path, "wb") as f:
|
| 264 |
f.write(zf.read(surface_file))
|
| 265 |
-
|
| 266 |
extracted_files['surface'] = str(surface_path)
|
| 267 |
elif surface_path.exists():
|
| 268 |
extracted_files['surface'] = str(surface_path)
|
| 269 |
|
| 270 |
-
|
| 271 |
-
atmospheric_file = None
|
| 272 |
-
for file in zip_contents:
|
| 273 |
-
if 'plev' in file.lower() or file.endswith('_plev.nc'):
|
| 274 |
-
atmospheric_file = file
|
| 275 |
-
break
|
| 276 |
-
|
| 277 |
if atmospheric_file and not atmospheric_path.exists():
|
| 278 |
with open(atmospheric_path, "wb") as f:
|
| 279 |
f.write(zf.read(atmospheric_file))
|
| 280 |
-
|
| 281 |
extracted_files['atmospheric'] = str(atmospheric_path)
|
| 282 |
elif atmospheric_path.exists():
|
| 283 |
extracted_files['atmospheric'] = str(atmospheric_path)
|
| 284 |
|
| 285 |
-
# If no specific files found, extract all .nc files
|
| 286 |
if not extracted_files:
|
| 287 |
nc_files = [f for f in zip_contents if f.endswith('.nc')]
|
| 288 |
for nc_file in nc_files:
|
|
@@ -300,28 +250,6 @@ class CAMSDownloader:
|
|
| 300 |
|
| 301 |
return extracted_files
|
| 302 |
|
| 303 |
-
def get_available_dates(self, start_date=None, end_date=None):
|
| 304 |
-
"""
|
| 305 |
-
Get list of dates for which CAMS data is typically available
|
| 306 |
-
Note: This doesn't check actual availability, just generates reasonable date range
|
| 307 |
-
|
| 308 |
-
Parameters:
|
| 309 |
-
start_date (str): Start date (default: 30 days ago)
|
| 310 |
-
end_date (str): End date (default: yesterday)
|
| 311 |
-
|
| 312 |
-
Returns:
|
| 313 |
-
list: List of date strings in YYYY-MM-DD format
|
| 314 |
-
"""
|
| 315 |
-
if start_date is None:
|
| 316 |
-
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
| 317 |
-
|
| 318 |
-
if end_date is None:
|
| 319 |
-
end_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
|
| 320 |
-
|
| 321 |
-
# Generate date range
|
| 322 |
-
date_range = pd.date_range(start=start_date, end=end_date, freq='D')
|
| 323 |
-
return [date.strftime('%Y-%m-%d') for date in date_range]
|
| 324 |
-
|
| 325 |
def list_downloaded_files(self):
|
| 326 |
"""List all downloaded CAMS files"""
|
| 327 |
downloaded_files = []
|
|
@@ -336,73 +264,60 @@ class CAMSDownloader:
|
|
| 336 |
}
|
| 337 |
downloaded_files.append(file_info)
|
| 338 |
|
| 339 |
-
# Sort by date (newest first)
|
| 340 |
downloaded_files.sort(key=lambda x: x['date'], reverse=True)
|
| 341 |
return downloaded_files
|
| 342 |
|
| 343 |
-
def cleanup_old_files(self, days_old=
|
| 344 |
-
"""
|
| 345 |
-
Clean up downloaded files older than specified days
|
| 346 |
-
|
| 347 |
-
Parameters:
|
| 348 |
-
days_old (int): Delete files older than this many days
|
| 349 |
-
"""
|
| 350 |
try:
|
| 351 |
cutoff_date = datetime.now() - timedelta(days=days_old)
|
| 352 |
-
|
| 353 |
deleted_count = 0
|
|
|
|
| 354 |
for zip_file in self.download_dir.glob("*-cams.nc.zip"):
|
| 355 |
if datetime.fromtimestamp(zip_file.stat().st_mtime) < cutoff_date:
|
| 356 |
zip_file.unlink()
|
| 357 |
deleted_count += 1
|
| 358 |
|
| 359 |
-
# Also clean extracted files
|
| 360 |
for nc_file in self.extracted_dir.glob("*.nc"):
|
| 361 |
if datetime.fromtimestamp(nc_file.stat().st_mtime) < cutoff_date:
|
| 362 |
nc_file.unlink()
|
| 363 |
deleted_count += 1
|
| 364 |
|
| 365 |
-
|
| 366 |
return deleted_count
|
| 367 |
|
| 368 |
except Exception as e:
|
| 369 |
-
|
| 370 |
return 0
|
| 371 |
|
| 372 |
|
| 373 |
def test_cams_downloader():
|
| 374 |
"""Test function for CAMS downloader"""
|
| 375 |
-
|
| 376 |
-
|
| 377 |
downloader = CAMSDownloader()
|
| 378 |
|
| 379 |
if not downloader.is_client_ready():
|
| 380 |
-
|
| 381 |
return False
|
| 382 |
|
| 383 |
-
# Test with recent date
|
| 384 |
test_date = (datetime.now() - timedelta(days=600)).strftime('%Y-%m-%d')
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
print("⚠️ This may take several minutes for the first download...")
|
| 388 |
|
| 389 |
try:
|
| 390 |
-
# Download data (will skip if already exists)
|
| 391 |
zip_path = downloader.download_cams_data(test_date)
|
| 392 |
-
|
| 393 |
|
| 394 |
-
# Test extraction
|
| 395 |
extracted_files = downloader.extract_cams_files(zip_path)
|
| 396 |
-
|
| 397 |
|
| 398 |
-
# List downloaded files
|
| 399 |
downloaded = downloader.list_downloaded_files()
|
| 400 |
-
|
| 401 |
|
| 402 |
return True
|
| 403 |
|
| 404 |
except Exception as e:
|
| 405 |
-
|
| 406 |
return False
|
| 407 |
|
| 408 |
|
|
|
|
| 1 |
+
import os
|
|
|
|
|
|
|
| 2 |
import cdsapi
|
| 3 |
import zipfile
|
| 4 |
+
import logging
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
from pathlib import Path
|
| 8 |
from datetime import datetime, timedelta
|
| 9 |
+
from constants import DOWNLOAD_FOLDER
|
| 10 |
+
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
class CAMSDownloader:
|
| 15 |
+
def __init__(self, download_dir=DOWNLOAD_FOLDER):
|
| 16 |
"""
|
| 17 |
Initialize CAMS downloader
|
| 18 |
|
|
|
|
| 31 |
|
| 32 |
def _init_client(self):
|
| 33 |
"""Initialize CDS API client"""
|
| 34 |
+
# Try to get the credentials from environment variables
|
| 35 |
try:
|
|
|
|
| 36 |
cdsapi_url = os.getenv('CDSAPI_URL')
|
| 37 |
cdsapi_key = os.getenv('CDSAPI_KEY')
|
| 38 |
|
| 39 |
if cdsapi_url and cdsapi_key:
|
| 40 |
self.client = cdsapi.Client(key=cdsapi_key, url=cdsapi_url)
|
| 41 |
+
logger.info("✅ CDS API client initialized from environment variables")
|
| 42 |
return
|
| 43 |
|
| 44 |
+
# Try to read from .cdsapirc file
|
| 45 |
cdsapirc_path = Path.cwd() / ".cdsapirc"
|
| 46 |
if not cdsapirc_path.exists():
|
| 47 |
cdsapirc_path = Path.home() / ".cdsapirc"
|
| 48 |
|
| 49 |
if cdsapirc_path.exists():
|
|
|
|
| 50 |
with open(cdsapirc_path, 'r') as f:
|
| 51 |
lines = f.readlines()
|
| 52 |
|
|
|
|
| 61 |
|
| 62 |
if url and key:
|
| 63 |
self.client = cdsapi.Client(key=key, url=url)
|
| 64 |
+
logger.info("✅ CDS API client initialized from .cdsapirc file")
|
| 65 |
return
|
| 66 |
else:
|
| 67 |
raise ValueError("Could not parse URL or key from .cdsapirc file")
|
| 68 |
|
| 69 |
# Last resort: Try default initialization
|
| 70 |
self.client = cdsapi.Client()
|
| 71 |
+
logger.info("✅ CDS API client initialized with default settings")
|
| 72 |
|
| 73 |
except Exception as e:
|
| 74 |
+
logger.warning(f"⚠️ Could not initialize CDS API client: {str(e)}")
|
| 75 |
+
logger.warning("Please ensure you have:\n"
|
| 76 |
+
"1. Created an account at https://cds.climate.copernicus.eu/\n"
|
| 77 |
+
"2. Set CDSAPI_URL and CDSAPI_KEY environment variables\n"
|
| 78 |
+
"3. Or created a .cdsapirc file in your home directory")
|
| 79 |
self.client = None
|
| 80 |
|
| 81 |
def is_client_ready(self):
|
|
|
|
| 83 |
return self.client is not None
|
| 84 |
|
| 85 |
def download_cams_data(self, date_str, variables=None, pressure_levels=None):
|
| 86 |
+
"""Download CAMS atmospheric composition data for a specific date"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
if not self.is_client_ready():
|
| 88 |
raise Exception("CDS API client not initialized. Please check your credentials.")
|
| 89 |
|
|
|
|
| 90 |
try:
|
| 91 |
target_date = pd.to_datetime(date_str)
|
| 92 |
date_str = target_date.strftime('%Y-%m-%d')
|
| 93 |
except:
|
| 94 |
raise ValueError(f"Invalid date format: {date_str}. Use YYYY-MM-DD format.")
|
| 95 |
|
|
|
|
| 96 |
filename = f"{date_str}-cams.nc.zip"
|
| 97 |
filepath = self.download_dir / filename
|
| 98 |
|
| 99 |
if filepath.exists():
|
| 100 |
+
logger.info(f"✅ Data for {date_str} already exists: {filename}")
|
| 101 |
return str(filepath)
|
| 102 |
|
|
|
|
| 103 |
if variables is None:
|
| 104 |
variables = [
|
| 105 |
+
# Meteorological surface-level variables:
|
| 106 |
"10m_u_component_of_wind",
|
| 107 |
"10m_v_component_of_wind",
|
| 108 |
"2m_temperature",
|
| 109 |
"mean_sea_level_pressure",
|
| 110 |
+
# Pollution surface-level variables:
|
|
|
|
| 111 |
"particulate_matter_1um",
|
| 112 |
"particulate_matter_2.5um",
|
| 113 |
"particulate_matter_10um",
|
|
|
|
| 116 |
"total_column_nitrogen_dioxide",
|
| 117 |
"total_column_ozone",
|
| 118 |
"total_column_sulphur_dioxide",
|
| 119 |
+
# Meteorological atmospheric variables:
|
|
|
|
| 120 |
"u_component_of_wind",
|
| 121 |
"v_component_of_wind",
|
| 122 |
"temperature",
|
| 123 |
"geopotential",
|
| 124 |
"specific_humidity",
|
| 125 |
+
# Pollution atmospheric variables:
|
|
|
|
| 126 |
"carbon_monoxide",
|
| 127 |
"nitrogen_dioxide",
|
| 128 |
"nitrogen_monoxide",
|
|
|
|
| 130 |
"sulphur_dioxide",
|
| 131 |
]
|
| 132 |
|
|
|
|
| 133 |
if pressure_levels is None:
|
| 134 |
pressure_levels = [
|
| 135 |
"50", "100", "150", "200", "250", "300", "400",
|
| 136 |
"500", "600", "700", "850", "925", "1000",
|
| 137 |
]
|
| 138 |
|
| 139 |
+
logger.info(f"🔄 Downloading CAMS data for {date_str}...")
|
| 140 |
+
logger.info(f"Variables: {len(variables)} selected")
|
| 141 |
+
logger.info(f"Pressure levels: {len(pressure_levels)} levels")
|
| 142 |
|
| 143 |
try:
|
| 144 |
+
logger.info("📡 Requesting data from CAMS API...")
|
|
|
|
| 145 |
self.client.retrieve(
|
| 146 |
"cams-global-atmospheric-composition-forecasts",
|
| 147 |
{
|
|
|
|
| 150 |
"variable": variables,
|
| 151 |
"pressure_level": pressure_levels,
|
| 152 |
"date": date_str,
|
| 153 |
+
"time": ["00:00", "12:00"],
|
| 154 |
"format": "netcdf_zip",
|
| 155 |
},
|
| 156 |
str(filepath),
|
|
|
|
| 159 |
# Validate the downloaded file
|
| 160 |
if filepath.exists():
|
| 161 |
file_size = filepath.stat().st_size
|
| 162 |
+
logger.info(f"📁 Downloaded file size: {file_size / 1024 / 1024:.2f} MB")
|
| 163 |
|
| 164 |
+
if file_size < 10000:
|
| 165 |
+
logger.warning(f"⚠️ Downloaded file is very small ({file_size} bytes)")
|
|
|
|
|
|
|
| 166 |
with open(filepath, 'rb') as f:
|
| 167 |
header = f.read(200)
|
| 168 |
if b'error' in header.lower() or b'html' in header.lower():
|
| 169 |
filepath.unlink()
|
| 170 |
raise Exception("CAMS API returned an error response instead of data")
|
| 171 |
|
| 172 |
+
logger.info(f"✅ Successfully downloaded: {filename}")
|
| 173 |
return str(filepath)
|
| 174 |
else:
|
| 175 |
raise Exception("Download completed but file was not created")
|
| 176 |
|
| 177 |
except Exception as e:
|
|
|
|
| 178 |
if filepath.exists():
|
| 179 |
+
logger.warning(f"🗑️ Cleaning up failed download: {filepath}")
|
| 180 |
filepath.unlink()
|
| 181 |
raise Exception(f"Error downloading CAMS data: {str(e)}")
|
| 182 |
|
| 183 |
def extract_cams_files(self, zip_path):
|
| 184 |
+
"""Extract surface and atmospheric data from CAMS ZIP file"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
zip_path = Path(zip_path)
|
| 186 |
if not zip_path.exists():
|
| 187 |
raise FileNotFoundError(f"ZIP file not found: {zip_path}")
|
| 188 |
|
|
|
|
| 189 |
try:
|
|
|
|
| 190 |
file_size = zip_path.stat().st_size
|
| 191 |
+
if file_size < 1000:
|
| 192 |
+
logger.warning(f"⚠️ Downloaded file is too small ({file_size} bytes), likely an error response")
|
|
|
|
| 193 |
with open(zip_path, 'rb') as f:
|
| 194 |
header = f.read(100)
|
| 195 |
if b'html' in header.lower() or b'error' in header.lower():
|
| 196 |
raise Exception("Downloaded file appears to be an HTML error page, not ZIP data")
|
| 197 |
|
|
|
|
| 198 |
if not zipfile.is_zipfile(zip_path):
|
| 199 |
+
logger.error(f"❌ File is not a valid ZIP file: {zip_path}")
|
|
|
|
| 200 |
with open(zip_path, 'r', errors='ignore') as f:
|
| 201 |
first_lines = f.read(200)
|
| 202 |
+
logger.debug(f"File contents preview: {first_lines[:100]}...")
|
| 203 |
raise Exception(f"Downloaded file is not a valid ZIP archive. File size: {file_size} bytes")
|
| 204 |
|
| 205 |
except Exception as e:
|
| 206 |
+
raise Exception(f"Error validating ZIP file: {str(e)}")
|
|
|
|
|
|
|
|
|
|
| 207 |
|
|
|
|
| 208 |
date_str = zip_path.stem.replace("-cams.nc", "")
|
|
|
|
| 209 |
surface_path = self.extracted_dir / f"{date_str}-cams-surface.nc"
|
| 210 |
atmospheric_path = self.extracted_dir / f"{date_str}-cams-atmospheric.nc"
|
| 211 |
|
|
|
|
| 215 |
with zipfile.ZipFile(zip_path, "r") as zf:
|
| 216 |
zip_contents = zf.namelist()
|
| 217 |
|
| 218 |
+
surface_file = next((f for f in zip_contents if 'sfc' in f.lower() or f.endswith('_sfc.nc')), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
if surface_file and not surface_path.exists():
|
| 220 |
with open(surface_path, "wb") as f:
|
| 221 |
f.write(zf.read(surface_file))
|
| 222 |
+
logger.info(f"✅ Extracted surface data: {surface_path.name}")
|
| 223 |
extracted_files['surface'] = str(surface_path)
|
| 224 |
elif surface_path.exists():
|
| 225 |
extracted_files['surface'] = str(surface_path)
|
| 226 |
|
| 227 |
+
atmospheric_file = next((f for f in zip_contents if 'plev' in f.lower() or f.endswith('_plev.nc')), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
if atmospheric_file and not atmospheric_path.exists():
|
| 229 |
with open(atmospheric_path, "wb") as f:
|
| 230 |
f.write(zf.read(atmospheric_file))
|
| 231 |
+
logger.info(f"✅ Extracted atmospheric data: {atmospheric_path.name}")
|
| 232 |
extracted_files['atmospheric'] = str(atmospheric_path)
|
| 233 |
elif atmospheric_path.exists():
|
| 234 |
extracted_files['atmospheric'] = str(atmospheric_path)
|
| 235 |
|
|
|
|
| 236 |
if not extracted_files:
|
| 237 |
nc_files = [f for f in zip_contents if f.endswith('.nc')]
|
| 238 |
for nc_file in nc_files:
|
|
|
|
| 250 |
|
| 251 |
return extracted_files
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
def list_downloaded_files(self):
|
| 254 |
"""List all downloaded CAMS files"""
|
| 255 |
downloaded_files = []
|
|
|
|
| 264 |
}
|
| 265 |
downloaded_files.append(file_info)
|
| 266 |
|
|
|
|
| 267 |
downloaded_files.sort(key=lambda x: x['date'], reverse=True)
|
| 268 |
return downloaded_files
|
| 269 |
|
| 270 |
+
def cleanup_old_files(self, days_old=7):
|
| 271 |
+
"""Clean up downloaded files older than specified days"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
try:
|
| 273 |
cutoff_date = datetime.now() - timedelta(days=days_old)
|
|
|
|
| 274 |
deleted_count = 0
|
| 275 |
+
|
| 276 |
for zip_file in self.download_dir.glob("*-cams.nc.zip"):
|
| 277 |
if datetime.fromtimestamp(zip_file.stat().st_mtime) < cutoff_date:
|
| 278 |
zip_file.unlink()
|
| 279 |
deleted_count += 1
|
| 280 |
|
|
|
|
| 281 |
for nc_file in self.extracted_dir.glob("*.nc"):
|
| 282 |
if datetime.fromtimestamp(nc_file.stat().st_mtime) < cutoff_date:
|
| 283 |
nc_file.unlink()
|
| 284 |
deleted_count += 1
|
| 285 |
|
| 286 |
+
logger.info(f"🧹 Cleaned up {deleted_count} old files")
|
| 287 |
return deleted_count
|
| 288 |
|
| 289 |
except Exception as e:
|
| 290 |
+
logger.error(f"Error during cleanup: {str(e)}")
|
| 291 |
return 0
|
| 292 |
|
| 293 |
|
| 294 |
def test_cams_downloader():
|
| 295 |
"""Test function for CAMS downloader"""
|
| 296 |
+
logger.info("Testing CAMS downloader...")
|
|
|
|
| 297 |
downloader = CAMSDownloader()
|
| 298 |
|
| 299 |
if not downloader.is_client_ready():
|
| 300 |
+
logger.error("❌ CDS API client not ready. Please check your credentials.")
|
| 301 |
return False
|
| 302 |
|
|
|
|
| 303 |
test_date = (datetime.now() - timedelta(days=600)).strftime('%Y-%m-%d')
|
| 304 |
+
logger.info(f"Testing download for date: {test_date}")
|
| 305 |
+
logger.warning("⚠️ This may take several minutes for the first download...")
|
|
|
|
| 306 |
|
| 307 |
try:
|
|
|
|
| 308 |
zip_path = downloader.download_cams_data(test_date)
|
| 309 |
+
logger.info(f"✅ Download successful: {zip_path}")
|
| 310 |
|
|
|
|
| 311 |
extracted_files = downloader.extract_cams_files(zip_path)
|
| 312 |
+
logger.info(f"✅ Extraction successful: {len(extracted_files)} files")
|
| 313 |
|
|
|
|
| 314 |
downloaded = downloader.list_downloaded_files()
|
| 315 |
+
logger.info(f"✅ Found {len(downloaded)} downloaded files")
|
| 316 |
|
| 317 |
return True
|
| 318 |
|
| 319 |
except Exception as e:
|
| 320 |
+
logger.error(f"❌ Test failed: {str(e)}")
|
| 321 |
return False
|
| 322 |
|
| 323 |
|
constants.py
CHANGED
|
@@ -1,5 +1,19 @@
|
|
| 1 |
# NetCDF variables and their properties (includes air pollution, meteorological, and other variables)
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
NETCDF_VARIABLES = {
|
| 4 |
# === AIR POLLUTION VARIABLES ===
|
| 5 |
# PM2.5
|
|
|
|
| 1 |
# NetCDF variables and their properties (includes air pollution, meteorological, and other variables)
|
| 2 |
|
| 3 |
+
# Folder Paths
|
| 4 |
+
DOWNLOAD_FOLDER = 'downloads/'
|
| 5 |
+
UPLOAD_FOLDER = 'uploads/'
|
| 6 |
+
PREDICTIONS_FOLDER = 'predictions/'
|
| 7 |
+
PLOTS_FOLDER = 'plots/'
|
| 8 |
+
AUTO_DOWNLOAD_FOLDER = 'auto_downloads/'
|
| 9 |
+
|
| 10 |
+
INDIA_BOUNDS = {
|
| 11 |
+
'lat_min': 6.0, # Southern tip (including southern islands)
|
| 12 |
+
'lat_max': 38.0, # Northern border (including Kashmir)
|
| 13 |
+
'lon_min': 68.0, # Western border
|
| 14 |
+
'lon_max': 98.0 # Eastern border (including Andaman & Nicobar)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
NETCDF_VARIABLES = {
|
| 18 |
# === AIR POLLUTION VARIABLES ===
|
| 19 |
# PM2.5
|
data_processor.py
CHANGED
|
@@ -12,16 +12,8 @@ import xarray as xr
|
|
| 12 |
from pathlib import Path
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
-
# India geographical bounds for coordinate trimming
|
| 16 |
-
INDIA_BOUNDS = {
|
| 17 |
-
'lat_min': 6.0, # Southern tip (including southern islands)
|
| 18 |
-
'lat_max': 38.0, # Northern border (including Kashmir)
|
| 19 |
-
'lon_min': 68.0, # Western border
|
| 20 |
-
'lon_max': 98.0 # Eastern border (including Andaman & Nicobar)
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
# Imports from our Modules
|
| 24 |
-
from constants import NETCDF_VARIABLES, AIR_POLLUTION_VARIABLES, PRESSURE_LEVELS
|
| 25 |
warnings.filterwarnings('ignore')
|
| 26 |
|
| 27 |
class NetCDFProcessor:
|
|
@@ -84,18 +76,9 @@ class NetCDFProcessor:
|
|
| 84 |
self.atmospheric_dataset = xr.open_dataset(tmp.name, engine='netcdf4')
|
| 85 |
print(f"Loaded atmospheric data: {atmospheric_file}")
|
| 86 |
|
| 87 |
-
# If no specific files found,
|
| 88 |
if not surface_file and not atmospheric_file:
|
| 89 |
-
|
| 90 |
-
if nc_files:
|
| 91 |
-
with zf.open(nc_files[0]) as f:
|
| 92 |
-
with tempfile.NamedTemporaryFile(suffix='.nc') as tmp:
|
| 93 |
-
tmp.write(f.read())
|
| 94 |
-
tmp.flush()
|
| 95 |
-
self.dataset = xr.open_dataset(tmp.name, engine='netcdf4')
|
| 96 |
-
print(f"Loaded dataset: {nc_files[0]}")
|
| 97 |
-
else:
|
| 98 |
-
raise ValueError("No NetCDF files found in ZIP")
|
| 99 |
|
| 100 |
return True
|
| 101 |
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
from datetime import datetime
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Imports from our Modules
|
| 16 |
+
from constants import NETCDF_VARIABLES, AIR_POLLUTION_VARIABLES, PRESSURE_LEVELS, INDIA_BOUNDS
|
| 17 |
warnings.filterwarnings('ignore')
|
| 18 |
|
| 19 |
class NetCDFProcessor:
|
|
|
|
| 76 |
self.atmospheric_dataset = xr.open_dataset(tmp.name, engine='netcdf4')
|
| 77 |
print(f"Loaded atmospheric data: {atmospheric_file}")
|
| 78 |
|
| 79 |
+
# If no specific files found, raise error
|
| 80 |
if not surface_file and not atmospheric_file:
|
| 81 |
+
raise ValueError("No NetCDF files found in ZIP")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
return True
|
| 84 |
|
deploy.sh
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
|
| 3 |
-
# Deployment script for Hugging Face Spaces
|
| 4 |
-
|
| 5 |
-
echo "🚀 Preparing CAMS Air Pollution Dashboard for Hugging Face deployment"
|
| 6 |
-
|
| 7 |
-
# Build the Docker image
|
| 8 |
-
echo "📦 Building Docker image..."
|
| 9 |
-
docker build -t cams-pollution-dashboard .
|
| 10 |
-
|
| 11 |
-
# Test the container locally (optional)
|
| 12 |
-
echo "🧪 To test locally, run:"
|
| 13 |
-
echo "docker run -p 7860:7860 cams-pollution-dashboard"
|
| 14 |
-
|
| 15 |
-
echo ""
|
| 16 |
-
echo "📋 Deployment checklist for Hugging Face Spaces:"
|
| 17 |
-
echo "1. Create a new Space on Hugging Face"
|
| 18 |
-
echo "2. Select 'Docker' as the SDK"
|
| 19 |
-
echo "3. Upload all files including Dockerfile"
|
| 20 |
-
echo "4. Make sure app_readme.md contains the correct YAML frontmatter"
|
| 21 |
-
echo "5. Push to your Hugging Face repository"
|
| 22 |
-
|
| 23 |
-
echo ""
|
| 24 |
-
echo "✅ Ready for deployment!"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
diagnose_cams.py
DELETED
|
@@ -1,131 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
CAMS Download Diagnostic Tool
|
| 4 |
-
Helps troubleshoot issues with CAMS data downloads
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import os
|
| 8 |
-
import zipfile
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from datetime import datetime, timedelta
|
| 11 |
-
|
| 12 |
-
def diagnose_cams_downloads():
|
| 13 |
-
"""Diagnose CAMS download issues"""
|
| 14 |
-
print("🔍 CAMS Download Diagnostic Tool")
|
| 15 |
-
print("=" * 50)
|
| 16 |
-
|
| 17 |
-
# Check downloads directory
|
| 18 |
-
downloads_dir = Path("downloads")
|
| 19 |
-
if not downloads_dir.exists():
|
| 20 |
-
print("❌ Downloads directory doesn't exist")
|
| 21 |
-
return
|
| 22 |
-
|
| 23 |
-
print(f"📁 Downloads directory: {downloads_dir.absolute()}")
|
| 24 |
-
|
| 25 |
-
# List all files in downloads
|
| 26 |
-
all_files = list(downloads_dir.glob("*"))
|
| 27 |
-
if not all_files:
|
| 28 |
-
print("📂 Downloads directory is empty")
|
| 29 |
-
return
|
| 30 |
-
|
| 31 |
-
print(f"\n📋 Found {len(all_files)} files:")
|
| 32 |
-
|
| 33 |
-
for file_path in all_files:
|
| 34 |
-
print(f"\n📄 File: {file_path.name}")
|
| 35 |
-
print(f" Size: {file_path.stat().st_size} bytes ({file_path.stat().st_size / 1024:.1f} KB)")
|
| 36 |
-
|
| 37 |
-
# Check if it's supposed to be a ZIP file
|
| 38 |
-
if file_path.suffix.lower() == '.zip' or 'cams' in file_path.name.lower():
|
| 39 |
-
print(f" Expected: ZIP file")
|
| 40 |
-
|
| 41 |
-
# Test if it's actually a ZIP
|
| 42 |
-
if zipfile.is_zipfile(file_path):
|
| 43 |
-
print(f" ✅ Valid ZIP file")
|
| 44 |
-
try:
|
| 45 |
-
with zipfile.ZipFile(file_path, 'r') as zf:
|
| 46 |
-
contents = zf.namelist()
|
| 47 |
-
print(f" 📦 Contains {len(contents)} files:")
|
| 48 |
-
for content in contents[:5]: # Show first 5 files
|
| 49 |
-
print(f" - {content}")
|
| 50 |
-
if len(contents) > 5:
|
| 51 |
-
print(f" ... and {len(contents) - 5} more")
|
| 52 |
-
except Exception as e:
|
| 53 |
-
print(f" ⚠️ Error reading ZIP: {e}")
|
| 54 |
-
else:
|
| 55 |
-
print(f" ❌ NOT a valid ZIP file")
|
| 56 |
-
|
| 57 |
-
# Try to read first few bytes to see what it actually is
|
| 58 |
-
try:
|
| 59 |
-
with open(file_path, 'rb') as f:
|
| 60 |
-
header = f.read(100)
|
| 61 |
-
print(f" 🔍 File header (first 100 bytes): {header[:50]}...")
|
| 62 |
-
|
| 63 |
-
# Check for common error patterns
|
| 64 |
-
header_str = header.decode('utf-8', errors='ignore').lower()
|
| 65 |
-
if 'html' in header_str:
|
| 66 |
-
print(f" 🚨 Appears to be HTML (likely an error page)")
|
| 67 |
-
elif 'error' in header_str:
|
| 68 |
-
print(f" 🚨 Contains 'error' - likely an error response")
|
| 69 |
-
elif 'json' in header_str:
|
| 70 |
-
print(f" 🚨 Appears to be JSON (likely an API error)")
|
| 71 |
-
elif header.startswith(b'PK'):
|
| 72 |
-
print(f" 🤔 Has ZIP signature but zipfile module rejects it")
|
| 73 |
-
else:
|
| 74 |
-
print(f" ❓ Unknown file format")
|
| 75 |
-
|
| 76 |
-
except Exception as e:
|
| 77 |
-
print(f" ❌ Error reading file: {e}")
|
| 78 |
-
|
| 79 |
-
def test_cds_connection():
|
| 80 |
-
"""Test CDS API connection"""
|
| 81 |
-
print("\n🌐 Testing CDS API Connection")
|
| 82 |
-
print("-" * 30)
|
| 83 |
-
|
| 84 |
-
try:
|
| 85 |
-
import cdsapi
|
| 86 |
-
|
| 87 |
-
# Check for .cdsapirc file
|
| 88 |
-
cdsapirc_path = Path.home() / '.cdsapirc'
|
| 89 |
-
if cdsapirc_path.exists():
|
| 90 |
-
print("✅ .cdsapirc file found")
|
| 91 |
-
|
| 92 |
-
# Try to initialize client
|
| 93 |
-
try:
|
| 94 |
-
client = cdsapi.Client()
|
| 95 |
-
print("✅ CDS API client initialized successfully")
|
| 96 |
-
|
| 97 |
-
# Test a simple info request (doesn't download data)
|
| 98 |
-
print("🔄 Testing API connection...")
|
| 99 |
-
# Note: This is just a connection test, not actually downloading
|
| 100 |
-
print("✅ CDS API connection appears to be working")
|
| 101 |
-
print("💡 If downloads fail, it may be due to:")
|
| 102 |
-
print(" - Invalid date range")
|
| 103 |
-
print(" - CAMS service temporary issues")
|
| 104 |
-
print(" - Account limitations")
|
| 105 |
-
|
| 106 |
-
except Exception as e:
|
| 107 |
-
print(f"❌ CDS API client initialization failed: {e}")
|
| 108 |
-
|
| 109 |
-
else:
|
| 110 |
-
print("❌ .cdsapirc file not found")
|
| 111 |
-
print("💡 Create ~/.cdsapirc with your CDS API credentials")
|
| 112 |
-
|
| 113 |
-
except ImportError:
|
| 114 |
-
print("❌ cdsapi module not installed")
|
| 115 |
-
print("💡 Install with: pip install cdsapi")
|
| 116 |
-
|
| 117 |
-
def suggest_solutions():
|
| 118 |
-
"""Suggest solutions for common issues"""
|
| 119 |
-
print("\n💡 Common Solutions")
|
| 120 |
-
print("-" * 20)
|
| 121 |
-
print("1. 🔄 Try a different date (some dates may not have data)")
|
| 122 |
-
print("2. 🕐 Wait and retry (CAMS servers may be busy)")
|
| 123 |
-
print("3. 🔑 Check CDS API credentials in ~/.cdsapirc")
|
| 124 |
-
print("4. 🗑️ Clear downloads directory and retry")
|
| 125 |
-
print("5. 📅 Use more recent dates (last 30 days usually work)")
|
| 126 |
-
print("6. 🌐 Check CDS website status: https://cds.climate.copernicus.eu/")
|
| 127 |
-
|
| 128 |
-
if __name__ == "__main__":
|
| 129 |
-
diagnose_cams_downloads()
|
| 130 |
-
test_cds_connection()
|
| 131 |
-
suggest_solutions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startup.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Startup script to ensure all required files are available before running the main app
|
| 4 |
-
"""
|
| 5 |
-
import os
|
| 6 |
-
import sys
|
| 7 |
-
import subprocess
|
| 8 |
-
import urllib.request
|
| 9 |
-
import zipfile
|
| 10 |
-
import geopandas as gpd
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
def ensure_shapefile_exists():
|
| 14 |
-
"""Ensure the India State Boundary shapefile exists"""
|
| 15 |
-
shapefile_path = "shapefiles/India_State_Boundary.shp"
|
| 16 |
-
|
| 17 |
-
if os.path.exists(shapefile_path):
|
| 18 |
-
print(f"✓ Shapefile found at {shapefile_path}")
|
| 19 |
-
return True
|
| 20 |
-
|
| 21 |
-
print(f"✗ Shapefile not found at {shapefile_path}")
|
| 22 |
-
|
| 23 |
-
# Try to pull from git lfs if available
|
| 24 |
-
try:
|
| 25 |
-
if os.path.exists(".git"):
|
| 26 |
-
print("Attempting to pull LFS files...")
|
| 27 |
-
result = subprocess.run(["git", "lfs", "pull"], capture_output=True, text=True)
|
| 28 |
-
if result.returncode == 0 and os.path.exists(shapefile_path):
|
| 29 |
-
print("✓ Successfully pulled LFS files")
|
| 30 |
-
return True
|
| 31 |
-
else:
|
| 32 |
-
print("✗ Git LFS pull failed or file still missing")
|
| 33 |
-
except Exception as e:
|
| 34 |
-
print(f"✗ Git LFS not available: {e}")
|
| 35 |
-
|
| 36 |
-
# Download alternative shapefile
|
| 37 |
-
print("Downloading alternative India boundary data...")
|
| 38 |
-
try:
|
| 39 |
-
# Create shapefiles directory
|
| 40 |
-
os.makedirs("shapefiles", exist_ok=True)
|
| 41 |
-
|
| 42 |
-
# Download from Natural Earth (reliable source)
|
| 43 |
-
url = "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_0_countries.zip"
|
| 44 |
-
zip_path = "temp_countries.zip"
|
| 45 |
-
|
| 46 |
-
print("Downloading Natural Earth country boundaries...")
|
| 47 |
-
urllib.request.urlretrieve(url, zip_path)
|
| 48 |
-
|
| 49 |
-
# Extract
|
| 50 |
-
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
| 51 |
-
zip_ref.extractall("temp_extract")
|
| 52 |
-
|
| 53 |
-
# Load and filter for India
|
| 54 |
-
world_data = gpd.read_file("temp_extract/ne_50m_admin_0_countries.shp")
|
| 55 |
-
india_data = world_data[world_data['NAME'] == 'India']
|
| 56 |
-
|
| 57 |
-
if len(india_data) == 0:
|
| 58 |
-
# Try alternative name matching
|
| 59 |
-
india_data = world_data[world_data['NAME_EN'] == 'India']
|
| 60 |
-
|
| 61 |
-
if len(india_data) > 0:
|
| 62 |
-
# Save as our expected shapefile
|
| 63 |
-
india_data.to_file(shapefile_path)
|
| 64 |
-
print(f"✓ Successfully created {shapefile_path}")
|
| 65 |
-
else:
|
| 66 |
-
raise Exception("Could not find India in the world data")
|
| 67 |
-
|
| 68 |
-
# Cleanup
|
| 69 |
-
os.remove(zip_path)
|
| 70 |
-
import shutil
|
| 71 |
-
shutil.rmtree("temp_extract", ignore_errors=True)
|
| 72 |
-
|
| 73 |
-
return True
|
| 74 |
-
|
| 75 |
-
except Exception as e:
|
| 76 |
-
print(f"✗ Failed to download alternative shapefile: {e}")
|
| 77 |
-
|
| 78 |
-
# Create a simple fallback
|
| 79 |
-
print("Creating fallback boundary...")
|
| 80 |
-
try:
|
| 81 |
-
from shapely.geometry import Polygon
|
| 82 |
-
|
| 83 |
-
# Simple India bounding box
|
| 84 |
-
india_bounds = [68.0, 6.0, 97.5, 37.0] # [min_lon, min_lat, max_lon, max_lat]
|
| 85 |
-
|
| 86 |
-
polygon = Polygon([
|
| 87 |
-
(india_bounds[0], india_bounds[1]), # SW
|
| 88 |
-
(india_bounds[2], india_bounds[1]), # SE
|
| 89 |
-
(india_bounds[2], india_bounds[3]), # NE
|
| 90 |
-
(india_bounds[0], india_bounds[3]), # NW
|
| 91 |
-
(india_bounds[0], india_bounds[1]) # Close
|
| 92 |
-
])
|
| 93 |
-
|
| 94 |
-
gdf = gpd.GeoDataFrame([1], geometry=[polygon], crs="EPSG:4326")
|
| 95 |
-
gdf['NAME'] = 'India'
|
| 96 |
-
gdf.to_file(shapefile_path)
|
| 97 |
-
|
| 98 |
-
print(f"✓ Created fallback boundary at {shapefile_path}")
|
| 99 |
-
return True
|
| 100 |
-
|
| 101 |
-
except Exception as e2:
|
| 102 |
-
print(f"✗ Failed to create fallback: {e2}")
|
| 103 |
-
return False
|
| 104 |
-
|
| 105 |
-
if __name__ == "__main__":
|
| 106 |
-
print("🚀 Starting CAMS Pollution Dashboard...")
|
| 107 |
-
print("Checking required files...")
|
| 108 |
-
|
| 109 |
-
if ensure_shapefile_exists():
|
| 110 |
-
print("✓ All required files are ready")
|
| 111 |
-
print("Starting Flask application...")
|
| 112 |
-
|
| 113 |
-
# Import and run the main app
|
| 114 |
-
from app import app
|
| 115 |
-
app.run(host='0.0.0.0', port=7860, debug=False)
|
| 116 |
-
else:
|
| 117 |
-
print("✗ Failed to ensure required files exist")
|
| 118 |
-
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/aurora_predict.html
CHANGED
|
@@ -286,13 +286,13 @@
|
|
| 286 |
<a href="{{ url_for('index') }}" class="back-link">Back to Main Dashboard</a>
|
| 287 |
|
| 288 |
<div class="header">
|
| 289 |
-
<h1
|
| 290 |
<p>Generate AI-powered air pollution forecasts using Microsoft's Aurora model</p>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
<div class="form-container">
|
| 294 |
<div class="info-box">
|
| 295 |
-
<h3
|
| 296 |
<ul>
|
| 297 |
<li><strong>Dual Time Input:</strong> Uses both T-1 (00:00) and T (12:00) timestamps for better accuracy</li>
|
| 298 |
<li><strong>Forward Predictions:</strong> Generate 1-4 steps forward, each covering 12 hours</li>
|
|
@@ -303,7 +303,7 @@
|
|
| 303 |
</div>
|
| 304 |
|
| 305 |
<div class="warning-box">
|
| 306 |
-
<h3
|
| 307 |
<p><strong>CPU Mode:</strong> Aurora will run on CPU for local testing. This is slower but doesn't require GPU.</p>
|
| 308 |
<p><strong>GPU Mode:</strong> If CUDA GPU is available, Aurora will use it for faster predictions.</p>
|
| 309 |
<p><strong>Processing Time:</strong> CPU: 5-15 minutes per step | GPU: 1-3 minutes total</p>
|
|
@@ -313,7 +313,7 @@
|
|
| 313 |
|
| 314 |
<form method="POST">
|
| 315 |
<div class="form-group">
|
| 316 |
-
<label for="date"
|
| 317 |
<input type="date"
|
| 318 |
id="date"
|
| 319 |
name="date"
|
|
@@ -327,7 +327,7 @@
|
|
| 327 |
</div>
|
| 328 |
|
| 329 |
<div class="form-group">
|
| 330 |
-
<label for="steps"
|
| 331 |
<select id="steps" name="steps" required>
|
| 332 |
<option value="1">1 step (12 hours forward) - Fastest</option>
|
| 333 |
<option value="2" selected>2 steps (24 hours forward) - CPU Friendly</option>
|
|
@@ -341,14 +341,14 @@
|
|
| 341 |
</div>
|
| 342 |
|
| 343 |
<button type="submit" class="btn" id="predictBtn">
|
| 344 |
-
|
| 345 |
</button>
|
| 346 |
</form>
|
| 347 |
|
| 348 |
<!-- Loading Overlay -->
|
| 349 |
<div class="loading-overlay" id="loadingOverlay">
|
| 350 |
<div class="loading-content">
|
| 351 |
-
<div class="aurora-icon"
|
| 352 |
<h2 style="color: #667eea; margin-bottom: 10px;">Aurora AI Processing</h2>
|
| 353 |
<p style="color: #666; margin-bottom: 20px;">Generating atmospheric predictions using Microsoft's Aurora model...</p>
|
| 354 |
|
|
@@ -393,7 +393,7 @@
|
|
| 393 |
</div>
|
| 394 |
|
| 395 |
<div style="margin-top: 30px; padding: 20px; background: #f5f5f5; border-radius: 10px;">
|
| 396 |
-
<h3 style="color: #555; margin-bottom: 15px;"
|
| 397 |
<ul style="color: #666; margin-left: 20px;">
|
| 398 |
<li>Interactive visualization of predicted air pollution concentrations</li>
|
| 399 |
<li>Step-by-step forecast evolution over time</li>
|
|
|
|
| 286 |
<a href="{{ url_for('index') }}" class="back-link">Back to Main Dashboard</a>
|
| 287 |
|
| 288 |
<div class="header">
|
| 289 |
+
<h1>Aurora ML Predictions</h1>
|
| 290 |
<p>Generate AI-powered air pollution forecasts using Microsoft's Aurora model</p>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
<div class="form-container">
|
| 294 |
<div class="info-box">
|
| 295 |
+
<h3>Enhanced Aurora Features</h3>
|
| 296 |
<ul>
|
| 297 |
<li><strong>Dual Time Input:</strong> Uses both T-1 (00:00) and T (12:00) timestamps for better accuracy</li>
|
| 298 |
<li><strong>Forward Predictions:</strong> Generate 1-4 steps forward, each covering 12 hours</li>
|
|
|
|
| 303 |
</div>
|
| 304 |
|
| 305 |
<div class="warning-box">
|
| 306 |
+
<h3>Performance Notes</h3>
|
| 307 |
<p><strong>CPU Mode:</strong> Aurora will run on CPU for local testing. This is slower but doesn't require GPU.</p>
|
| 308 |
<p><strong>GPU Mode:</strong> If CUDA GPU is available, Aurora will use it for faster predictions.</p>
|
| 309 |
<p><strong>Processing Time:</strong> CPU: 5-15 minutes per step | GPU: 1-3 minutes total</p>
|
|
|
|
| 313 |
|
| 314 |
<form method="POST">
|
| 315 |
<div class="form-group">
|
| 316 |
+
<label for="date">Select Date for Initial Conditions:</label>
|
| 317 |
<input type="date"
|
| 318 |
id="date"
|
| 319 |
name="date"
|
|
|
|
| 327 |
</div>
|
| 328 |
|
| 329 |
<div class="form-group">
|
| 330 |
+
<label for="steps">Number of Forward Prediction Steps:</label>
|
| 331 |
<select id="steps" name="steps" required>
|
| 332 |
<option value="1">1 step (12 hours forward) - Fastest</option>
|
| 333 |
<option value="2" selected>2 steps (24 hours forward) - CPU Friendly</option>
|
|
|
|
| 341 |
</div>
|
| 342 |
|
| 343 |
<button type="submit" class="btn" id="predictBtn">
|
| 344 |
+
Generate Aurora Predictions
|
| 345 |
</button>
|
| 346 |
</form>
|
| 347 |
|
| 348 |
<!-- Loading Overlay -->
|
| 349 |
<div class="loading-overlay" id="loadingOverlay">
|
| 350 |
<div class="loading-content">
|
| 351 |
+
<div class="aurora-icon"></div>
|
| 352 |
<h2 style="color: #667eea; margin-bottom: 10px;">Aurora AI Processing</h2>
|
| 353 |
<p style="color: #666; margin-bottom: 20px;">Generating atmospheric predictions using Microsoft's Aurora model...</p>
|
| 354 |
|
|
|
|
| 393 |
</div>
|
| 394 |
|
| 395 |
<div style="margin-top: 30px; padding: 20px; background: #f5f5f5; border-radius: 10px;">
|
| 396 |
+
<h3 style="color: #555; margin-bottom: 15px;">What You'll Get:</h3>
|
| 397 |
<ul style="color: #666; margin-left: 20px;">
|
| 398 |
<li>Interactive visualization of predicted air pollution concentrations</li>
|
| 399 |
<li>Step-by-step forecast evolution over time</li>
|
templates/aurora_prediction_plot.html
CHANGED
|
@@ -238,12 +238,12 @@
|
|
| 238 |
<a href="{{ url_for('index') }}" class="back-link">Back to Main Dashboard</a>
|
| 239 |
|
| 240 |
<div class="header">
|
| 241 |
-
<h1
|
| 242 |
<p>AI-powered atmospheric forecasting visualization</p>
|
| 243 |
</div>
|
| 244 |
|
| 245 |
<div class="step-indicator">
|
| 246 |
-
<h3
|
| 247 |
<p>Forecast time: T+{{ (step + 1) * 12 }}h ahead from initial conditions</p>
|
| 248 |
</div>
|
| 249 |
|
|
@@ -251,7 +251,7 @@
|
|
| 251 |
<div class="controls-container" id="controlsContainer">
|
| 252 |
<div class="controls-row">
|
| 253 |
<div class="form-group">
|
| 254 |
-
<label for="variable"
|
| 255 |
<select id="variable" name="variable">
|
| 256 |
{% for var in variables %}
|
| 257 |
<option value="{{ var }}" {% if var == var_name %}selected{% endif %}>
|
|
@@ -262,7 +262,7 @@
|
|
| 262 |
</div>
|
| 263 |
|
| 264 |
<div class="form-group">
|
| 265 |
-
<label for="step"
|
| 266 |
<select id="step" name="step">
|
| 267 |
{% for s in steps %}
|
| 268 |
<option value="{{ s }}" {% if s == step %}selected{% endif %}>
|
|
@@ -273,7 +273,7 @@
|
|
| 273 |
</div>
|
| 274 |
|
| 275 |
<div class="form-group">
|
| 276 |
-
<label for="color_theme"
|
| 277 |
<select id="color_theme" name="color_theme">
|
| 278 |
{% for theme_id, theme_name in color_themes.items() %}
|
| 279 |
<option value="{{ theme_id }}" {% if theme_id == current_color_theme %}selected{% endif %}>
|
|
@@ -286,7 +286,7 @@
|
|
| 286 |
<div class="form-group">
|
| 287 |
<label> </label>
|
| 288 |
<button type="submit" class="btn" id="updateBtn">
|
| 289 |
-
|
| 290 |
<div class="mini-loading" id="miniLoading"></div>
|
| 291 |
</button>
|
| 292 |
</div>
|
|
@@ -302,7 +302,7 @@
|
|
| 302 |
|
| 303 |
<div class="info-grid">
|
| 304 |
<div class="info-card">
|
| 305 |
-
<h3
|
| 306 |
<p><strong>Variable:</strong> {{ var_name }}</p>
|
| 307 |
<p><strong>Forecast Step:</strong> {{ step }}</p>
|
| 308 |
<p><strong>Time Ahead:</strong> {{ step * 6 }} hours</p>
|
|
@@ -310,7 +310,7 @@
|
|
| 310 |
</div>
|
| 311 |
|
| 312 |
<div class="info-card">
|
| 313 |
-
<h3
|
| 314 |
<p><strong>Model:</strong> Microsoft Aurora Air Pollution</p>
|
| 315 |
<p><strong>Version:</strong> 0.4</p>
|
| 316 |
<p><strong>Type:</strong> Foundation Model</p>
|
|
@@ -318,7 +318,7 @@
|
|
| 318 |
</div>
|
| 319 |
|
| 320 |
<div class="info-card">
|
| 321 |
-
<h3
|
| 322 |
<p><strong>Total Steps:</strong> {{ steps|length }}</p>
|
| 323 |
<p><strong>Step Interval:</strong> 6 hours</p>
|
| 324 |
<p><strong>Max Forecast:</strong> {{ (steps|length - 1) * 6 }} hours</p>
|
|
@@ -327,15 +327,15 @@
|
|
| 327 |
</div>
|
| 328 |
|
| 329 |
<div class="download-section">
|
| 330 |
-
<h3 style="color: #667eea; margin-bottom: 20px;"
|
| 331 |
<p style="margin-bottom: 20px; color: #666;">
|
| 332 |
Download the complete NetCDF file containing all forecast steps and variables
|
| 333 |
</p>
|
| 334 |
<a href="{{ download_url }}" class="download-btn">
|
| 335 |
-
|
| 336 |
</a>
|
| 337 |
<a href="{{ url_for('aurora_predict') }}" class="download-btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
| 338 |
-
|
| 339 |
</a>
|
| 340 |
</div>
|
| 341 |
</div>
|
|
@@ -370,7 +370,7 @@
|
|
| 370 |
miniLoading.style.display = 'inline-block';
|
| 371 |
controlsContainer.classList.add('form-updating');
|
| 372 |
updateBtn.disabled = true;
|
| 373 |
-
updateBtn.textContent = '
|
| 374 |
}
|
| 375 |
|
| 376 |
// Auto-hide loading indicator if page doesn't redirect within 10 seconds
|
|
@@ -384,7 +384,7 @@
|
|
| 384 |
miniLoading.style.display = 'none';
|
| 385 |
controlsContainer.classList.remove('form-updating');
|
| 386 |
updateBtn.disabled = false;
|
| 387 |
-
updateBtn.innerHTML = '
|
| 388 |
}
|
| 389 |
});
|
| 390 |
</script>
|
|
|
|
| 238 |
<a href="{{ url_for('index') }}" class="back-link">Back to Main Dashboard</a>
|
| 239 |
|
| 240 |
<div class="header">
|
| 241 |
+
<h1>Aurora ML Prediction Results</h1>
|
| 242 |
<p>AI-powered atmospheric forecasting visualization</p>
|
| 243 |
</div>
|
| 244 |
|
| 245 |
<div class="step-indicator">
|
| 246 |
+
<h3>Current View: Step {{ step + 1 }} of {{ max_steps }}</h3>
|
| 247 |
<p>Forecast time: T+{{ (step + 1) * 12 }}h ahead from initial conditions</p>
|
| 248 |
</div>
|
| 249 |
|
|
|
|
| 251 |
<div class="controls-container" id="controlsContainer">
|
| 252 |
<div class="controls-row">
|
| 253 |
<div class="form-group">
|
| 254 |
+
<label for="variable">Variable:</label>
|
| 255 |
<select id="variable" name="variable">
|
| 256 |
{% for var in variables %}
|
| 257 |
<option value="{{ var }}" {% if var == var_name %}selected{% endif %}>
|
|
|
|
| 262 |
</div>
|
| 263 |
|
| 264 |
<div class="form-group">
|
| 265 |
+
<label for="step">Forecast Step:</label>
|
| 266 |
<select id="step" name="step">
|
| 267 |
{% for s in steps %}
|
| 268 |
<option value="{{ s }}" {% if s == step %}selected{% endif %}>
|
|
|
|
| 273 |
</div>
|
| 274 |
|
| 275 |
<div class="form-group">
|
| 276 |
+
<label for="color_theme">Color Theme:</label>
|
| 277 |
<select id="color_theme" name="color_theme">
|
| 278 |
{% for theme_id, theme_name in color_themes.items() %}
|
| 279 |
<option value="{{ theme_id }}" {% if theme_id == current_color_theme %}selected{% endif %}>
|
|
|
|
| 286 |
<div class="form-group">
|
| 287 |
<label> </label>
|
| 288 |
<button type="submit" class="btn" id="updateBtn">
|
| 289 |
+
Update View
|
| 290 |
<div class="mini-loading" id="miniLoading"></div>
|
| 291 |
</button>
|
| 292 |
</div>
|
|
|
|
| 302 |
|
| 303 |
<div class="info-grid">
|
| 304 |
<div class="info-card">
|
| 305 |
+
<h3>Variable Information</h3>
|
| 306 |
<p><strong>Variable:</strong> {{ var_name }}</p>
|
| 307 |
<p><strong>Forecast Step:</strong> {{ step }}</p>
|
| 308 |
<p><strong>Time Ahead:</strong> {{ step * 6 }} hours</p>
|
|
|
|
| 310 |
</div>
|
| 311 |
|
| 312 |
<div class="info-card">
|
| 313 |
+
<h3>Model Information</h3>
|
| 314 |
<p><strong>Model:</strong> Microsoft Aurora Air Pollution</p>
|
| 315 |
<p><strong>Version:</strong> 0.4</p>
|
| 316 |
<p><strong>Type:</strong> Foundation Model</p>
|
|
|
|
| 318 |
</div>
|
| 319 |
|
| 320 |
<div class="info-card">
|
| 321 |
+
<h3>Forecast Details</h3>
|
| 322 |
<p><strong>Total Steps:</strong> {{ steps|length }}</p>
|
| 323 |
<p><strong>Step Interval:</strong> 6 hours</p>
|
| 324 |
<p><strong>Max Forecast:</strong> {{ (steps|length - 1) * 6 }} hours</p>
|
|
|
|
| 327 |
</div>
|
| 328 |
|
| 329 |
<div class="download-section">
|
| 330 |
+
<h3 style="color: #667eea; margin-bottom: 20px;">Download Prediction Data</h3>
|
| 331 |
<p style="margin-bottom: 20px; color: #666;">
|
| 332 |
Download the complete NetCDF file containing all forecast steps and variables
|
| 333 |
</p>
|
| 334 |
<a href="{{ download_url }}" class="download-btn">
|
| 335 |
+
Download NetCDF File
|
| 336 |
</a>
|
| 337 |
<a href="{{ url_for('aurora_predict') }}" class="download-btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
| 338 |
+
Generate New Prediction
|
| 339 |
</a>
|
| 340 |
</div>
|
| 341 |
</div>
|
|
|
|
| 370 |
miniLoading.style.display = 'inline-block';
|
| 371 |
controlsContainer.classList.add('form-updating');
|
| 372 |
updateBtn.disabled = true;
|
| 373 |
+
updateBtn.textContent = 'Updating...';
|
| 374 |
}
|
| 375 |
|
| 376 |
// Auto-hide loading indicator if page doesn't redirect within 10 seconds
|
|
|
|
| 384 |
miniLoading.style.display = 'none';
|
| 385 |
controlsContainer.classList.remove('form-updating');
|
| 386 |
updateBtn.disabled = false;
|
| 387 |
+
updateBtn.innerHTML = 'Update View<div class="mini-loading" id="miniLoading"></div>';
|
| 388 |
}
|
| 389 |
});
|
| 390 |
</script>
|
templates/aurora_variables.html
CHANGED
|
@@ -141,17 +141,17 @@
|
|
| 141 |
<body>
|
| 142 |
<div class="container">
|
| 143 |
<a href="{{ url_for('prediction_runs') }}" class="back-link">← Back to Prediction Runs</a>
|
| 144 |
-
|
| 145 |
-
<h1
|
| 146 |
-
|
| 147 |
<div class="info-box">
|
| 148 |
-
<strong
|
| 149 |
-
<strong
|
| 150 |
</div>
|
| 151 |
|
| 152 |
<!-- Step 1: Step Selection -->
|
| 153 |
<div class="method-section">
|
| 154 |
-
<h2
|
| 155 |
<p>Choose which prediction time step to analyze:</p>
|
| 156 |
<div class="step-selector">
|
| 157 |
{% for step_data in steps_data %}
|
|
@@ -165,8 +165,8 @@
|
|
| 165 |
|
| 166 |
<!-- Step 2: Variable Selection (hidden until step selected) -->
|
| 167 |
<div id="variableSection" class="hidden-section">
|
| 168 |
-
|
| 169 |
-
<h2
|
| 170 |
<div id="variableLoading" class="loading">Loading variables...</div>
|
| 171 |
<div id="variableContent" class="hidden-section">
|
| 172 |
<form method="POST" action="{{ url_for('aurora_plot') }}" id="plotForm">
|
|
@@ -182,7 +182,7 @@
|
|
| 182 |
|
| 183 |
<!-- Step 3: Pressure Level (shown for atmospheric variables) -->
|
| 184 |
<div id="pressureSection" class="hidden-section">
|
| 185 |
-
<h2
|
| 186 |
<div class="form-group">
|
| 187 |
<label for="pressure_level">Pressure Level (hPa):</label>
|
| 188 |
<select name="pressure_level" id="pressure_level">
|
|
@@ -192,7 +192,7 @@
|
|
| 192 |
|
| 193 |
<!-- Step 4: Color Theme Selection -->
|
| 194 |
<div id="plotOptionsSection" class="hidden-section">
|
| 195 |
-
<h2
|
| 196 |
<div class="form-group">
|
| 197 |
<label for="color_theme">Select Color Scheme:</label>
|
| 198 |
<select name="color_theme" id="color_theme" onchange="updateColorPreview()">
|
|
@@ -212,12 +212,12 @@
|
|
| 212 |
</div>
|
| 213 |
|
| 214 |
<!-- Step 5: Generate Plot -->
|
| 215 |
-
<h2
|
| 216 |
<button type="submit" name="plot_type" value="static" class="btn" id="staticPlotBtn" disabled>
|
| 217 |
-
|
| 218 |
</button>
|
| 219 |
<button type="submit" name="plot_type" value="interactive" class="btn btn-success" id="interactivePlotBtn" disabled>
|
| 220 |
-
|
| 221 |
</button>
|
| 222 |
</div>
|
| 223 |
</form>
|
|
@@ -227,9 +227,9 @@
|
|
| 227 |
|
| 228 |
<!-- Download Section -->
|
| 229 |
<div class="method-section">
|
| 230 |
-
<h2
|
| 231 |
<a href="{{ url_for('download_prediction_netcdf', filename=run_dir) }}" class="btn btn-secondary">
|
| 232 |
-
|
| 233 |
</a>
|
| 234 |
</div>
|
| 235 |
</div>
|
|
|
|
| 141 |
<body>
|
| 142 |
<div class="container">
|
| 143 |
<a href="{{ url_for('prediction_runs') }}" class="back-link">← Back to Prediction Runs</a>
|
| 144 |
+
|
| 145 |
+
<h1>Aurora Prediction Variables</h1>
|
| 146 |
+
|
| 147 |
<div class="info-box">
|
| 148 |
+
<strong>Run Directory:</strong> {{ run_dir }}<br>
|
| 149 |
+
<strong>Steps Available:</strong> {{ steps_data|length }} ({{ (steps_data|length * 12) }}h coverage)
|
| 150 |
</div>
|
| 151 |
|
| 152 |
<!-- Step 1: Step Selection -->
|
| 153 |
<div class="method-section">
|
| 154 |
+
<h2>Step 1: Select Prediction Step</h2>
|
| 155 |
<p>Choose which prediction time step to analyze:</p>
|
| 156 |
<div class="step-selector">
|
| 157 |
{% for step_data in steps_data %}
|
|
|
|
| 165 |
|
| 166 |
<!-- Step 2: Variable Selection (hidden until step selected) -->
|
| 167 |
<div id="variableSection" class="hidden-section">
|
| 168 |
+
<div class="form-section">
|
| 169 |
+
<h2>Step 2: Select Variable</h2>
|
| 170 |
<div id="variableLoading" class="loading">Loading variables...</div>
|
| 171 |
<div id="variableContent" class="hidden-section">
|
| 172 |
<form method="POST" action="{{ url_for('aurora_plot') }}" id="plotForm">
|
|
|
|
| 182 |
|
| 183 |
<!-- Step 3: Pressure Level (shown for atmospheric variables) -->
|
| 184 |
<div id="pressureSection" class="hidden-section">
|
| 185 |
+
<h2>Step 3: Select Pressure Level</h2>
|
| 186 |
<div class="form-group">
|
| 187 |
<label for="pressure_level">Pressure Level (hPa):</label>
|
| 188 |
<select name="pressure_level" id="pressure_level">
|
|
|
|
| 192 |
|
| 193 |
<!-- Step 4: Color Theme Selection -->
|
| 194 |
<div id="plotOptionsSection" class="hidden-section">
|
| 195 |
+
<h2>Step 4: Select Color Theme</h2>
|
| 196 |
<div class="form-group">
|
| 197 |
<label for="color_theme">Select Color Scheme:</label>
|
| 198 |
<select name="color_theme" id="color_theme" onchange="updateColorPreview()">
|
|
|
|
| 212 |
</div>
|
| 213 |
|
| 214 |
<!-- Step 5: Generate Plot -->
|
| 215 |
+
<h2>Step 5: Generate Plot</h2>
|
| 216 |
<button type="submit" name="plot_type" value="static" class="btn" id="staticPlotBtn" disabled>
|
| 217 |
+
Generate Static Plot
|
| 218 |
</button>
|
| 219 |
<button type="submit" name="plot_type" value="interactive" class="btn btn-success" id="interactivePlotBtn" disabled>
|
| 220 |
+
Generate Interactive Plot
|
| 221 |
</button>
|
| 222 |
</div>
|
| 223 |
</form>
|
|
|
|
| 227 |
|
| 228 |
<!-- Download Section -->
|
| 229 |
<div class="method-section">
|
| 230 |
+
<h2>Download Data</h2>
|
| 231 |
<a href="{{ url_for('download_prediction_netcdf', filename=run_dir) }}" class="btn btn-secondary">
|
| 232 |
+
Download All Files
|
| 233 |
</a>
|
| 234 |
</div>
|
| 235 |
</div>
|
templates/gallery.html
CHANGED
|
@@ -245,7 +245,7 @@
|
|
| 245 |
<body>
|
| 246 |
<div class="container">
|
| 247 |
<div class="header">
|
| 248 |
-
<h1
|
| 249 |
<p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
|
| 250 |
View and manage all your saved pollution maps
|
| 251 |
</p>
|
|
@@ -255,7 +255,7 @@
|
|
| 255 |
<a href="{{ url_for('index') }}" class="btn btn-secondary">← Back to Dashboard</a>
|
| 256 |
<a href="{{ url_for('cleanup') }}" class="btn btn-danger"
|
| 257 |
onclick="return confirm('This will delete plots older than 24 hours. Continue?')">
|
| 258 |
-
|
| 259 |
</a>
|
| 260 |
</div>
|
| 261 |
|
|
@@ -279,29 +279,28 @@
|
|
| 279 |
|
| 280 |
<!-- Interactive Plots Section -->
|
| 281 |
<div class="section">
|
| 282 |
-
<h2
|
| 283 |
{% if interactive_plots %}
|
| 284 |
<div class="plots-grid">
|
| 285 |
{% for plot in interactive_plots %}
|
| 286 |
<div class="plot-card">
|
| 287 |
<div class="plot-preview interactive">
|
| 288 |
-
🌍
|
| 289 |
</div>
|
| 290 |
<div class="plot-info">
|
| 291 |
<h3>{{ plot.variable|title }} - {{ plot.region|title }}{{ ' (' + plot.plot_type + ')' if plot.plot_type else '' }}</h3>
|
| 292 |
<div class="plot-meta">
|
| 293 |
-
<span
|
| 294 |
-
<span
|
| 295 |
-
<span
|
| 296 |
-
<span
|
| 297 |
</div>
|
| 298 |
<div class="plot-actions">
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
</div>
|
| 306 |
</div>
|
| 307 |
</div>
|
|
@@ -317,7 +316,7 @@
|
|
| 317 |
|
| 318 |
<!-- Static Plots Section -->
|
| 319 |
<div class="section">
|
| 320 |
-
<h2
|
| 321 |
{% if static_plots %}
|
| 322 |
<div class="plots-grid">
|
| 323 |
{% for plot in static_plots %}
|
|
@@ -325,23 +324,23 @@
|
|
| 325 |
<div class="plot-preview">
|
| 326 |
<img src="{{ url_for('serve_plot', filename=plot.filename) }}"
|
| 327 |
alt="{{ plot.variable }} plot"
|
| 328 |
-
onerror="this.style.display='none'; this.parentElement.innerHTML='<div style=\'color: #e74c3c; text-align: center;\'
|
| 329 |
</div>
|
| 330 |
<div class="plot-info">
|
| 331 |
<h3>{{ plot.variable|title }} - {{ plot.region|title }}{{ ' (' + plot.plot_type + ')' if plot.plot_type else '' }}</h3>
|
| 332 |
<div class="plot-meta">
|
| 333 |
-
<span
|
| 334 |
-
<span
|
| 335 |
-
<span
|
| 336 |
-
<span
|
| 337 |
</div>
|
| 338 |
<div class="plot-actions">
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
</div>
|
| 346 |
</div>
|
| 347 |
</div>
|
|
@@ -363,8 +362,8 @@
|
|
| 363 |
viewButtons.forEach(button => {
|
| 364 |
button.addEventListener('click', function() {
|
| 365 |
const originalText = this.textContent;
|
| 366 |
-
this.textContent = '
|
| 367 |
-
|
| 368 |
// Reset after a delay if still on page
|
| 369 |
setTimeout(() => {
|
| 370 |
this.textContent = originalText;
|
|
@@ -376,8 +375,8 @@
|
|
| 376 |
downloadButtons.forEach(button => {
|
| 377 |
button.addEventListener('click', function() {
|
| 378 |
const originalText = this.textContent;
|
| 379 |
-
this.textContent = '
|
| 380 |
-
|
| 381 |
setTimeout(() => {
|
| 382 |
this.textContent = originalText;
|
| 383 |
}, 2000);
|
|
@@ -403,7 +402,7 @@
|
|
| 403 |
}
|
| 404 |
|
| 405 |
const originalText = button.textContent;
|
| 406 |
-
button.textContent = '
|
| 407 |
button.disabled = true;
|
| 408 |
|
| 409 |
fetch(`/delete_plot/${filename}`, {
|
|
@@ -432,7 +431,7 @@
|
|
| 432 |
background: #27ae60; color: white; padding: 15px 25px;
|
| 433 |
border-radius: 8px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
| 434 |
`;
|
| 435 |
-
successMsg.textContent =
|
| 436 |
document.body.appendChild(successMsg);
|
| 437 |
|
| 438 |
setTimeout(() => {
|
|
|
|
| 245 |
<body>
|
| 246 |
<div class="container">
|
| 247 |
<div class="header">
|
| 248 |
+
<h1>Plot Gallery</h1>
|
| 249 |
<p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
|
| 250 |
View and manage all your saved pollution maps
|
| 251 |
</p>
|
|
|
|
| 255 |
<a href="{{ url_for('index') }}" class="btn btn-secondary">← Back to Dashboard</a>
|
| 256 |
<a href="{{ url_for('cleanup') }}" class="btn btn-danger"
|
| 257 |
onclick="return confirm('This will delete plots older than 24 hours. Continue?')">
|
| 258 |
+
Clean Old Plots
|
| 259 |
</a>
|
| 260 |
</div>
|
| 261 |
|
|
|
|
| 279 |
|
| 280 |
<!-- Interactive Plots Section -->
|
| 281 |
<div class="section">
|
| 282 |
+
<h2>Interactive Plots</h2>
|
| 283 |
{% if interactive_plots %}
|
| 284 |
<div class="plots-grid">
|
| 285 |
{% for plot in interactive_plots %}
|
| 286 |
<div class="plot-card">
|
| 287 |
<div class="plot-preview interactive">
|
|
|
|
| 288 |
</div>
|
| 289 |
<div class="plot-info">
|
| 290 |
<h3>{{ plot.variable|title }} - {{ plot.region|title }}{{ ' (' + plot.plot_type + ')' if plot.plot_type else '' }}</h3>
|
| 291 |
<div class="plot-meta">
|
| 292 |
+
<span>{{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
|
| 293 |
+
<span>{{ plot.theme|title }}</span>
|
| 294 |
+
<span>{{ "%.1f"|format(plot.size/1024) }} KB</span>
|
| 295 |
+
<span>Interactive</span>
|
| 296 |
</div>
|
| 297 |
<div class="plot-actions">
|
| 298 |
+
<a href="{{ url_for('view_interactive_plot', filename=plot.filename) }}"
|
| 299 |
+
class="btn-small btn-view">View</a>
|
| 300 |
+
<a href="{{ url_for('serve_plot', filename=plot.filename) }}"
|
| 301 |
+
class="btn-small btn-download" download>Download</a>
|
| 302 |
+
<button onclick="deletePlot('{{ plot.filename }}')"
|
| 303 |
+
class="btn-small btn-delete">Delete</button>
|
| 304 |
</div>
|
| 305 |
</div>
|
| 306 |
</div>
|
|
|
|
| 316 |
|
| 317 |
<!-- Static Plots Section -->
|
| 318 |
<div class="section">
|
| 319 |
+
<h2>Static Plots</h2>
|
| 320 |
{% if static_plots %}
|
| 321 |
<div class="plots-grid">
|
| 322 |
{% for plot in static_plots %}
|
|
|
|
| 324 |
<div class="plot-preview">
|
| 325 |
<img src="{{ url_for('serve_plot', filename=plot.filename) }}"
|
| 326 |
alt="{{ plot.variable }} plot"
|
| 327 |
+
onerror="this.style.display='none'; this.parentElement.innerHTML='<div style=\'color: #e74c3c; text-align: center;\'>Preview unavailable</div>'">
|
| 328 |
</div>
|
| 329 |
<div class="plot-info">
|
| 330 |
<h3>{{ plot.variable|title }} - {{ plot.region|title }}{{ ' (' + plot.plot_type + ')' if plot.plot_type else '' }}</h3>
|
| 331 |
<div class="plot-meta">
|
| 332 |
+
<span>{{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
|
| 333 |
+
<span>{{ plot.theme|title }}</span>
|
| 334 |
+
<span>{{ "%.1f"|format(plot.size/1024) }} KB</span>
|
| 335 |
+
<span>{{ plot.extension.upper() }}</span>
|
| 336 |
</div>
|
| 337 |
<div class="plot-actions">
|
| 338 |
+
<a href="{{ url_for('serve_plot', filename=plot.filename) }}"
|
| 339 |
+
class="btn-small btn-view" target="_blank">View</a>
|
| 340 |
+
<a href="{{ url_for('serve_plot', filename=plot.filename) }}"
|
| 341 |
+
class="btn-small btn-download" download>Download</a>
|
| 342 |
+
<button onclick="deletePlot('{{ plot.filename }}')"
|
| 343 |
+
class="btn-small btn-delete">Delete</button>
|
| 344 |
</div>
|
| 345 |
</div>
|
| 346 |
</div>
|
|
|
|
| 362 |
viewButtons.forEach(button => {
|
| 363 |
button.addEventListener('click', function() {
|
| 364 |
const originalText = this.textContent;
|
| 365 |
+
this.textContent = 'Loading...';
|
| 366 |
+
|
| 367 |
// Reset after a delay if still on page
|
| 368 |
setTimeout(() => {
|
| 369 |
this.textContent = originalText;
|
|
|
|
| 375 |
downloadButtons.forEach(button => {
|
| 376 |
button.addEventListener('click', function() {
|
| 377 |
const originalText = this.textContent;
|
| 378 |
+
this.textContent = 'Downloading...';
|
| 379 |
+
|
| 380 |
setTimeout(() => {
|
| 381 |
this.textContent = originalText;
|
| 382 |
}, 2000);
|
|
|
|
| 402 |
}
|
| 403 |
|
| 404 |
const originalText = button.textContent;
|
| 405 |
+
button.textContent = 'Deleting...';
|
| 406 |
button.disabled = true;
|
| 407 |
|
| 408 |
fetch(`/delete_plot/${filename}`, {
|
|
|
|
| 431 |
background: #27ae60; color: white; padding: 15px 25px;
|
| 432 |
border-radius: 8px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
| 433 |
`;
|
| 434 |
+
successMsg.textContent = `${filename} deleted successfully`;
|
| 435 |
document.body.appendChild(successMsg);
|
| 436 |
|
| 437 |
setTimeout(() => {
|
templates/index.html
CHANGED
|
@@ -155,11 +155,11 @@
|
|
| 155 |
</head>
|
| 156 |
<body>
|
| 157 |
<div class="container">
|
| 158 |
-
<h1
|
| 159 |
|
| 160 |
<div style="text-align: center; margin-bottom: 20px;">
|
| 161 |
<a href="{{ url_for('gallery') }}" class="btn">
|
| 162 |
-
|
| 163 |
</a>
|
| 164 |
</div>
|
| 165 |
|
|
@@ -172,9 +172,9 @@
|
|
| 172 |
{% endwith %}
|
| 173 |
|
| 174 |
<div class="info-box">
|
| 175 |
-
<strong
|
| 176 |
<span class="status-indicator {% if cds_ready %}status-ready{% else %}status-error{% endif %}">
|
| 177 |
-
{% if cds_ready %}
|
| 178 |
</span>
|
| 179 |
{% if not cds_ready %}
|
| 180 |
<small class="help-text">
|
|
@@ -185,7 +185,7 @@
|
|
| 185 |
|
| 186 |
<div class="two-column">
|
| 187 |
<div class="method-section">
|
| 188 |
-
<h2
|
| 189 |
<p>Upload your own NetCDF (.nc) or ZIP (.zip) file containing CAMS data</p>
|
| 190 |
|
| 191 |
<form action="/upload" method="post" enctype="multipart/form-data">
|
|
@@ -197,12 +197,12 @@
|
|
| 197 |
<strong>Maximum size:</strong> 500MB
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
-
<button type="submit" class="btn"
|
| 201 |
</form>
|
| 202 |
</div>
|
| 203 |
|
| 204 |
<div class="method-section">
|
| 205 |
-
<h2
|
| 206 |
<p>Download CAMS data for a specific date (requires CDS API)</p>
|
| 207 |
|
| 208 |
<form action="/download_date" method="post">
|
|
@@ -213,7 +213,7 @@
|
|
| 213 |
</div>
|
| 214 |
|
| 215 |
<button type="submit" class="btn" id="download_btn" {% if not cds_ready %}disabled{% endif %}>
|
| 216 |
-
|
| 217 |
</button>
|
| 218 |
|
| 219 |
{% if not cds_ready %}
|
|
@@ -242,7 +242,7 @@
|
|
| 242 |
{% endif %} -->
|
| 243 |
|
| 244 |
<div class="method-section">
|
| 245 |
-
<h2
|
| 246 |
<p>Select a previously uploaded or downloaded file for visualization.</p>
|
| 247 |
<form id="recentFileForm" action="" method="get" style="display: flex; gap: 10px; align-items: center;">
|
| 248 |
<select name="recent_file" id="recent_file" required style="flex: 1;">
|
|
@@ -259,11 +259,11 @@
|
|
| 259 |
|
| 260 |
<div class="container">
|
| 261 |
<div class="method-section" style="border-left: 4px solid #9b59b6;">
|
| 262 |
-
<h2
|
| 263 |
<p>Generate AI-powered air pollution forecasts using Microsoft's Aurora foundation model</p>
|
| 264 |
|
| 265 |
<div style="background: #f8f9ff; padding: 15px; border-radius: 8px; margin: 15px 0; border: 2px solid #e3e7ff;">
|
| 266 |
-
<p style="margin-bottom: 10px;"><strong
|
| 267 |
<ul style="margin-left: 20px; color: #666;">
|
| 268 |
<li>Uses dual timestamps (T-1 and T) for improved prediction accuracy</li>
|
| 269 |
<li>Forward predictions from 1-4 steps (12-48 hours coverage)</li>
|
|
@@ -276,15 +276,15 @@
|
|
| 276 |
{% if aurora_available is defined and aurora_available %}
|
| 277 |
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
| 278 |
<a href="{{ url_for('aurora_predict') }}" class="btn" style="background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);">
|
| 279 |
-
|
| 280 |
</a>
|
| 281 |
<a href="{{ url_for('prediction_runs') }}" class="btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
| 282 |
-
|
| 283 |
</a>
|
| 284 |
</div>
|
| 285 |
{% else %}
|
| 286 |
<button class="btn" disabled style="background: #bdc3c7; cursor: not-allowed;">
|
| 287 |
-
|
| 288 |
</button>
|
| 289 |
<p style="margin-top: 10px; font-size: 14px; color: #721c24;">
|
| 290 |
Aurora dependencies not installed. Requires PyTorch and aurora-forecast package.
|
|
@@ -294,7 +294,7 @@
|
|
| 294 |
</div>
|
| 295 |
|
| 296 |
<div class="container">
|
| 297 |
-
<h2
|
| 298 |
<ol style="line-height: 1.8;">
|
| 299 |
<li><strong>Choose your method:</strong> Upload your own file or download data by date</li>
|
| 300 |
<li><strong>File Analysis:</strong> The system will detect air pollution variables in your data</li>
|
|
@@ -303,13 +303,13 @@
|
|
| 303 |
</ol>
|
| 304 |
|
| 305 |
<div class="info-box">
|
| 306 |
-
<strong
|
| 307 |
PM2.5, PM10, PM1, NO₂, SO₂, O₃, CO, NO, NH₃, Total Column measurements, and more
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
|
| 311 |
<div class="cleanup-section">
|
| 312 |
-
<h3
|
| 313 |
<p>Clean up old files to free up disk space</p>
|
| 314 |
<a href="/cleanup" class="btn">Clean Old Files</a>
|
| 315 |
</div>
|
|
@@ -337,7 +337,7 @@
|
|
| 337 |
uploadForm.addEventListener('submit', function(e) {
|
| 338 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 339 |
if (submitBtn) {
|
| 340 |
-
addLoadingState(submitBtn, '
|
| 341 |
}
|
| 342 |
});
|
| 343 |
}
|
|
@@ -352,7 +352,7 @@
|
|
| 352 |
}
|
| 353 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 354 |
if (submitBtn) {
|
| 355 |
-
addLoadingState(submitBtn, '
|
| 356 |
}
|
| 357 |
});
|
| 358 |
}
|
|
@@ -362,7 +362,7 @@
|
|
| 362 |
if (cleanupBtn) {
|
| 363 |
cleanupBtn.addEventListener('click', function(e) {
|
| 364 |
e.preventDefault();
|
| 365 |
-
this.textContent = '
|
| 366 |
this.style.pointerEvents = 'none';
|
| 367 |
window.location.href = this.href;
|
| 368 |
});
|
|
@@ -388,7 +388,7 @@
|
|
| 388 |
// Show loading state
|
| 389 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 390 |
if (submitBtn) {
|
| 391 |
-
addLoadingState(submitBtn, '
|
| 392 |
}
|
| 393 |
|
| 394 |
const [filename, filetype] = val.split('|');
|
|
|
|
| 155 |
</head>
|
| 156 |
<body>
|
| 157 |
<div class="container">
|
| 158 |
+
<h1>CAMS Air Pollution Visualization</h1>
|
| 159 |
|
| 160 |
<div style="text-align: center; margin-bottom: 20px;">
|
| 161 |
<a href="{{ url_for('gallery') }}" class="btn">
|
| 162 |
+
View Plot Gallery
|
| 163 |
</a>
|
| 164 |
</div>
|
| 165 |
|
|
|
|
| 172 |
{% endwith %}
|
| 173 |
|
| 174 |
<div class="info-box">
|
| 175 |
+
<strong>System Status:</strong>
|
| 176 |
<span class="status-indicator {% if cds_ready %}status-ready{% else %}status-error{% endif %}">
|
| 177 |
+
{% if cds_ready %}CDS API Ready{% else %}CDS API Not Configured{% endif %}
|
| 178 |
</span>
|
| 179 |
{% if not cds_ready %}
|
| 180 |
<small class="help-text">
|
|
|
|
| 185 |
|
| 186 |
<div class="two-column">
|
| 187 |
<div class="method-section">
|
| 188 |
+
<h2>Method 1: Upload File</h2>
|
| 189 |
<p>Upload your own NetCDF (.nc) or ZIP (.zip) file containing CAMS data</p>
|
| 190 |
|
| 191 |
<form action="/upload" method="post" enctype="multipart/form-data">
|
|
|
|
| 197 |
<strong>Maximum size:</strong> 500MB
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
+
<button type="submit" class="btn">Upload & Analyze</button>
|
| 201 |
</form>
|
| 202 |
</div>
|
| 203 |
|
| 204 |
<div class="method-section">
|
| 205 |
+
<h2>Method 2: Download by Date</h2>
|
| 206 |
<p>Download CAMS data for a specific date (requires CDS API)</p>
|
| 207 |
|
| 208 |
<form action="/download_date" method="post">
|
|
|
|
| 213 |
</div>
|
| 214 |
|
| 215 |
<button type="submit" class="btn" id="download_btn" {% if not cds_ready %}disabled{% endif %}>
|
| 216 |
+
Download & Analyze
|
| 217 |
</button>
|
| 218 |
|
| 219 |
{% if not cds_ready %}
|
|
|
|
| 242 |
{% endif %} -->
|
| 243 |
|
| 244 |
<div class="method-section">
|
| 245 |
+
<h2>Method 3: Choose from Recent Files</h2>
|
| 246 |
<p>Select a previously uploaded or downloaded file for visualization.</p>
|
| 247 |
<form id="recentFileForm" action="" method="get" style="display: flex; gap: 10px; align-items: center;">
|
| 248 |
<select name="recent_file" id="recent_file" required style="flex: 1;">
|
|
|
|
| 259 |
|
| 260 |
<div class="container">
|
| 261 |
<div class="method-section" style="border-left: 4px solid #9b59b6;">
|
| 262 |
+
<h2>Method 4: Aurora ML Predictions</h2>
|
| 263 |
<p>Generate AI-powered air pollution forecasts using Microsoft's Aurora foundation model</p>
|
| 264 |
|
| 265 |
<div style="background: #f8f9ff; padding: 15px; border-radius: 8px; margin: 15px 0; border: 2px solid #e3e7ff;">
|
| 266 |
+
<p style="margin-bottom: 10px;"><strong>Enhanced Aurora Features:</strong></p>
|
| 267 |
<ul style="margin-left: 20px; color: #666;">
|
| 268 |
<li>Uses dual timestamps (T-1 and T) for improved prediction accuracy</li>
|
| 269 |
<li>Forward predictions from 1-4 steps (12-48 hours coverage)</li>
|
|
|
|
| 276 |
{% if aurora_available is defined and aurora_available %}
|
| 277 |
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
| 278 |
<a href="{{ url_for('aurora_predict') }}" class="btn" style="background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);">
|
| 279 |
+
Generate Aurora Predictions
|
| 280 |
</a>
|
| 281 |
<a href="{{ url_for('prediction_runs') }}" class="btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
| 282 |
+
Browse Existing Predictions
|
| 283 |
</a>
|
| 284 |
</div>
|
| 285 |
{% else %}
|
| 286 |
<button class="btn" disabled style="background: #bdc3c7; cursor: not-allowed;">
|
| 287 |
+
Aurora Model Not Available
|
| 288 |
</button>
|
| 289 |
<p style="margin-top: 10px; font-size: 14px; color: #721c24;">
|
| 290 |
Aurora dependencies not installed. Requires PyTorch and aurora-forecast package.
|
|
|
|
| 294 |
</div>
|
| 295 |
|
| 296 |
<div class="container">
|
| 297 |
+
<h2>How to Use</h2>
|
| 298 |
<ol style="line-height: 1.8;">
|
| 299 |
<li><strong>Choose your method:</strong> Upload your own file or download data by date</li>
|
| 300 |
<li><strong>File Analysis:</strong> The system will detect air pollution variables in your data</li>
|
|
|
|
| 303 |
</ol>
|
| 304 |
|
| 305 |
<div class="info-box">
|
| 306 |
+
<strong>Supported Variables:</strong>
|
| 307 |
PM2.5, PM10, PM1, NO₂, SO₂, O₃, CO, NO, NH₃, Total Column measurements, and more
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
|
| 311 |
<div class="cleanup-section">
|
| 312 |
+
<h3>Maintenance</h3>
|
| 313 |
<p>Clean up old files to free up disk space</p>
|
| 314 |
<a href="/cleanup" class="btn">Clean Old Files</a>
|
| 315 |
</div>
|
|
|
|
| 337 |
uploadForm.addEventListener('submit', function(e) {
|
| 338 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 339 |
if (submitBtn) {
|
| 340 |
+
addLoadingState(submitBtn, 'Upload File');
|
| 341 |
}
|
| 342 |
});
|
| 343 |
}
|
|
|
|
| 352 |
}
|
| 353 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 354 |
if (submitBtn) {
|
| 355 |
+
addLoadingState(submitBtn, 'Download CAMS Data');
|
| 356 |
}
|
| 357 |
});
|
| 358 |
}
|
|
|
|
| 362 |
if (cleanupBtn) {
|
| 363 |
cleanupBtn.addEventListener('click', function(e) {
|
| 364 |
e.preventDefault();
|
| 365 |
+
this.textContent = 'Cleaning...';
|
| 366 |
this.style.pointerEvents = 'none';
|
| 367 |
window.location.href = this.href;
|
| 368 |
});
|
|
|
|
| 388 |
// Show loading state
|
| 389 |
const submitBtn = this.querySelector('button[type="submit"]');
|
| 390 |
if (submitBtn) {
|
| 391 |
+
addLoadingState(submitBtn, 'Analyze');
|
| 392 |
}
|
| 393 |
|
| 394 |
const [filename, filetype] = val.split('|');
|
templates/interactive_plot.html
CHANGED
|
@@ -197,34 +197,34 @@
|
|
| 197 |
<body>
|
| 198 |
<div class="container">
|
| 199 |
<div class="header">
|
| 200 |
-
<h1
|
| 201 |
<p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
|
| 202 |
Hover over the map to see exact coordinates and pollution values. Use the toolbar to zoom, pan, and download.
|
| 203 |
</p>
|
| 204 |
</div>
|
| 205 |
|
| 206 |
<div class="instructions">
|
| 207 |
-
<h3
|
| 208 |
<ul>
|
| 209 |
-
<li><strong
|
| 210 |
-
<li><strong
|
| 211 |
-
<li><strong
|
| 212 |
-
<li><strong
|
| 213 |
-
<li><strong
|
| 214 |
-
<li><strong
|
| 215 |
</ul>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
<div class="controls">
|
| 219 |
<a href="{{ url_for('index') }}" class="btn btn-back">← Back to Dashboard</a>
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
</div>
|
| 229 |
|
| 230 |
<div class="plot-container">
|
|
@@ -234,57 +234,57 @@
|
|
| 234 |
</div>
|
| 235 |
|
| 236 |
<div class="plot-info">
|
| 237 |
-
<h2 style="color: #2c3e50; margin-bottom: 20px;"
|
| 238 |
|
| 239 |
<div class="info-grid">
|
| 240 |
<div class="info-item">
|
| 241 |
-
<h3
|
| 242 |
<p>{{ plot_info.variable }}</p>
|
| 243 |
</div>
|
| 244 |
|
| 245 |
{% if plot_info.units %}
|
| 246 |
<div class="info-item">
|
| 247 |
-
<h3
|
| 248 |
<p>{{ plot_info.units }}</p>
|
| 249 |
</div>
|
| 250 |
{% endif %}
|
| 251 |
|
| 252 |
{% if plot_info.pressure_level %}
|
| 253 |
<div class="info-item">
|
| 254 |
-
<h3
|
| 255 |
<p>{{ plot_info.pressure_level }} hPa</p>
|
| 256 |
</div>
|
| 257 |
{% endif %}
|
| 258 |
|
| 259 |
<div class="info-item">
|
| 260 |
-
<h3
|
| 261 |
<p>{{ plot_info.color_theme }}</p>
|
| 262 |
</div>
|
| 263 |
|
| 264 |
<div class="info-item">
|
| 265 |
-
<h3
|
| 266 |
<p>{{ plot_info.shape }}</p>
|
| 267 |
</div>
|
| 268 |
|
| 269 |
<div class="info-item">
|
| 270 |
-
<h3
|
| 271 |
<p>{{ plot_info.generated_time }}</p>
|
| 272 |
</div>
|
| 273 |
<!-- </div>
|
| 274 |
|
| 275 |
<div class="info-grid"> -->
|
| 276 |
<div class="info-item">
|
| 277 |
-
<h3
|
| 278 |
<p>{{ "%.3f"|format(plot_info.data_range.min) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 279 |
</div>
|
| 280 |
|
| 281 |
<div class="info-item">
|
| 282 |
-
<h3
|
| 283 |
<p>{{ "%.3f"|format(plot_info.data_range.max) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 284 |
</div>
|
| 285 |
|
| 286 |
<div class="info-item">
|
| 287 |
-
<h3
|
| 288 |
<p>{{ "%.3f"|format(plot_info.data_range.mean) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 289 |
</div>
|
| 290 |
</div>
|
|
@@ -297,11 +297,11 @@
|
|
| 297 |
console.log('Interactive plot page loaded');
|
| 298 |
|
| 299 |
// Check if Plotly is loaded
|
| 300 |
-
|
| 301 |
console.error('Plotly is not loaded!');
|
| 302 |
document.getElementById('plotly-container').innerHTML =
|
| 303 |
'<div style="padding: 40px; text-align: center; color: red; border: 2px dashed red; margin: 20px;">' +
|
| 304 |
-
'<h3
|
| 305 |
'<p>Plotly library failed to load. Please refresh the page or check your internet connection.</p>' +
|
| 306 |
'</div>';
|
| 307 |
return;
|
|
@@ -310,13 +310,13 @@
|
|
| 310 |
// Look for the plotly div
|
| 311 |
const plotDiv = document.querySelector('[id^="interactive-plot"]') || document.querySelector('.plotly-graph-div');
|
| 312 |
|
| 313 |
-
|
| 314 |
console.error('No Plotly div found!');
|
| 315 |
document.getElementById('plotly-container').innerHTML =
|
| 316 |
'<div style="padding: 40px; text-align: center; color: orange; border: 2px dashed orange; margin: 20px;">' +
|
| 317 |
-
'<h3
|
| 318 |
'<p>The interactive plot could not be initialized. The data may be processing.</p>' +
|
| 319 |
-
'<button onclick="location.reload()" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;"
|
| 320 |
'</div>';
|
| 321 |
return;
|
| 322 |
}
|
|
@@ -333,11 +333,11 @@
|
|
| 333 |
// Add loading indicator for downloads
|
| 334 |
const downloadButtons = document.querySelectorAll('.btn-download');
|
| 335 |
downloadButtons.forEach(button => {
|
| 336 |
-
|
| 337 |
const originalText = this.textContent;
|
| 338 |
-
this.textContent = '
|
| 339 |
this.style.pointerEvents = 'none';
|
| 340 |
-
|
| 341 |
setTimeout(() => {
|
| 342 |
this.textContent = originalText;
|
| 343 |
this.style.pointerEvents = 'auto';
|
|
|
|
| 197 |
<body>
|
| 198 |
<div class="container">
|
| 199 |
<div class="header">
|
| 200 |
+
<h1>Interactive Air Pollution Map</h1>
|
| 201 |
<p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
|
| 202 |
Hover over the map to see exact coordinates and pollution values. Use the toolbar to zoom, pan, and download.
|
| 203 |
</p>
|
| 204 |
</div>
|
| 205 |
|
| 206 |
<div class="instructions">
|
| 207 |
+
<h3>How to Use This Interactive Map:</h3>
|
| 208 |
<ul>
|
| 209 |
+
<li><strong>Hover:</strong> Move your mouse over any point to see exact coordinates, pollution values, and location data</li>
|
| 210 |
+
<li><strong>Zoom:</strong> Use mouse wheel, zoom buttons in toolbar, or draw a rectangle to zoom to specific area</li>
|
| 211 |
+
<li><strong>Pan:</strong> Click and drag to move around the map</li>
|
| 212 |
+
<li><strong>Download:</strong> Click the camera icon in the toolbar to download as PNG image</li>
|
| 213 |
+
<li><strong>Reset:</strong> Double-click anywhere to reset zoom to original view</li>
|
| 214 |
+
<li><strong>Annotate:</strong> Use drawing tools in the toolbar to add lines, shapes, and annotations</li>
|
| 215 |
</ul>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
<div class="controls">
|
| 219 |
<a href="{{ url_for('index') }}" class="btn btn-back">← Back to Dashboard</a>
|
| 220 |
+
{% if plot_info.png_path %}
|
| 221 |
+
<a href="{{ url_for('serve_plot', filename=plot_info.png_path.split('/')[-1]) }}"
|
| 222 |
+
class="btn btn-download" download>Download PNG</a>
|
| 223 |
+
{% endif %}
|
| 224 |
+
{% if plot_info.html_path %}
|
| 225 |
+
<a href="{{ url_for('serve_plot', filename=plot_info.html_path.split('/')[-1]) }}"
|
| 226 |
+
class="btn btn-download" download>Download HTML</a>
|
| 227 |
+
{% endif %}
|
| 228 |
</div>
|
| 229 |
|
| 230 |
<div class="plot-container">
|
|
|
|
| 234 |
</div>
|
| 235 |
|
| 236 |
<div class="plot-info">
|
| 237 |
+
<h2 style="color: #2c3e50; margin-bottom: 20px;">Plot Information</h2>
|
| 238 |
|
| 239 |
<div class="info-grid">
|
| 240 |
<div class="info-item">
|
| 241 |
+
<h3>Variable</h3>
|
| 242 |
<p>{{ plot_info.variable }}</p>
|
| 243 |
</div>
|
| 244 |
|
| 245 |
{% if plot_info.units %}
|
| 246 |
<div class="info-item">
|
| 247 |
+
<h3>Units</h3>
|
| 248 |
<p>{{ plot_info.units }}</p>
|
| 249 |
</div>
|
| 250 |
{% endif %}
|
| 251 |
|
| 252 |
{% if plot_info.pressure_level %}
|
| 253 |
<div class="info-item">
|
| 254 |
+
<h3>Pressure Level</h3>
|
| 255 |
<p>{{ plot_info.pressure_level }} hPa</p>
|
| 256 |
</div>
|
| 257 |
{% endif %}
|
| 258 |
|
| 259 |
<div class="info-item">
|
| 260 |
+
<h3>Color Theme</h3>
|
| 261 |
<p>{{ plot_info.color_theme }}</p>
|
| 262 |
</div>
|
| 263 |
|
| 264 |
<div class="info-item">
|
| 265 |
+
<h3>Data Shape</h3>
|
| 266 |
<p>{{ plot_info.shape }}</p>
|
| 267 |
</div>
|
| 268 |
|
| 269 |
<div class="info-item">
|
| 270 |
+
<h3>Generated</h3>
|
| 271 |
<p>{{ plot_info.generated_time }}</p>
|
| 272 |
</div>
|
| 273 |
<!-- </div>
|
| 274 |
|
| 275 |
<div class="info-grid"> -->
|
| 276 |
<div class="info-item">
|
| 277 |
+
<h3>Minimum Value</h3>
|
| 278 |
<p>{{ "%.3f"|format(plot_info.data_range.min) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 279 |
</div>
|
| 280 |
|
| 281 |
<div class="info-item">
|
| 282 |
+
<h3>Maximum Value</h3>
|
| 283 |
<p>{{ "%.3f"|format(plot_info.data_range.max) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 284 |
</div>
|
| 285 |
|
| 286 |
<div class="info-item">
|
| 287 |
+
<h3>Average Value</h3>
|
| 288 |
<p>{{ "%.3f"|format(plot_info.data_range.mean) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
|
| 289 |
</div>
|
| 290 |
</div>
|
|
|
|
| 297 |
console.log('Interactive plot page loaded');
|
| 298 |
|
| 299 |
// Check if Plotly is loaded
|
| 300 |
+
if (typeof Plotly === 'undefined') {
|
| 301 |
console.error('Plotly is not loaded!');
|
| 302 |
document.getElementById('plotly-container').innerHTML =
|
| 303 |
'<div style="padding: 40px; text-align: center; color: red; border: 2px dashed red; margin: 20px;">' +
|
| 304 |
+
'<h3>Plot Loading Error</h3>' +
|
| 305 |
'<p>Plotly library failed to load. Please refresh the page or check your internet connection.</p>' +
|
| 306 |
'</div>';
|
| 307 |
return;
|
|
|
|
| 310 |
// Look for the plotly div
|
| 311 |
const plotDiv = document.querySelector('[id^="interactive-plot"]') || document.querySelector('.plotly-graph-div');
|
| 312 |
|
| 313 |
+
if (!plotDiv) {
|
| 314 |
console.error('No Plotly div found!');
|
| 315 |
document.getElementById('plotly-container').innerHTML =
|
| 316 |
'<div style="padding: 40px; text-align: center; color: orange; border: 2px dashed orange; margin: 20px;">' +
|
| 317 |
+
'<h3>Plot Not Found</h3>' +
|
| 318 |
'<p>The interactive plot could not be initialized. The data may be processing.</p>' +
|
| 319 |
+
'<button onclick="location.reload()" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;">Refresh Page</button>' +
|
| 320 |
'</div>';
|
| 321 |
return;
|
| 322 |
}
|
|
|
|
| 333 |
// Add loading indicator for downloads
|
| 334 |
const downloadButtons = document.querySelectorAll('.btn-download');
|
| 335 |
downloadButtons.forEach(button => {
|
| 336 |
+
button.addEventListener('click', function() {
|
| 337 |
const originalText = this.textContent;
|
| 338 |
+
this.textContent = 'Preparing download...';
|
| 339 |
this.style.pointerEvents = 'none';
|
| 340 |
+
|
| 341 |
setTimeout(() => {
|
| 342 |
this.textContent = originalText;
|
| 343 |
this.style.pointerEvents = 'auto';
|
templates/plot.html
CHANGED
|
@@ -222,7 +222,7 @@
|
|
| 222 |
</head>
|
| 223 |
<body>
|
| 224 |
<div class="container">
|
| 225 |
-
<h1
|
| 226 |
|
| 227 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 228 |
{% if messages %}
|
|
@@ -233,7 +233,7 @@
|
|
| 233 |
{% endwith %}
|
| 234 |
|
| 235 |
<div class="breadcrumb">
|
| 236 |
-
<a href="{{ url_for('index') }}"
|
| 237 |
<a href="javascript:history.back()">Variable Selection</a> →
|
| 238 |
Visualization
|
| 239 |
</div>
|
|
@@ -247,10 +247,10 @@
|
|
| 247 |
|
| 248 |
<div class="plot-controls">
|
| 249 |
<button onclick="downloadPlot()" class="btn btn-download">
|
| 250 |
-
|
| 251 |
</button>
|
| 252 |
<button onclick="toggleFullscreen()" class="btn btn-secondary">
|
| 253 |
-
|
| 254 |
</button>
|
| 255 |
</div>
|
| 256 |
</div>
|
|
@@ -258,7 +258,7 @@
|
|
| 258 |
|
| 259 |
<div class="two-column">
|
| 260 |
<div class="info-section">
|
| 261 |
-
<h2
|
| 262 |
<div class="info-grid">
|
| 263 |
<div class="info-item">
|
| 264 |
<strong>Variable:</strong>
|
|
@@ -290,7 +290,7 @@
|
|
| 290 |
</div>
|
| 291 |
|
| 292 |
<div class="info-section">
|
| 293 |
-
<h2
|
| 294 |
<div class="stats-grid">
|
| 295 |
<div class="stat-item">
|
| 296 |
<div class="stat-value">{{ "%.3f"|format(plot_info.data_range.min) }}</div>
|
|
@@ -313,23 +313,23 @@
|
|
| 313 |
</div>
|
| 314 |
|
| 315 |
<div class="action-section">
|
| 316 |
-
<h2
|
| 317 |
<div class="button-group">
|
| 318 |
<a href="javascript:history.back()" class="btn btn-secondary">
|
| 319 |
← Create Another Visualization
|
| 320 |
</a>
|
| 321 |
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
| 322 |
-
|
| 323 |
</a>
|
| 324 |
<button onclick="sharePlot()" class="btn">
|
| 325 |
-
|
| 326 |
</button>
|
| 327 |
</div>
|
| 328 |
</div>
|
| 329 |
|
| 330 |
<div class="technical-section">
|
| 331 |
<details>
|
| 332 |
-
<summary><h3
|
| 333 |
<div class="technical-content">
|
| 334 |
<div class="technical-grid">
|
| 335 |
<div>
|
|
|
|
| 222 |
</head>
|
| 223 |
<body>
|
| 224 |
<div class="container">
|
| 225 |
+
<h1>{{ plot_info.variable }} Visualization</h1>
|
| 226 |
|
| 227 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 228 |
{% if messages %}
|
|
|
|
| 233 |
{% endwith %}
|
| 234 |
|
| 235 |
<div class="breadcrumb">
|
| 236 |
+
<a href="{{ url_for('index') }}">Home</a> →
|
| 237 |
<a href="javascript:history.back()">Variable Selection</a> →
|
| 238 |
Visualization
|
| 239 |
</div>
|
|
|
|
| 247 |
|
| 248 |
<div class="plot-controls">
|
| 249 |
<button onclick="downloadPlot()" class="btn btn-download">
|
| 250 |
+
Download Image
|
| 251 |
</button>
|
| 252 |
<button onclick="toggleFullscreen()" class="btn btn-secondary">
|
| 253 |
+
View Fullscreen
|
| 254 |
</button>
|
| 255 |
</div>
|
| 256 |
</div>
|
|
|
|
| 258 |
|
| 259 |
<div class="two-column">
|
| 260 |
<div class="info-section">
|
| 261 |
+
<h2>Variable Information</h2>
|
| 262 |
<div class="info-grid">
|
| 263 |
<div class="info-item">
|
| 264 |
<strong>Variable:</strong>
|
|
|
|
| 290 |
</div>
|
| 291 |
|
| 292 |
<div class="info-section">
|
| 293 |
+
<h2>Data Statistics</h2>
|
| 294 |
<div class="stats-grid">
|
| 295 |
<div class="stat-item">
|
| 296 |
<div class="stat-value">{{ "%.3f"|format(plot_info.data_range.min) }}</div>
|
|
|
|
| 313 |
</div>
|
| 314 |
|
| 315 |
<div class="action-section">
|
| 316 |
+
<h2>Actions</h2>
|
| 317 |
<div class="button-group">
|
| 318 |
<a href="javascript:history.back()" class="btn btn-secondary">
|
| 319 |
← Create Another Visualization
|
| 320 |
</a>
|
| 321 |
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
| 322 |
+
Back to Home
|
| 323 |
</a>
|
| 324 |
<button onclick="sharePlot()" class="btn">
|
| 325 |
+
Share Plot
|
| 326 |
</button>
|
| 327 |
</div>
|
| 328 |
</div>
|
| 329 |
|
| 330 |
<div class="technical-section">
|
| 331 |
<details>
|
| 332 |
+
<summary><h3>Technical Details</h3></summary>
|
| 333 |
<div class="technical-content">
|
| 334 |
<div class="technical-grid">
|
| 335 |
<div>
|
templates/prediction_runs.html
CHANGED
|
@@ -237,7 +237,7 @@
|
|
| 237 |
<body>
|
| 238 |
<div class="container">
|
| 239 |
<div class="header">
|
| 240 |
-
<h1
|
| 241 |
<p>Browse and manage your atmospheric prediction runs</p>
|
| 242 |
</div>
|
| 243 |
|
|
@@ -269,30 +269,26 @@
|
|
| 269 |
{% for run in runs %}
|
| 270 |
<div class="run-card">
|
| 271 |
<div class="run-header">
|
| 272 |
-
<div class="run-title"
|
| 273 |
<div class="run-status {{ 'status-available' if run.available else 'status-unavailable' }}">
|
| 274 |
-
{{ '
|
| 275 |
</div>
|
| 276 |
</div>
|
| 277 |
|
| 278 |
<div class="run-details">
|
| 279 |
<div class="detail-item">
|
| 280 |
-
<span class="detail-icon">🕐</span>
|
| 281 |
<span class="detail-label">Run Time:</span>
|
| 282 |
<span class="detail-value">{{ run.run_timestamp[:8] }} {{ run.run_timestamp[9:].replace('_', ':') }}</span>
|
| 283 |
</div>
|
| 284 |
<div class="detail-item">
|
| 285 |
-
<span class="detail-icon">📊</span>
|
| 286 |
<span class="detail-label">Steps:</span>
|
| 287 |
<span class="detail-value">{{ run.steps }} steps</span>
|
| 288 |
</div>
|
| 289 |
<div class="detail-item">
|
| 290 |
-
<span class="detail-icon">⏱️</span>
|
| 291 |
<span class="detail-label">Coverage:</span>
|
| 292 |
<span class="detail-value">{{ run.time_coverage_hours }}h forward</span>
|
| 293 |
</div>
|
| 294 |
<div class="detail-item">
|
| 295 |
-
<span class="detail-icon">📥</span>
|
| 296 |
<span class="detail-label">Input Times:</span>
|
| 297 |
<span class="detail-value">{{ run.input_times|join(", ") }}</span>
|
| 298 |
</div>
|
|
@@ -302,20 +298,20 @@
|
|
| 302 |
{% if run.available %}
|
| 303 |
<a href="{{ url_for('aurora_variables', run_dir=run.relative_path) }}"
|
| 304 |
class="btn">
|
| 305 |
-
|
| 306 |
</a>
|
| 307 |
<a href="{{ url_for('download_prediction_netcdf', filename=run.relative_path) }}"
|
| 308 |
class="btn btn-secondary">
|
| 309 |
-
|
| 310 |
</a>
|
| 311 |
{% else %}
|
| 312 |
<button class="btn" disabled>
|
| 313 |
-
|
| 314 |
</button>
|
| 315 |
{% endif %}
|
| 316 |
|
| 317 |
<a href="{{ url_for('aurora_predict') }}" class="btn btn-secondary">
|
| 318 |
-
|
| 319 |
</a>
|
| 320 |
</div>
|
| 321 |
</div>
|
|
@@ -323,11 +319,11 @@
|
|
| 323 |
|
| 324 |
{% else %}
|
| 325 |
<div class="no-runs">
|
| 326 |
-
<h3
|
| 327 |
<p>You haven't created any Aurora predictions yet.</p>
|
| 328 |
<p style="margin-top: 20px;">
|
| 329 |
<a href="{{ url_for('aurora_predict') }}" class="btn">
|
| 330 |
-
|
| 331 |
</a>
|
| 332 |
</p>
|
| 333 |
</div>
|
|
|
|
| 237 |
<body>
|
| 238 |
<div class="container">
|
| 239 |
<div class="header">
|
| 240 |
+
<h1>Aurora Prediction Runs</h1>
|
| 241 |
<p>Browse and manage your atmospheric prediction runs</p>
|
| 242 |
</div>
|
| 243 |
|
|
|
|
| 269 |
{% for run in runs %}
|
| 270 |
<div class="run-card">
|
| 271 |
<div class="run-header">
|
| 272 |
+
<div class="run-title">{{ run.date }}</div>
|
| 273 |
<div class="run-status {{ 'status-available' if run.available else 'status-unavailable' }}">
|
| 274 |
+
{{ 'Available' if run.available else 'Missing' }}
|
| 275 |
</div>
|
| 276 |
</div>
|
| 277 |
|
| 278 |
<div class="run-details">
|
| 279 |
<div class="detail-item">
|
|
|
|
| 280 |
<span class="detail-label">Run Time:</span>
|
| 281 |
<span class="detail-value">{{ run.run_timestamp[:8] }} {{ run.run_timestamp[9:].replace('_', ':') }}</span>
|
| 282 |
</div>
|
| 283 |
<div class="detail-item">
|
|
|
|
| 284 |
<span class="detail-label">Steps:</span>
|
| 285 |
<span class="detail-value">{{ run.steps }} steps</span>
|
| 286 |
</div>
|
| 287 |
<div class="detail-item">
|
|
|
|
| 288 |
<span class="detail-label">Coverage:</span>
|
| 289 |
<span class="detail-value">{{ run.time_coverage_hours }}h forward</span>
|
| 290 |
</div>
|
| 291 |
<div class="detail-item">
|
|
|
|
| 292 |
<span class="detail-label">Input Times:</span>
|
| 293 |
<span class="detail-value">{{ run.input_times|join(", ") }}</span>
|
| 294 |
</div>
|
|
|
|
| 298 |
{% if run.available %}
|
| 299 |
<a href="{{ url_for('aurora_variables', run_dir=run.relative_path) }}"
|
| 300 |
class="btn">
|
| 301 |
+
View Variables
|
| 302 |
</a>
|
| 303 |
<a href="{{ url_for('download_prediction_netcdf', filename=run.relative_path) }}"
|
| 304 |
class="btn btn-secondary">
|
| 305 |
+
Download Files
|
| 306 |
</a>
|
| 307 |
{% else %}
|
| 308 |
<button class="btn" disabled>
|
| 309 |
+
File Not Available
|
| 310 |
</button>
|
| 311 |
{% endif %}
|
| 312 |
|
| 313 |
<a href="{{ url_for('aurora_predict') }}" class="btn btn-secondary">
|
| 314 |
+
Create New Run
|
| 315 |
</a>
|
| 316 |
</div>
|
| 317 |
</div>
|
|
|
|
| 319 |
|
| 320 |
{% else %}
|
| 321 |
<div class="no-runs">
|
| 322 |
+
<h3>No Aurora Prediction Runs Found</h3>
|
| 323 |
<p>You haven't created any Aurora predictions yet.</p>
|
| 324 |
<p style="margin-top: 20px;">
|
| 325 |
<a href="{{ url_for('aurora_predict') }}" class="btn">
|
| 326 |
+
Create Your First Prediction
|
| 327 |
</a>
|
| 328 |
</p>
|
| 329 |
</div>
|
templates/variables.html
CHANGED
|
@@ -196,7 +196,7 @@
|
|
| 196 |
</head>
|
| 197 |
<body>
|
| 198 |
<div class="container">
|
| 199 |
-
<h1
|
| 200 |
|
| 201 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 202 |
{% if messages %}
|
|
@@ -207,7 +207,7 @@
|
|
| 207 |
{% endwith %}
|
| 208 |
|
| 209 |
<div class="breadcrumb">
|
| 210 |
-
<a href="{{ url_for('index') }}"
|
| 211 |
</div>
|
| 212 |
|
| 213 |
<form action="/visualize" method="post" id="visualizeForm">
|
|
@@ -215,7 +215,7 @@
|
|
| 215 |
<input type="hidden" name="is_download" value="{{ is_download }}">
|
| 216 |
|
| 217 |
<div class="form-section">
|
| 218 |
-
<h2
|
| 219 |
<p>Found {{ variables|length }} air pollution variable(s) in your data. Select one below:</p>
|
| 220 |
|
| 221 |
<div class="form-group">
|
|
@@ -241,7 +241,7 @@
|
|
| 241 |
</div>
|
| 242 |
|
| 243 |
<div class="pressure-section" id="pressureSection" style="display: none;">
|
| 244 |
-
<h3
|
| 245 |
<p>This is an atmospheric variable. Please select a pressure level:</p>
|
| 246 |
|
| 247 |
<div class="form-group">
|
|
@@ -263,14 +263,14 @@
|
|
| 263 |
<option value="1000">1000 hPa (Surface Level)</option>
|
| 264 |
</select>
|
| 265 |
<div class="info-text">
|
| 266 |
-
|
| 267 |
</div>
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<!-- Time Selection -->
|
| 272 |
<div class="form-section" id="timeSection" style="display: none;">
|
| 273 |
-
<h3
|
| 274 |
<p>Multiple time steps available. Please select a time:</p>
|
| 275 |
|
| 276 |
<div class="form-group">
|
|
@@ -279,13 +279,13 @@
|
|
| 279 |
<option value="">-- Loading available times --</option>
|
| 280 |
</select>
|
| 281 |
<div class="info-text">
|
| 282 |
-
|
| 283 |
</div>
|
| 284 |
</div>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<div class="form-section">
|
| 288 |
-
<h2
|
| 289 |
<div class="form-group">
|
| 290 |
<label for="color_theme">Select Color Scheme:</label>
|
| 291 |
<select name="color_theme" id="color_theme" onchange="updateColorPreview()">
|
|
@@ -311,10 +311,10 @@
|
|
| 311 |
</label>
|
| 312 |
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
| 313 |
<button type="submit" formaction="{{ url_for('visualize') }}" class="btn">
|
| 314 |
-
|
| 315 |
</button>
|
| 316 |
<button type="submit" formaction="{{ url_for('visualize_interactive') }}" class="btn btn-interactive">
|
| 317 |
-
|
| 318 |
</button>
|
| 319 |
</div>
|
| 320 |
<p style="margin-top: 10px; font-size: 14px; color: #7f8c8d;">
|
|
@@ -324,7 +324,7 @@
|
|
| 324 |
</div>
|
| 325 |
|
| 326 |
<div class="loading-message" id="loadingMessage" style="display: none;">
|
| 327 |
-
<p
|
| 328 |
<div class="spinner"></div>
|
| 329 |
</div>
|
| 330 |
</div>
|
|
@@ -512,13 +512,13 @@
|
|
| 512 |
|
| 513 |
// Determine which button was clicked
|
| 514 |
const submitEvent = e.submitter;
|
| 515 |
-
let loadingText = '
|
| 516 |
|
| 517 |
if (submitEvent && submitEvent.formAction) {
|
| 518 |
if (submitEvent.formAction.includes('visualize_interactive')) {
|
| 519 |
-
loadingText = '
|
| 520 |
} else {
|
| 521 |
-
loadingText = '
|
| 522 |
}
|
| 523 |
}
|
| 524 |
|
|
|
|
| 196 |
</head>
|
| 197 |
<body>
|
| 198 |
<div class="container">
|
| 199 |
+
<h1>Variable Selection</h1>
|
| 200 |
|
| 201 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 202 |
{% if messages %}
|
|
|
|
| 207 |
{% endwith %}
|
| 208 |
|
| 209 |
<div class="breadcrumb">
|
| 210 |
+
<a href="{{ url_for('index') }}">Home</a> → Variable Selection
|
| 211 |
</div>
|
| 212 |
|
| 213 |
<form action="/visualize" method="post" id="visualizeForm">
|
|
|
|
| 215 |
<input type="hidden" name="is_download" value="{{ is_download }}">
|
| 216 |
|
| 217 |
<div class="form-section">
|
| 218 |
+
<h2>Select Air Pollution Variable</h2>
|
| 219 |
<p>Found {{ variables|length }} air pollution variable(s) in your data. Select one below:</p>
|
| 220 |
|
| 221 |
<div class="form-group">
|
|
|
|
| 241 |
</div>
|
| 242 |
|
| 243 |
<div class="pressure-section" id="pressureSection" style="display: none;">
|
| 244 |
+
<h3>Pressure Level Selection</h3>
|
| 245 |
<p>This is an atmospheric variable. Please select a pressure level:</p>
|
| 246 |
|
| 247 |
<div class="form-group">
|
|
|
|
| 263 |
<option value="1000">1000 hPa (Surface Level)</option>
|
| 264 |
</select>
|
| 265 |
<div class="info-text">
|
| 266 |
+
<strong>Tip:</strong> 850 hPa is commonly used for atmospheric analysis (pre-selected)
|
| 267 |
</div>
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<!-- Time Selection -->
|
| 272 |
<div class="form-section" id="timeSection" style="display: none;">
|
| 273 |
+
<h3>Time Selection</h3>
|
| 274 |
<p>Multiple time steps available. Please select a time:</p>
|
| 275 |
|
| 276 |
<div class="form-group">
|
|
|
|
| 279 |
<option value="">-- Loading available times --</option>
|
| 280 |
</select>
|
| 281 |
<div class="info-text">
|
| 282 |
+
<strong>Tip:</strong> Latest time is usually pre-selected
|
| 283 |
</div>
|
| 284 |
</div>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<div class="form-section">
|
| 288 |
+
<h2>Color Theme</h2>
|
| 289 |
<div class="form-group">
|
| 290 |
<label for="color_theme">Select Color Scheme:</label>
|
| 291 |
<select name="color_theme" id="color_theme" onchange="updateColorPreview()">
|
|
|
|
| 311 |
</label>
|
| 312 |
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
| 313 |
<button type="submit" formaction="{{ url_for('visualize') }}" class="btn">
|
| 314 |
+
Generate Static Plot (PNG)
|
| 315 |
</button>
|
| 316 |
<button type="submit" formaction="{{ url_for('visualize_interactive') }}" class="btn btn-interactive">
|
| 317 |
+
Generate Interactive Plot (with hover info)
|
| 318 |
</button>
|
| 319 |
</div>
|
| 320 |
<p style="margin-top: 10px; font-size: 14px; color: #7f8c8d;">
|
|
|
|
| 324 |
</div>
|
| 325 |
|
| 326 |
<div class="loading-message" id="loadingMessage" style="display: none;">
|
| 327 |
+
<p>Generating map visualization... This may take a moment.</p>
|
| 328 |
<div class="spinner"></div>
|
| 329 |
</div>
|
| 330 |
</div>
|
|
|
|
| 512 |
|
| 513 |
// Determine which button was clicked
|
| 514 |
const submitEvent = e.submitter;
|
| 515 |
+
let loadingText = 'Generating...';
|
| 516 |
|
| 517 |
if (submitEvent && submitEvent.formAction) {
|
| 518 |
if (submitEvent.formAction.includes('visualize_interactive')) {
|
| 519 |
+
loadingText = 'Creating Interactive Plot...';
|
| 520 |
} else {
|
| 521 |
+
loadingText = 'Creating Static Plot...';
|
| 522 |
}
|
| 523 |
}
|
| 524 |
|
templates/view_interactive.html
CHANGED
|
@@ -137,7 +137,7 @@
|
|
| 137 |
<body>
|
| 138 |
<div class="container">
|
| 139 |
<div class="header">
|
| 140 |
-
<h1
|
| 141 |
<p style="color: #7f8c8d; margin-top: 10px;">
|
| 142 |
Viewing saved interactive plot: {{ plot_info.filename }}
|
| 143 |
</p>
|
|
@@ -145,9 +145,9 @@
|
|
| 145 |
|
| 146 |
<div class="controls">
|
| 147 |
<a href="{{ url_for('gallery') }}" class="btn btn-secondary">← Back to Gallery</a>
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
</div>
|
| 152 |
|
| 153 |
<div class="plot-container">
|
|
@@ -157,38 +157,38 @@
|
|
| 157 |
</div>
|
| 158 |
|
| 159 |
<div class="plot-info-section">
|
| 160 |
-
<h2 style="color: #2c3e50; margin-bottom: 20px;"
|
| 161 |
|
| 162 |
<div class="info-grid">
|
| 163 |
<div class="info-item">
|
| 164 |
-
<h3
|
| 165 |
<p>{{ plot_info.variable }}</p>
|
| 166 |
</div>
|
| 167 |
|
| 168 |
{% if plot_info.pressure_level %}
|
| 169 |
<div class="info-item">
|
| 170 |
-
<h3
|
| 171 |
<p>{{ plot_info.pressure_level }}</p>
|
| 172 |
</div>
|
| 173 |
{% endif %}
|
| 174 |
|
| 175 |
<div class="info-item">
|
| 176 |
-
<h3
|
| 177 |
<p>{{ plot_info.theme }}</p>
|
| 178 |
</div>
|
| 179 |
|
| 180 |
<div class="info-item">
|
| 181 |
-
<h3
|
| 182 |
<p>{{ plot_info.generated_time }}</p>
|
| 183 |
</div>
|
| 184 |
|
| 185 |
<div class="info-item">
|
| 186 |
-
<h3
|
| 187 |
<p>{{ plot_info.plot_type }} Plot</p>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
<div class="info-item" style="grid-column: 1 / -1;">
|
| 191 |
-
<h3
|
| 192 |
<p style="font-family: monospace; background: #f8f9fa; padding: 8px; border-radius: 4px; word-break: break-all;">{{ plot_info.filename }}</p>
|
| 193 |
</div>
|
| 194 |
</div>
|
|
|
|
| 137 |
<body>
|
| 138 |
<div class="container">
|
| 139 |
<div class="header">
|
| 140 |
+
<h1>Interactive Plot Viewer</h1>
|
| 141 |
<p style="color: #7f8c8d; margin-top: 10px;">
|
| 142 |
Viewing saved interactive plot: {{ plot_info.filename }}
|
| 143 |
</p>
|
|
|
|
| 145 |
|
| 146 |
<div class="controls">
|
| 147 |
<a href="{{ url_for('gallery') }}" class="btn btn-secondary">← Back to Gallery</a>
|
| 148 |
+
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
|
| 149 |
+
<a href="{{ url_for('serve_plot', filename=plot_info.filename) }}"
|
| 150 |
+
class="btn btn-download" download>Download HTML</a>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
<div class="plot-container">
|
|
|
|
| 157 |
</div>
|
| 158 |
|
| 159 |
<div class="plot-info-section">
|
| 160 |
+
<h2 style="color: #2c3e50; margin-bottom: 20px;">Plot Information</h2>
|
| 161 |
|
| 162 |
<div class="info-grid">
|
| 163 |
<div class="info-item">
|
| 164 |
+
<h3>Variable</h3>
|
| 165 |
<p>{{ plot_info.variable }}</p>
|
| 166 |
</div>
|
| 167 |
|
| 168 |
{% if plot_info.pressure_level %}
|
| 169 |
<div class="info-item">
|
| 170 |
+
<h3>Pressure Level</h3>
|
| 171 |
<p>{{ plot_info.pressure_level }}</p>
|
| 172 |
</div>
|
| 173 |
{% endif %}
|
| 174 |
|
| 175 |
<div class="info-item">
|
| 176 |
+
<h3>Color Theme</h3>
|
| 177 |
<p>{{ plot_info.theme }}</p>
|
| 178 |
</div>
|
| 179 |
|
| 180 |
<div class="info-item">
|
| 181 |
+
<h3>Generated</h3>
|
| 182 |
<p>{{ plot_info.generated_time }}</p>
|
| 183 |
</div>
|
| 184 |
|
| 185 |
<div class="info-item">
|
| 186 |
+
<h3>Type</h3>
|
| 187 |
<p>{{ plot_info.plot_type }} Plot</p>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
<div class="info-item" style="grid-column: 1 / -1;">
|
| 191 |
+
<h3>Filename</h3>
|
| 192 |
<p style="font-family: monospace; background: #f8f9fa; padding: 8px; border-radius: 4px; word-break: break-all;">{{ plot_info.filename }}</p>
|
| 193 |
</div>
|
| 194 |
</div>
|