mirix's picture
Upload 4 files
61681cd verified
import gradio as gr
import geopandas as gpd
import pandas as pd
import json
import base64
from shapely.geometry import MultiPolygon
# ──────────────────────────────────────────────────────────────
# IUCN Category reference information
# ──────────────────────────────────────────────────────────────
IUCN_CATEGORIES = {
"Ia": {
"name": "Strict Nature Reserve",
"description": (
"Strictly protected for biodiversity, geological/geomorphological"
" features, and scientific research."
),
},
"Ib": {
"name": "Wilderness Area",
"description": (
"Large, unmodified areas retaining natural character, without"
" permanent habitation, managed to preserve natural condition."
),
},
"II": {
"name": "National Park",
"description": (
"Large natural/near-natural areas protecting large-scale ecological"
" processes and species, while allowing compatible spiritual/"
"recreational use."
),
},
"III": {
"name": "Natural Monument/Feature",
"description": (
"Set aside to protect a specific natural monument, landform, sea"
" mount, or geological feature."
),
},
"IV": {
"name": "Habitat/Species Management Area",
"description": (
"Protects particular species or habitats, often requiring regular,"
" active management interventions."
),
},
"V": {
"name": "Protected Landscape/Seascape",
"description": (
"Protects areas where the interaction of people and nature over"
" time has produced a distinct character."
),
},
"VI": {
"name": "Sustainable Use Area",
"description": (
"Conserves ecosystems and habitats together with associated"
" cultural values and traditional natural resource management"
" systems."
),
},
"Not Applicable": {
"name": "Not Applicable",
"description": "IUCN category is not applicable to this protected area.",
},
"Not Assigned": {
"name": "Not Assigned",
"description": "IUCN category has not been assigned.",
},
"Not Reported": {
"name": "Not Reported",
"description": "IUCN category has not been reported.",
},
}
# ──────────────────────────────────────────────────────────────
# Color palette for IUCN categories
# ──────────────────────────────────────────────────────────────
IUCN_COLORS = {
"Ia": "rgba(95, 70, 144, 0.65)",
"Ib": "rgba(29, 105, 150, 0.65)",
"II": "rgba(56, 166, 165, 0.65)",
"III": "rgba(15, 133, 84, 0.65)",
"IV": "rgba(115, 175, 72, 0.65)",
"V": "rgba(237, 173, 8, 0.65)",
"VI": "rgba(225, 124, 5, 0.65)",
"Not Applicable": "rgba(204, 80, 62, 0.65)",
"Not Assigned": "rgba(148, 103, 189, 0.65)",
"Not Reported": "rgba(140, 140, 140, 0.65)",
}
DEFAULT_COLOR = "rgba(200, 200, 200, 0.5)"
# Terrain exaggeration (1 = real, 1.5-3 = good for country scale)
TERRAIN_EXAGGERATION = 20
# ──────────────────────────────────────────────────────────────
# Helper: keep only the largest polygon from a MultiPolygon
# ──────────────────────────────────────────────────────────────
def largest_polygon(geom):
if isinstance(geom, MultiPolygon):
return max(geom.geoms, key=lambda g: g.area)
return geom
# ──────────────────────────────────────────────────────────────
# Load data
# ──────────────────────────────────────────────────────────────
try:
protected_gdf = gpd.read_parquet(
"Russia_protected_areas_simplified.geoparquet"
)
protected_gdf = protected_gdf.to_crs(epsg=4326)
print(f"Loaded {len(protected_gdf)} protected areas")
protected_gdf["geometry"] = protected_gdf.geometry.apply(largest_polygon)
if "IUCN_CAT" in protected_gdf.columns:
protected_gdf["IUCN_CAT"] = (
protected_gdf["IUCN_CAT"]
.fillna("Not Reported")
.astype(str)
.apply(lambda x: x if x in IUCN_CATEGORIES else "Not Reported")
)
columns_to_keep = [
"NAME", "DESIG", "DESIG_ENG", "DESIG_TYPE", "IUCN_CAT", "geometry",
]
available_columns = [
c for c in columns_to_keep if c in protected_gdf.columns
]
protected_gdf = protected_gdf[available_columns].copy()
print(f"Columns retained: {', '.join(protected_gdf.columns.tolist())}")
print("\nIUCN Category distribution:")
print(protected_gdf["IUCN_CAT"].value_counts().sort_index())
except Exception as e:
print(f"Error loading protected areas: {e}")
protected_gdf = None
# ──────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────
def format_value(value):
if pd.isna(value) or value in ("", "None"):
return "N/A"
return str(value)
def create_legend_table():
rows = [
{
"Category": cat,
"Name": info["name"],
"Primary Objective": info["description"],
}
for cat, info in IUCN_CATEGORIES.items()
]
return pd.DataFrame(rows)
# ──────────────────────────────────────────────────────────────
# Convert GeoDataFrame β†’ GeoJSON with IUCN_CAT in properties
# ──────────────────────────────────────────────────────────────
def gdf_to_geojson(gdf):
features = []
for _, row in gdf.iterrows():
props = {
"name": format_value(row.get("NAME")),
"desig": format_value(row.get("DESIG")),
"desig_eng": format_value(row.get("DESIG_ENG")),
"desig_type": format_value(row.get("DESIG_TYPE")),
"iucn_cat": format_value(row.get("IUCN_CAT")),
}
features.append({
"type": "Feature",
"geometry": row["geometry"].__geo_interface__,
"properties": props,
})
return {"type": "FeatureCollection", "features": features}
# ──────────────────────────────────────────────────────────────
# Build the MapLibre GL JS map (full HTML)
# ──────────────────────────────────────────────────────────────
def create_map():
if protected_gdf is None:
return "<p style='color:red'>Error: Could not load data.</p>"
print("Building GeoJSON …")
geojson_data = gdf_to_geojson(protected_gdf)
geojson_str = json.dumps(geojson_data)
print(f" {len(geojson_data['features'])} features serialized.")
# Build the MapLibre color match expression for IUCN_CAT
color_expr = ["match", ["get", "iucn_cat"]]
for cat, color in IUCN_COLORS.items():
color_expr.append(cat)
color_expr.append(color)
color_expr.append(DEFAULT_COLOR) # fallback
color_expr_json = json.dumps(color_expr)
# Legend HTML
legend_items = ""
for cat, color in IUCN_COLORS.items():
label = f"{cat} β€” {IUCN_CATEGORIES[cat]['name']}"
legend_items += (
f'<div style="display:flex;align-items:center;margin:2px 0">'
f'<span style="display:inline-block;width:14px;height:14px;'
f"background:{color};border:1px solid #555;"
f'margin-right:6px;border-radius:2px"></span>'
f'<span style="color:#222">{label}</span></div>'
)
map_html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet">
<style>
body {{ margin:0; padding:0; }}
#map {{ position:absolute; top:0; bottom:0; width:100%; }}
#legend {{
position:absolute; top:10px; left:10px; z-index:10;
background:rgba(255,255,255,0.92); padding:10px 14px;
border-radius:6px; font-size:11px; font-family:sans-serif;
max-height:340px; overflow-y:auto;
box-shadow:0 2px 6px rgba(0,0,0,0.3);
}}
#legend b {{ font-size:12px; }}
.maplibregl-popup-content {{
font-family:sans-serif; font-size:13px; padding:10px;
}}
</style>
</head>
<body>
<div id="map"></div>
<div id="legend"><b>IUCN Category</b><br>{legend_items}</div>
<script>
const geojsonData = {geojson_str};
const map = new maplibregl.Map({{
container: 'map',
style: {{
version: 8,
glyphs: "https://demotiles.maplibre.org/font/{{fontstack}}/{{range}}.pbf",
sources: {{
'esri-satellite': {{
type: 'raster',
tiles: [
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{{z}}/{{y}}/{{x}}'
],
tileSize: 256,
maxzoom: 18,
attribution: 'Β© Esri'
}},
'carto-labels': {{
type: 'raster',
tiles: [
'https://a.basemaps.cartocdn.com/light_only_labels/{{z}}/{{x}}/{{y}}@2x.png',
'https://b.basemaps.cartocdn.com/light_only_labels/{{z}}/{{x}}/{{y}}@2x.png',
'https://c.basemaps.cartocdn.com/light_only_labels/{{z}}/{{x}}/{{y}}@2x.png'
],
tileSize: 256,
maxzoom: 19,
attribution: 'Β© CartoDB Β© OpenStreetMap contributors'
}},
'terrain-dem': {{
type: 'raster-dem',
tiles: [
'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{{z}}/{{x}}/{{y}}.png'
],
tileSize: 256,
maxzoom: 15,
encoding: 'terrarium'
}}
}},
layers: [
{{
id: 'satellite',
type: 'raster',
source: 'esri-satellite',
minzoom: 0,
maxzoom: 22
}},
{{
id: 'labels-overlay',
type: 'raster',
source: 'carto-labels',
minzoom: 0,
maxzoom: 22,
paint: {{ 'raster-opacity': 0.85 }}
}}
],
terrain: {{
source: 'terrain-dem',
exaggeration: {TERRAIN_EXAGGERATION}
}}
}},
center: [90, 60],
zoom: 2.5,
pitch: 50,
bearing: 0,
maxPitch: 85,
antialias: true
}});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
map.on('load', () => {{
// Add protected-areas source
map.addSource('protected-areas', {{
type: 'geojson',
data: geojsonData
}});
// Filled polygons (inserted BELOW labels so labels stay on top)
map.addLayer({{
id: 'protected-fill',
type: 'fill',
source: 'protected-areas',
paint: {{
'fill-color': {color_expr_json},
'fill-opacity': 0.6
}}
}}, 'labels-overlay');
// Polygon outlines
map.addLayer({{
id: 'protected-outline',
type: 'line',
source: 'protected-areas',
paint: {{
'line-color': '#ffffff',
'line-width': 0.8,
'line-opacity': 0.5
}}
}}, 'labels-overlay');
// Hover popup
const popup = new maplibregl.Popup({{
closeButton: false,
closeOnClick: false,
maxWidth: '320px'
}});
map.on('mouseenter', 'protected-fill', (e) => {{
map.getCanvas().style.cursor = 'pointer';
const p = e.features[0].properties;
popup.setLngLat(e.lngLat)
.setHTML(
'<b>Name:</b> ' + p.name + '<br>' +
'<b>Designation:</b> ' + p.desig + '<br>' +
'<b>Designation (EN):</b> ' + p.desig_eng + '<br>' +
'<b>Designation Type:</b> ' + p.desig_type + '<br>' +
'<b>IUCN Category:</b> ' + p.iucn_cat
)
.addTo(map);
}});
map.on('mousemove', 'protected-fill', (e) => {{
popup.setLngLat(e.lngLat);
}});
map.on('mouseleave', 'protected-fill', () => {{
map.getCanvas().style.cursor = '';
popup.remove();
}});
}});
</script>
</body>
</html>"""
# Encode as base64 data-URI
html_bytes = map_html.encode("utf-8")
b64 = base64.b64encode(html_bytes).decode("ascii")
return (
f'<iframe src="data:text/html;base64,{b64}" '
f'width="100%" height="800" '
f'style="border:none;border-radius:8px;" '
f'sandbox="allow-scripts allow-same-origin" '
f'loading="lazy"></iframe>'
)
# ──────────────────────────────────────────────────────────────
# Gradio UI
# ──────────────────────────────────────────────────────────────
with gr.Blocks(title="Russia Protected Areas") as demo:
gr.Markdown("# Protected Areas of Russia β€” 3D Terrain")
gr.Markdown(
"Explore protected areas in Russia colored by their IUCN"
" conservation category, draped over 3D satellite terrain."
" Drag to rotate, scroll to zoom, Ctrl+drag to change pitch."
)
with gr.Row():
map_html = gr.HTML(value=create_map(), label="3D Terrain Map")
with gr.Row():
gr.Markdown("## IUCN Protected Area Categories")
with gr.Row():
legend_table = gr.DataFrame(
value=create_legend_table(),
label="Category Definitions",
interactive=False,
wrap=True,
)
if protected_gdf is not None:
with gr.Row():
gr.Markdown("## Statistics")
with gr.Row():
total_areas = len(protected_gdf)
stats_data = []
for cat in IUCN_CATEGORIES:
count = len(protected_gdf[protected_gdf["IUCN_CAT"] == cat])
if count > 0:
percentage = (count / total_areas) * 100
stats_data.append({
"IUCN Category": cat,
"Number of Areas": f"{count:,}",
"Percentage": f"{percentage:.1f}%",
})
gr.DataFrame(
value=pd.DataFrame(stats_data),
label=f"Distribution of {total_areas:,} Protected Areas",
interactive=False,
)
gr.Markdown("""
---
### Data Sources & Attribution
**Protected Areas Data:** [Protected Planet - WDPA](https://www.protectedplanet.net/)
**IUCN Categories:** [IUCN Protected Area Categories System](https://www.iucn.org/theme/protected-areas/about/protected-area-categories)
**Elevation Tiles:** [AWS Terrain Tiles (Terrarium)](https://registry.opendata.aws/terrain-tiles/)
**Satellite Imagery:** [Esri World Imagery](https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9)
**Labels & Borders:** [CartoDB Basemaps](https://carto.com/basemaps/) Β© OpenStreetMap contributors
""")
if __name__ == "__main__":
demo.launch()