Spaces:
Sleeping
Sleeping
| 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() | |