digital-twin-backend / aqi_map.py
abrez ‎
Added policy simulation
a8da582
import folium
import os
import time
import random
import pandas as pd
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# Delhi center coordinates
DELHI_LAT = 28.7041
DELHI_LON = 77.1025
# Base hotspots data with AQI values and weight ratios
# Weight = how much this location deviates from average (>1 = higher, <1 = lower)
BASE_HOTSPOTS = [
{"name": "Chandni Chowk", "lat": 28.656, "lon": 77.231, "base_aqi": 384, "weight": 1.2092},
{"name": "Nehru Nagar", "lat": 28.5697, "lon": 77.253, "base_aqi": 384, "weight": 1.2092},
{"name": "New Moti Bagh", "lat": 28.582, "lon": 77.1717, "base_aqi": 377, "weight": 1.1872},
{"name": "Shadipur", "lat": 28.6517, "lon": 77.1582, "base_aqi": 375, "weight": 1.1809},
{"name": "Wazirpur", "lat": 28.6967, "lon": 77.1658, "base_aqi": 354, "weight": 1.1148},
{"name": "Mundka", "lat": 28.6794, "lon": 77.0284, "base_aqi": 353, "weight": 1.1116},
{"name": "Punjabi Bagh", "lat": 28.673, "lon": 77.1374, "base_aqi": 349, "weight": 1.0990},
{"name": "RK Puram", "lat": 28.5505, "lon": 77.1849, "base_aqi": 342, "weight": 1.0770},
{"name": "Jahangirpuri", "lat": 28.716, "lon": 77.1829, "base_aqi": 338, "weight": 1.0644},
{"name": "Okhla Phase-2", "lat": 28.5365, "lon": 77.2803, "base_aqi": 337, "weight": 1.0612},
{"name": "Dwarka Sector-8", "lat": 28.5656, "lon": 77.067, "base_aqi": 336, "weight": 1.0581},
{"name": "Dhyanchand Stadium", "lat": 28.6125, "lon": 77.2372, "base_aqi": 335, "weight": 1.0549},
{"name": "Patparganj", "lat": 28.612, "lon": 77.292, "base_aqi": 334, "weight": 1.0518},
{"name": "Bawana", "lat": 28.8, "lon": 77.03, "base_aqi": 328, "weight": 1.0329},
{"name": "Sirifort", "lat": 28.5521, "lon": 77.2193, "base_aqi": 326, "weight": 1.0266},
{"name": "ITO", "lat": 28.6294, "lon": 77.241, "base_aqi": 324, "weight": 1.0203},
{"name": "Karni Singh Shooting Range", "lat": 28.4998, "lon": 77.2668, "base_aqi": 323, "weight": 1.0171},
{"name": "Sonia Vihar", "lat": 28.7074, "lon": 77.2599, "base_aqi": 317, "weight": 0.9982},
{"name": "JLN Stadium", "lat": 28.5828, "lon": 77.2344, "base_aqi": 314, "weight": 0.9888},
{"name": "Rohini", "lat": 28.7019, "lon": 77.0984, "base_aqi": 312, "weight": 0.9825},
{"name": "Narela", "lat": 28.85, "lon": 77.1, "base_aqi": 311, "weight": 0.9793},
{"name": "Mandir Marg", "lat": 28.6325, "lon": 77.1994, "base_aqi": 311, "weight": 0.9793},
{"name": "IGI Airport T3", "lat": 28.5562, "lon": 77.1, "base_aqi": 305, "weight": 0.9605},
{"name": "Vivek Vihar", "lat": 28.6635, "lon": 77.3152, "base_aqi": 303, "weight": 0.9542},
{"name": "Pusa IMD", "lat": 28.6335, "lon": 77.1651, "base_aqi": 298, "weight": 0.9384},
{"name": "Burari Crossing", "lat": 28.7592, "lon": 77.1938, "base_aqi": 294, "weight": 0.9258},
{"name": "Aurobindo Marg", "lat": 28.545, "lon": 77.205, "base_aqi": 287, "weight": 0.9038},
{"name": "Dilshad Garden (IHBAS)", "lat": 28.681, "lon": 77.305, "base_aqi": 283, "weight": 0.8912},
{"name": "Lodhi Road", "lat": 28.5921, "lon": 77.2284, "base_aqi": 282, "weight": 0.8880},
{"name": "NSIT-Dwarka", "lat": 28.6081, "lon": 77.0193, "base_aqi": 275, "weight": 0.8660},
{"name": "Alipur", "lat": 28.8, "lon": 77.15, "base_aqi": 273, "weight": 0.8597},
{"name": "Najafgarh", "lat": 28.6125, "lon": 76.983, "base_aqi": 262, "weight": 0.8250},
{"name": "CRRI-Mathura Road", "lat": 28.5518, "lon": 77.2752, "base_aqi": 252, "weight": 0.7936},
{"name": "DTU", "lat": 28.7501, "lon": 77.1177, "base_aqi": 219, "weight": 0.6896},
]
# Baseline average AQI (average of all base_aqi values)
BASELINE_AVG_AQI = sum(h['base_aqi'] for h in BASE_HOTSPOTS) / len(BASE_HOTSPOTS)
def get_emission_scaling_factor(year: int) -> float:
"""
Calculate the scaling factor for a forecast year relative to 2025 baseline.
Returns the ratio of year's avg emissions to 2025's avg emissions.
"""
try:
# Load forecast data
forecast_path = Path(__file__).parent / "emission_forecast_3years_full.csv"
if not forecast_path.exists():
print(f"Forecast file not found: {forecast_path}")
return 1.0
df = pd.read_csv(forecast_path)
df['Date'] = pd.to_datetime(df['Date'])
df['Year'] = df['Date'].dt.year
# Get baseline 2025 average
baseline_2025 = df[df['Year'] == 2025]['Total_Emission'].mean()
# Get selected year average
year_avg = df[df['Year'] == year]['Total_Emission'].mean()
if pd.isna(baseline_2025) or pd.isna(year_avg) or baseline_2025 == 0:
print(f"Could not calculate scaling factor for year {year}")
return 1.0
scaling_factor = year_avg / baseline_2025
print(f"Year {year}: avg emission = {year_avg:.2f}, baseline = {baseline_2025:.2f}, scaling = {scaling_factor:.4f}")
return scaling_factor
except Exception as e:
print(f"Error calculating emission scaling factor: {e}")
return 1.0
def add_delhi_boundary_to_map(m):
"""Helper to fetch and add Delhi boundary to a map."""
import requests
try:
geojson_url = "https://raw.githubusercontent.com/datameet/Municipal_Spatial_Data/master/Delhi/Delhi_Boundary.geojson"
# Add Boundary Outline
folium.GeoJson(
geojson_url,
name="Delhi Boundary",
style_function=lambda x: {
'fillColor': 'none',
'color': 'black', # Black for visibility on light tiles
'weight': 3,
'dashArray': '5, 5',
'opacity': 0.6
}
).add_to(m)
return True
except Exception as e:
print(f"Error adding boundary: {e}")
return False
def generate_heatmap_html():
"""Generates a Folium map with Grid Heatmap (Clipped to Delhi)."""
# Create base map
m = folium.Map(
location=[DELHI_LAT, DELHI_LON],
zoom_start=10,
control_scale=True
)
# Heatmap Grid Generation
import requests
from shapely.geometry import shape, Point
from shapely.ops import unary_union
# Fetch Delhi Boundary for layout clipping & display
# We fetch manually here for the shapely polygon, AND add the visual layer
has_boundary = False
delhi_polygon = None
# Add visual boundary
add_delhi_boundary_to_map(m)
try:
geojson_url = "https://raw.githubusercontent.com/datameet/Municipal_Spatial_Data/master/Delhi/Delhi_Boundary.geojson"
resp = requests.get(geojson_url)
data = resp.json()
# Create a unified polygon for checking
features = data.get('features', [])
shapes = [shape(f['geometry']) for f in features]
delhi_polygon = unary_union(shapes)
has_boundary = True
except Exception as e:
print(f"Error processing boundary for clipping: {e}")
# Grid config
lat_min, lat_max = 28.40, 28.88
lon_min, lon_max = 76.85, 77.35
step = 0.015 # Approx 1.5km grid size
def get_color(value):
"""Red-scale heatmap colors like the reference image."""
if value < 100: return "#fee5d9" # Very Light Pink
if value < 200: return "#fcae91" # Light Pink/Orange
if value < 300: return "#fb6a4a" # Salmon
if value < 400: return "#de2d26" # Red
return "#a50f15" # Dark Red/Brown
lat = lat_min
while lat < lat_max:
lon = lon_min
while lon < lon_max:
# Check if center of grid is inside Delhi
center_point = Point(lon + step/2, lat + step/2)
if has_boundary and not delhi_polygon.contains(center_point):
# Skip if outside
lon += step
continue
# Generate simulated AQI/CO2 data with spatial coherence
# Higher near center (Delhi)
dist_center = ((lat - DELHI_LAT)**2 + (lon - DELHI_LON)**2)**0.5
# Base value + random noise - distance decay
base_value = 450 - (dist_center * 800)
val = base_value + random.randint(-50, 50)
val = max(50, min(500, val)) # Clamp 50-500
color = get_color(val)
# Draw grid cell
folium.Rectangle(
bounds=[[lat, lon], [lat + step, lon + step]],
color=None, # No border
fill=True,
fill_color=color,
fill_opacity=0.6, # Slightly transparent
tooltip=f"Zone AQI: {int(val)}"
).add_to(m)
lon += step
lat += step
return m.get_root().render()
def generate_hotspots_html():
"""Generates the original Hotspot Map with markers."""
m = folium.Map(location=[DELHI_LAT, DELHI_LON], zoom_start=11)
# Add Visual Boundary
add_delhi_boundary_to_map(m)
# Original Hotspots Data
hotspots = [
{"name": "Chandni Chowk", "lat": 28.656, "lon": 77.231, "aqi": 384},
{"name": "Nehru Nagar", "lat": 28.5697, "lon": 77.253, "aqi": 384},
{"name": "New Moti Bagh", "lat": 28.582, "lon": 77.1717, "aqi": 377},
{"name": "Shadipur", "lat": 28.6517, "lon": 77.1582, "aqi": 375},
{"name": "Wazirpur", "lat": 28.6967, "lon": 77.1658, "aqi": 354},
{"name": "Ashok Vihar", "lat": 28.6856, "lon": 77.178, "aqi": 231},
{"name": "Mundka", "lat": 28.6794, "lon": 77.0284, "aqi": 353},
{"name": "Punjabi Bagh", "lat": 28.673, "lon": 77.1374, "aqi": 349},
{"name": "RK Puram", "lat": 28.5505, "lon": 77.1849, "aqi": 342},
{"name": "Jahangirpuri", "lat": 28.716, "lon": 77.1829, "aqi": 338},
{"name": "Okhla Phase-2", "lat": 28.5365, "lon": 77.2803, "aqi": 337},
{"name": "Dwarka Sector-8", "lat": 28.5656, "lon": 77.067, "aqi": 336},
{"name": "Patparganj", "lat": 28.612, "lon": 77.292, "aqi": 334},
{"name": "Bawana", "lat": 28.8, "lon": 77.03, "aqi": 328},
{"name": "Sonia Vihar", "lat": 28.7074, "lon": 77.2599, "aqi": 317},
{"name": "Rohini", "lat": 28.7019, "lon": 77.0984, "aqi": 312},
{"name": "Narela", "lat": 28.85, "lon": 77.1, "aqi": 311},
{"name": "Mandir Marg", "lat": 28.6325, "lon": 77.1994, "aqi": 311},
{"name": "Vivek Vihar", "lat": 28.6635, "lon": 77.3152, "aqi": 303},
{"name": "Anand Vihar", "lat": 28.6508, "lon": 77.3152, "aqi": 335},
{"name": "Dhyanchand Stadium", "lat": 28.6125, "lon": 77.2372, "aqi": 335},
{"name": "Sirifort", "lat": 28.5521, "lon": 77.2193, "aqi": 326},
{"name": "Karni Singh Shooting Range", "lat": 28.4998, "lon": 77.2668, "aqi": 323},
{"name": "ITO", "lat": 28.6294, "lon": 77.241, "aqi": 324},
{"name": "JLN Stadium", "lat": 28.5828, "lon": 77.2344, "aqi": 314},
{"name": "IGI Airport T3", "lat": 28.5562, "lon": 77.1, "aqi": 305},
{"name": "Pusa IMD", "lat": 28.6335, "lon": 77.1651, "aqi": 298},
{"name": "Burari Crossing", "lat": 28.7592, "lon": 77.1938, "aqi": 294},
{"name": "Aurobindo Marg", "lat": 28.545, "lon": 77.205, "aqi": 287},
{"name": "Dilshad Garden (IHBAS)", "lat": 28.681, "lon": 77.305, "aqi": 283},
{"name": "Lodhi Road", "lat": 28.5921, "lon": 77.2284, "aqi": 282},
{"name": "NSIT-Dwarka", "lat": 28.6081, "lon": 77.0193, "aqi": 275},
{"name": "Alipur", "lat": 28.8, "lon": 77.15, "aqi": 273},
{"name": "Najafgarh", "lat": 28.6125, "lon": 76.983, "aqi": 262},
{"name": "CRRI-Mathura Road", "lat": 28.5518, "lon": 77.2752, "aqi": 252},
{"name": "DTU", "lat": 28.7501, "lon": 77.1177, "aqi": 219},
{"name": "DU North Campus", "lat": 28.69, "lon": 77.21, "aqi": 298},
{"name": "Ayanagar", "lat": 28.4809, "lon": 77.1255, "aqi": 342}
]
for spot in hotspots:
color = "gray"
aqi = spot['aqi']
if isinstance(aqi, (int, float)):
color = "green"
if aqi > 100: color = "yellow"
if aqi > 200: color = "orange"
if aqi > 300: color = "red"
if aqi > 400: color = "purple"
folium.CircleMarker(
location=[spot['lat'], spot['lon']],
radius=15,
popup=f"{spot['name']}: AQI {spot['aqi']}",
color=color,
fill=True,
fill_color=color,
fill_opacity=0.7
).add_to(m)
folium.Marker(
location=[spot['lat'], spot['lon']],
icon=folium.DivIcon(html=f'<div style="font-weight: bold; color: white; text-shadow: 0 0 3px black;">{spot["aqi"]}</div>')
).add_to(m)
return m.get_root().render()
def generate_forecast_hotspots_html(year: int):
"""
Generates a Hotspot Map with AQI values adjusted for the forecast year.
Uses emission scaling factor to project future AQI values.
"""
m = folium.Map(location=[DELHI_LAT, DELHI_LON], zoom_start=11)
# Add Visual Boundary
add_delhi_boundary_to_map(m)
# Get scaling factor for the year
scaling_factor = get_emission_scaling_factor(year)
# Generate adjusted hotspots
for spot in BASE_HOTSPOTS:
# Apply scaling: new_aqi = base_aqi * scaling_factor
# The weight is already embedded in base_aqi, so we just scale by emission change
adjusted_aqi = int(spot['base_aqi'] * scaling_factor)
# Determine color based on AQI
color = "gray"
if isinstance(adjusted_aqi, (int, float)):
color = "green"
if adjusted_aqi > 100: color = "yellow"
if adjusted_aqi > 200: color = "orange"
if adjusted_aqi > 300: color = "red"
if adjusted_aqi > 400: color = "purple"
folium.CircleMarker(
location=[spot['lat'], spot['lon']],
radius=15,
popup=f"{spot['name']}: AQI {adjusted_aqi} ({year} Forecast)",
color=color,
fill=True,
fill_color=color,
fill_opacity=0.7
).add_to(m)
folium.Marker(
location=[spot['lat'], spot['lon']],
icon=folium.DivIcon(html=f'<div style="font-weight: bold; color: white; text-shadow: 0 0 3px black;">{adjusted_aqi}</div>')
).add_to(m)
return m.get_root().render()
def render_map_to_png(html_content, output_path):
"""
Renders the HTML content to a PNG image using Selenium headless Chrome.
"""
import uuid
unique_id = uuid.uuid4()
tmp_html_path = os.path.abspath(f"temp_map_{unique_id}.html")
# Write HTML to temp file
with open(tmp_html_path, "w", encoding="utf-8") as f:
f.write(html_content)
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=800,600")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
driver = None
try:
# Auto-install chromedriver
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)
# Load local HTML file
driver.get(f"file:///{tmp_html_path}")
time.sleep(2) # Wait for map to render tiles
# Save screenshot
driver.save_screenshot(output_path)
print(f"Map image saved to {output_path}")
except Exception as e:
print(f"Error rendering map to PNG: {e}")
import traceback
traceback.print_exc()
finally:
if driver:
driver.quit()
# Cleanup temp file
if os.path.exists(tmp_html_path):
os.remove(tmp_html_path)
if __name__ == "__main__":
# Test run
html = generate_aqi_map_html()
render_map_to_png(html, "aqi_map_test.png")