aditya-me13 commited on
Commit
808378f
·
1 Parent(s): 4f0125c
cams_downloader.py CHANGED
@@ -1,15 +1,18 @@
1
- # cams_downloader.py
2
- # Download CAMS atmospheric composition data
3
-
4
  import cdsapi
5
  import zipfile
6
- import os
 
 
7
  from pathlib import Path
8
  from datetime import datetime, timedelta
9
- import pandas as pd
 
 
 
10
 
11
  class CAMSDownloader:
12
- def __init__(self, download_dir="downloads"):
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
- print("✅ CDS API client initialized from environment variables")
39
  return
40
 
41
- # Fallback: Try to read .cdsapirc file from current directory first, then home directory
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
- print("✅ CDS API client initialized from .cdsapirc file")
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
- print("✅ CDS API client initialized with default settings")
70
 
71
  except Exception as e:
72
- print(f"⚠️ Warning: Could not initialize CDS API client: {str(e)}")
73
- print("Please ensure you have:")
74
- print("1. Created an account at https://cds.climate.copernicus.eu/")
75
- print("2. Set CDSAPI_URL and CDSAPI_KEY environment variables (recommended for cloud deployments)")
76
- print("3. Or created a .cdsapirc file in your home directory with your credentials")
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
- print(f"✅ Data for {date_str} already exists: {filename}")
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
- print(f"🔄 Downloading CAMS data for {date_str}...")
155
- print(f"Variables: {len(variables)} selected")
156
- print(f"Pressure levels: {len(pressure_levels)} levels")
157
 
158
  try:
159
- # Make the API request
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"], # Two time steps
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
- print(f"📁 Downloaded file size: {file_size / 1024 / 1024:.2f} MB")
179
 
180
- # Basic validation - CAMS files should be reasonably large
181
- if file_size < 10000: # Less than 10KB is suspicious
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
- print(f"✅ Successfully downloaded: {filename}")
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
- print(f"🗑️ Cleaning up failed download: {filepath}")
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: # Less than 1KB is probably an error response
221
- print(f"⚠️ Downloaded file is too small ({file_size} bytes), likely an error response")
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
- print(f"❌ File is not a valid ZIP file: {zip_path}")
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
- print(f"File contents preview: {first_lines[:100]}...")
235
  raise Exception(f"Downloaded file is not a valid ZIP archive. File size: {file_size} bytes")
236
 
237
  except Exception as e:
238
- if "ZIP" in str(e) or "zip" in str(e):
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
- # Extract surface data
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
- print(f"✅ Extracted surface data: {surface_path.name}")
266
  extracted_files['surface'] = str(surface_path)
267
  elif surface_path.exists():
268
  extracted_files['surface'] = str(surface_path)
269
 
270
- # Extract atmospheric data
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
- print(f"✅ Extracted atmospheric data: {atmospheric_path.name}")
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=30):
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
- print(f"🧹 Cleaned up {deleted_count} old files")
366
  return deleted_count
367
 
368
  except Exception as e:
369
- print(f"Error during cleanup: {str(e)}")
370
  return 0
371
 
372
 
373
  def test_cams_downloader():
374
  """Test function for CAMS downloader"""
375
- print("Testing CAMS downloader...")
376
-
377
  downloader = CAMSDownloader()
378
 
379
  if not downloader.is_client_ready():
380
- print("❌ CDS API client not ready. Please check your credentials.")
381
  return False
382
 
383
- # Test with recent date
384
  test_date = (datetime.now() - timedelta(days=600)).strftime('%Y-%m-%d')
385
-
386
- print(f"Testing download for date: {test_date}")
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
- print(f"✅ Download successful: {zip_path}")
393
 
394
- # Test extraction
395
  extracted_files = downloader.extract_cams_files(zip_path)
396
- print(f"✅ Extraction successful: {len(extracted_files)} files")
397
 
398
- # List downloaded files
399
  downloaded = downloader.list_downloaded_files()
400
- print(f"✅ Found {len(downloaded)} downloaded files")
401
 
402
  return True
403
 
404
  except Exception as e:
405
- print(f"❌ Test failed: {str(e)}")
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, try to load the first .nc file
88
  if not surface_file and not atmospheric_file:
89
- nc_files = [f for f in zip_contents if f.endswith('.nc')]
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>🔮 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,7 +303,7 @@
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,7 +313,7 @@
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,7 +327,7 @@
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,14 +341,14 @@
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,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;">📊 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>
 
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>🔮 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,7 +251,7 @@
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,7 +262,7 @@
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,7 +273,7 @@
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,7 +286,7 @@
286
  <div class="form-group">
287
  <label>&nbsp;</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,7 +302,7 @@
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,7 +310,7 @@
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,7 +318,7 @@
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,15 +327,15 @@
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,7 +370,7 @@
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,7 +384,7 @@
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>
 
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>&nbsp;</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>🔮 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,8 +165,8 @@
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,7 +182,7 @@
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,7 +192,7 @@
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,12 +212,12 @@
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,9 +227,9 @@
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>
 
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>📊 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,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
- 🧹 Clean Old Plots
259
  </a>
260
  </div>
261
 
@@ -279,29 +279,28 @@
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
- 🌍
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>📅 {{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
294
- <span>🎨 {{ plot.theme|title }}</span>
295
- <span>📁 {{ "%.1f"|format(plot.size/1024) }} KB</span>
296
- <span>🌐 Interactive</span>
297
  </div>
298
  <div class="plot-actions">
299
- <a href="{{ url_for('view_interactive_plot', filename=plot.filename) }}"
300
- class="btn-small btn-view">👁️ View</a>
301
- <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
302
- class="btn-small btn-download" download>💾 Download</a>
303
- <button onclick="deletePlot('{{ plot.filename }}')"
304
- class="btn-small btn-delete">🗑️ Delete</button>
305
  </div>
306
  </div>
307
  </div>
@@ -317,7 +316,7 @@
317
 
318
  <!-- Static Plots Section -->
319
  <div class="section">
320
- <h2>📊 Static Plots</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;\'>📷<br>Preview unavailable</div>'">
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>📅 {{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
334
- <span>🎨 {{ plot.theme|title }}</span>
335
- <span>📁 {{ "%.1f"|format(plot.size/1024) }} KB</span>
336
- <span>🖼️ {{ plot.extension.upper() }}</span>
337
  </div>
338
  <div class="plot-actions">
339
- <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
340
- class="btn-small btn-view" target="_blank">👁️ View</a>
341
- <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
342
- class="btn-small btn-download" download>💾 Download</a>
343
- <button onclick="deletePlot('{{ plot.filename }}')"
344
- class="btn-small btn-delete">🗑️ Delete</button>
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 = 'Loading...';
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 = '📥 Downloading...';
380
-
381
  setTimeout(() => {
382
  this.textContent = originalText;
383
  }, 2000);
@@ -403,7 +402,7 @@
403
  }
404
 
405
  const originalText = button.textContent;
406
- button.textContent = '🗑️ Deleting...';
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 = `✅ ${filename} deleted successfully`;
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>🌍 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,9 +172,9 @@
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,7 +185,7 @@
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,12 +197,12 @@
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,7 +213,7 @@
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,7 +242,7 @@
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,11 +259,11 @@
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,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
- 🔮 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,7 +294,7 @@
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,13 +303,13 @@
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,7 +337,7 @@
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,7 +352,7 @@
352
  }
353
  const submitBtn = this.querySelector('button[type="submit"]');
354
  if (submitBtn) {
355
- addLoadingState(submitBtn, '📥 Download CAMS Data');
356
  }
357
  });
358
  }
@@ -362,7 +362,7 @@
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,7 +388,7 @@
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('|');
 
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>🌍 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,57 +234,57 @@
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,11 +297,11 @@
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,13 +310,13 @@
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,11 +333,11 @@
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';
 
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>🗺️ {{ plot_info.variable }} Visualization</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') }}">🏠 Home</a> →
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
- 💾 Download Image
251
  </button>
252
  <button onclick="toggleFullscreen()" class="btn btn-secondary">
253
- 🔍 View Fullscreen
254
  </button>
255
  </div>
256
  </div>
@@ -258,7 +258,7 @@
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,7 +290,7 @@
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,23 +313,23 @@
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>
 
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>🔮 Aurora Prediction Runs</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">📅 {{ 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-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
- 📊 View Variables
306
  </a>
307
  <a href="{{ url_for('download_prediction_netcdf', filename=run.relative_path) }}"
308
  class="btn btn-secondary">
309
- 💾 Download Files
310
  </a>
311
  {% else %}
312
  <button class="btn" disabled>
313
- File Not Available
314
  </button>
315
  {% endif %}
316
 
317
  <a href="{{ url_for('aurora_predict') }}" class="btn btn-secondary">
318
- 🔄 Create New Run
319
  </a>
320
  </div>
321
  </div>
@@ -323,11 +319,11 @@
323
 
324
  {% else %}
325
  <div class="no-runs">
326
- <h3>🔮 No Aurora Prediction Runs Found</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
- 🚀 Create Your First Prediction
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>🔬 Variable Selection</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') }}">🏠 Home</a> → Variable Selection
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>📊 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,7 +241,7 @@
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,14 +263,14 @@
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,13 +279,13 @@
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,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
- 📊 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,7 +324,7 @@
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,13 +512,13 @@
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
 
 
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>🌍 Interactive Plot Viewer</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
- <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,38 +157,38 @@
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>
 
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>