Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Aplicação Gradio para Análise Geoespacial - VERSÃO MELHORADA (UI + ZIP) | |
| Melhorias solicitadas: | |
| - Legendas nos gráficos (Matplotlib) e mapas (Folium) | |
| - Mapas interativos e mapas com grid passam a ser exibidos corretamente na interface | |
| (via iframe com srcdoc) | |
| - Todo output gerado é salvo e pode ser baixado em um único ZIP | |
| EXECUTAR: python3.11 app_melhorado_v3.py (ou python app_melhorado_v3.py) | |
| ACESSAR: http://localhost:7860 | |
| """ | |
| import gradio as gr | |
| import geopandas as gpd | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import folium | |
| from folium import plugins | |
| import matplotlib.colors as mcolors | |
| from sklearn.cluster import KMeans, DBSCAN | |
| from sklearn.preprocessing import StandardScaler | |
| import warnings | |
| from datetime import datetime | |
| import tempfile | |
| import os | |
| from pathlib import Path | |
| import zipfile | |
| import html as _html | |
| warnings.filterwarnings('ignore') | |
| # Configurar estilo (mantido) | |
| sns.set_style("whitegrid") | |
| plt.rcParams['figure.figsize'] = (12, 8) | |
| plt.rcParams['font.size'] = 10 | |
| # ============================================================================ | |
| # VARIÁVEIS GLOBAIS | |
| # ============================================================================ | |
| buildings_gdf = None | |
| highways_gdf = None | |
| grid_gdf = None | |
| # Sessão atual de outputs (para ZIP) | |
| CURRENT_SESSION_DIR = None | |
| # Pasta de outputs (DEVE ficar dentro do diretório de trabalho para o Gradio aceitar downloads) | |
| # Pasta de outputs | |
| # - Em Hugging Face Spaces, é seguro escrever em /tmp (recomendado) e também no diretório do app. | |
| # - Para evitar erros de permissões/paths, usamos /tmp por padrão quando disponível. | |
| OUTPUT_ROOT = Path(os.environ.get('GEO_OUTPUT_ROOT', Path(tempfile.gettempdir()) / 'geospatial_downloads')) | |
| OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) | |
| # ============================================================================ | |
| # HELPERS (ZIP + EMBED) | |
| # ============================================================================ | |
| def _ensure_session_dir(): | |
| """Cria (uma vez) a pasta de outputs da sessão atual.""" | |
| global CURRENT_SESSION_DIR | |
| if CURRENT_SESSION_DIR is None: | |
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| CURRENT_SESSION_DIR = OUTPUT_ROOT / f"session_{ts}" | |
| CURRENT_SESSION_DIR.mkdir(parents=True, exist_ok=True) | |
| return CURRENT_SESSION_DIR | |
| def _save_text(name: str, content: str) -> str: | |
| """Salva texto na sessão e devolve o caminho.""" | |
| session_dir = _ensure_session_dir() | |
| fp = session_dir / name | |
| fp.write_text(content, encoding="utf-8") | |
| return str(fp) | |
| def _save_gpkg(name: str, gdf: gpd.GeoDataFrame, layer: str = "data") -> str: | |
| """Salva um GeoDataFrame em GPKG na sessão e devolve o caminho.""" | |
| session_dir = _ensure_session_dir() | |
| fp = session_dir / name | |
| # garante extensão .gpkg | |
| if fp.suffix.lower() != ".gpkg": | |
| fp = fp.with_suffix(".gpkg") | |
| # escreve (sempre sobrescreve) | |
| gdf.to_file(fp, layer=layer, driver="GPKG") | |
| return str(fp) | |
| def _copy_into_session(src_path: str, name: str | None = None) -> str: | |
| """Copia um arquivo gerado (tmp) para a pasta da sessão e devolve o novo caminho.""" | |
| session_dir = _ensure_session_dir() | |
| src = Path(src_path) | |
| dst = session_dir / (name or src.name) | |
| # leitura/escrita binária para evitar problemas cross-platform | |
| dst.write_bytes(src.read_bytes()) | |
| return str(dst) | |
| def _make_zip() -> str | None: | |
| """Empacota TODO o conteúdo da sessão em um ZIP e devolve o caminho.""" | |
| session_dir = _ensure_session_dir() | |
| # se ainda não há nada, retorna None | |
| if not any(session_dir.iterdir()): | |
| return None | |
| zip_path = session_dir.with_suffix(".zip") | |
| with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as z: | |
| for p in session_dir.rglob("*"): | |
| if p.is_file(): | |
| z.write(p, arcname=str(p.relative_to(session_dir))) | |
| return str(zip_path) | |
| def _iframe_srcdoc(html_content: str, height: int = 650) -> str: | |
| """ | |
| Garante exibição consistente de mapas Folium no Gradio. | |
| Usa iframe com srcdoc e HTML escapado. | |
| """ | |
| escaped = _html.escape(html_content, quote=True) | |
| return f""" | |
| <div style="width:100%; height:{height}px; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;"> | |
| <iframe | |
| style="width:100%; height:100%; border:0;" | |
| srcdoc="{escaped}"> | |
| </iframe> | |
| </div> | |
| """ | |
| def _add_folium_legend(m: folium.Map, title: str, items: list[tuple[str, str]]): | |
| """ | |
| Adiciona legenda simples no canto do mapa (HTML overlay). | |
| items: [(label, color), ...] | |
| """ | |
| rows = "\n".join( | |
| [f"<div style='display:flex; align-items:center; gap:8px; margin:4px 0;'>" | |
| f"<span style='width:14px; height:14px; border-radius:3px; background:{c}; display:inline-block; border:1px solid rgba(0,0,0,0.2)'></span>" | |
| f"<span style='font-size:12px; color:#111827;'>{_html.escape(str(l))}</span>" | |
| f"</div>" for l, c in items] | |
| ) | |
| legend_html = f""" | |
| <div style=" | |
| position: fixed; | |
| bottom: 20px; | |
| left: 20px; | |
| z-index: 9999; | |
| background: rgba(255,255,255,0.92); | |
| padding: 10px 12px; | |
| border-radius: 12px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.12); | |
| border: 1px solid rgba(0,0,0,0.08); | |
| max-width: 260px;"> | |
| <div style="font-weight:700; font-size:13px; margin-bottom:6px; color:#111827;"> | |
| {_html.escape(title)} | |
| </div> | |
| {rows} | |
| </div> | |
| """ | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| def create_zip_for_download(): | |
| """Gera/atualiza o ZIP com TODOS os outputs da sessão e devolve o caminho.""" | |
| try: | |
| zp = _make_zip() | |
| if zp is None: | |
| return '⚠️ Ainda não há outputs nesta sessão para empacotar.', None | |
| from pathlib import Path as _P | |
| return f'📦 ZIP gerado: {_P(zp).name}', zp | |
| except Exception as e: | |
| return f'❌ Erro ao gerar ZIP: {e}', None | |
| # ============================================================================ | |
| # FUNÇÕES DE PROCESSAMENTO | |
| # ============================================================================ | |
| def load_data(buildings_file, highways_file): | |
| """Carrega os arquivos GPKG""" | |
| global buildings_gdf, highways_gdf, grid_gdf, CURRENT_SESSION_DIR | |
| try: | |
| if buildings_file is None or highways_file is None: | |
| return "❌ Por favor, carregue ambos os arquivos (edifícios e ruas)", None | |
| # reset de sessão a cada novo carregamento (para zip limpo) | |
| CURRENT_SESSION_DIR = None | |
| grid_gdf = None | |
| buildings_gdf = gpd.read_file(buildings_file) | |
| highways_gdf = gpd.read_file(highways_file) | |
| # Reprojetar para UTM (mantido) | |
| buildings_gdf = buildings_gdf.to_crs(epsg=32632) | |
| highways_gdf = highways_gdf.to_crs(epsg=32632) | |
| # Calcular métricas básicas (mantido) | |
| buildings_gdf['area_m2'] = buildings_gdf.geometry.area | |
| buildings_gdf['perimeter_m'] = buildings_gdf.geometry.length | |
| highways_gdf['length_m'] = highways_gdf.geometry.length | |
| msg = f"""✓ Dados carregados com sucesso! | |
| 📊 EDIFÍCIOS: {len(buildings_gdf):,} registros | |
| • Área total: {buildings_gdf['area_m2'].sum():,.0f} m² | |
| • Área média: {buildings_gdf['area_m2'].mean():.0f} m² | |
| 📊 RUAS: {len(highways_gdf):,} registros | |
| • Comprimento total: {highways_gdf['length_m'].sum():,.0f} m ({highways_gdf['length_m'].sum()/1000:,.1f} km) | |
| • Comprimento médio: {highways_gdf['length_m'].mean():.0f} m | |
| 📍 Sistema de Coordenadas: {buildings_gdf.crs} | |
| ⏰ Data de Carregamento: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | |
| """ | |
| # salvar outputs base | |
| _save_text("status_carregamento.txt", msg) | |
| return msg | |
| except Exception as e: | |
| return f"❌ Erro ao carregar dados: {str(e)}" | |
| def exploratory_analysis(): | |
| """Análise exploratória básica""" | |
| global buildings_gdf, highways_gdf | |
| if buildings_gdf is None or highways_gdf is None: | |
| return "❌ Carregue os dados primeiro" | |
| try: | |
| report = f""" | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ ANÁLISE EXPLORATÓRIA - EDIFÍCIOS ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| 📐 ÁREA DOS EDIFÍCIOS (m²): | |
| Total: {buildings_gdf['area_m2'].sum():,.0f} m² | |
| Média: {buildings_gdf['area_m2'].mean():,.0f} m² | |
| Mediana: {buildings_gdf['area_m2'].median():,.0f} m² | |
| Mínima: {buildings_gdf['area_m2'].min():,.0f} m² | |
| Máxima: {buildings_gdf['area_m2'].max():,.0f} m² | |
| Desvio Padrão: {buildings_gdf['area_m2'].std():,.0f} m² | |
| Q1 (25%): {buildings_gdf['area_m2'].quantile(0.25):,.0f} m² | |
| Q3 (75%): {buildings_gdf['area_m2'].quantile(0.75):,.0f} m² | |
| 📏 PERÍMETRO DOS EDIFÍCIOS (m): | |
| Média: {buildings_gdf['perimeter_m'].mean():,.0f} m | |
| Mediana: {buildings_gdf['perimeter_m'].median():,.0f} m | |
| Máxima: {buildings_gdf['perimeter_m'].max():,.0f} m | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ ANÁLISE EXPLORATÓRIA - RUAS ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| 🛣️ COMPRIMENTO DAS RUAS (m): | |
| Total: {highways_gdf['length_m'].sum():,.0f} m ({highways_gdf['length_m'].sum()/1000:,.1f} km) | |
| Média: {highways_gdf['length_m'].mean():,.0f} m | |
| Mediana: {highways_gdf['length_m'].median():,.0f} m | |
| Mínima: {highways_gdf['length_m'].min():,.0f} m | |
| Máxima: {highways_gdf['length_m'].max():,.0f} m | |
| Desvio Padrão: {highways_gdf['length_m'].std():,.0f} m | |
| Q1 (25%): {highways_gdf['length_m'].quantile(0.25):,.0f} m | |
| Q3 (75%): {highways_gdf['length_m'].quantile(0.75):,.0f} m | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ TIPOS DE EDIFÍCIOS (TOP 15) ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| """ | |
| if 'building' in buildings_gdf.columns: | |
| building_types = buildings_gdf['building'].value_counts().head(15) | |
| for i, (btype, count) in enumerate(building_types.items(), 1): | |
| pct = (count / len(buildings_gdf)) * 100 | |
| bar = "█" * int(pct / 2) | |
| report += f"\n{i:2d}. {str(btype):20s}: {count:6d} ({pct:5.1f}%) {bar}" | |
| report += f""" | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ TIPOS DE RUAS (TOP 15) ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| """ | |
| if 'highway' in highways_gdf.columns: | |
| highway_types = highways_gdf['highway'].value_counts().head(15) | |
| for i, (htype, count) in enumerate(highway_types.items(), 1): | |
| pct = (count / len(highways_gdf)) * 100 | |
| bar = "█" * int(pct / 2) | |
| report += f"\n{i:2d}. {str(htype):30s}: {count:6d} ({pct:5.1f}%) {bar}" | |
| # salvar relatório | |
| _save_text("relatorio_eda.txt", report) | |
| return report | |
| except Exception as e: | |
| return f"❌ Erro na análise: {str(e)}" | |
| def create_distributions(): | |
| """Cria gráficos de distribuição (com legendas)""" | |
| global buildings_gdf, highways_gdf | |
| if buildings_gdf is None or highways_gdf is None: | |
| return None | |
| try: | |
| fig, axes = plt.subplots(2, 2, figsize=(14, 10)) | |
| fig.suptitle('Distribuições - Edifícios e Ruas', fontsize=16, fontweight='bold') | |
| # Histograma área | |
| axes[0, 0].hist(buildings_gdf['area_m2'], bins=50, edgecolor='black', alpha=0.7, color='steelblue', label='Edifícios (área)') | |
| axes[0, 0].set_xlabel('Área (m²)') | |
| axes[0, 0].set_ylabel('Frequência') | |
| axes[0, 0].set_title('Distribuição de Áreas de Edifícios') | |
| axes[0, 0].set_yscale('log') | |
| axes[0, 0].grid(True, alpha=0.3) | |
| axes[0, 0].legend(loc='upper right') | |
| # Boxplot área | |
| buildings_gdf.boxplot(column='area_m2', ax=axes[0, 1]) | |
| axes[0, 1].set_ylabel('Área (m²)') | |
| axes[0, 1].set_title('Box Plot - Áreas de Edifícios') | |
| axes[0, 1].set_yscale('log') | |
| axes[0, 1].grid(True, alpha=0.3) | |
| # legenda simples | |
| axes[0, 1].plot([], [], label='Edifícios (área)', color='black') | |
| axes[0, 1].legend(loc='upper right') | |
| # Histograma ruas | |
| axes[1, 0].hist(highways_gdf['length_m'], bins=50, edgecolor='black', alpha=0.7, color='coral', label='Ruas (comprimento)') | |
| axes[1, 0].set_xlabel('Comprimento (m)') | |
| axes[1, 0].set_ylabel('Frequência') | |
| axes[1, 0].set_title('Distribuição de Comprimentos de Ruas') | |
| axes[1, 0].set_yscale('log') | |
| axes[1, 0].grid(True, alpha=0.3) | |
| axes[1, 0].legend(loc='upper right') | |
| # Boxplot ruas | |
| highways_gdf.boxplot(column='length_m', ax=axes[1, 1]) | |
| axes[1, 1].set_ylabel('Comprimento (m)') | |
| axes[1, 1].set_title('Box Plot - Comprimentos de Ruas') | |
| axes[1, 1].set_yscale('log') | |
| axes[1, 1].grid(True, alpha=0.3) | |
| axes[1, 1].plot([], [], label='Ruas (comprimento)', color='black') | |
| axes[1, 1].legend(loc='upper right') | |
| plt.tight_layout() | |
| # Salvar em arquivo temporário e copiar para sessão | |
| with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: | |
| plt.savefig(tmp.name, format='png', dpi=120, bbox_inches='tight') | |
| plt.close() | |
| out_path = _copy_into_session(tmp.name, "graficos_distribuicoes.png") | |
| return out_path | |
| except Exception as e: | |
| print(f"Erro: {e}") | |
| return None | |
| def create_types_charts(): | |
| """Cria gráficos de tipos (com legendas)""" | |
| global buildings_gdf, highways_gdf | |
| if buildings_gdf is None or highways_gdf is None: | |
| return None | |
| try: | |
| fig, axes = plt.subplots(1, 2, figsize=(16, 6)) | |
| fig.suptitle('Tipos de Edifícios e Ruas', fontsize=16, fontweight='bold') | |
| if 'building' in buildings_gdf.columns: | |
| building_types = buildings_gdf['building'].value_counts().head(15) | |
| building_types.plot(kind='barh', ax=axes[0], color='steelblue', label='Quantidade') | |
| axes[0].set_xlabel('Quantidade') | |
| axes[0].set_title('Top 15 Tipos de Edifícios') | |
| axes[0].grid(True, alpha=0.3, axis='x') | |
| axes[0].legend(loc='lower right') | |
| if 'highway' in highways_gdf.columns: | |
| highway_types = highways_gdf['highway'].value_counts().head(15) | |
| highway_types.plot(kind='barh', ax=axes[1], color='coral', label='Quantidade') | |
| axes[1].set_xlabel('Quantidade') | |
| axes[1].set_title('Top 15 Tipos de Ruas') | |
| axes[1].grid(True, alpha=0.3, axis='x') | |
| axes[1].legend(loc='lower right') | |
| plt.tight_layout() | |
| with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: | |
| plt.savefig(tmp.name, format='png', dpi=120, bbox_inches='tight') | |
| plt.close() | |
| out_path = _copy_into_session(tmp.name, "graficos_tipos.png") | |
| return out_path | |
| except Exception as e: | |
| print(f"Erro: {e}") | |
| return None | |
| def spatial_analysis(): | |
| """Análise espacial com clustering + gráficos explicativos""" | |
| global buildings_gdf, highways_gdf, grid_gdf | |
| if buildings_gdf is None or highways_gdf is None: | |
| return "❌ Carregue os dados primeiro" | |
| try: | |
| # Grid | |
| grid_size = 500 | |
| minx, miny, maxx, maxy = buildings_gdf.total_bounds | |
| cols = np.arange(minx, maxx + grid_size, grid_size) | |
| rows = np.arange(miny, maxy + grid_size, grid_size) | |
| cells = [] | |
| cell_ids = [] | |
| cell_id = 0 | |
| from shapely.geometry import box | |
| for i in range(len(cols) - 1): | |
| for j in range(len(rows) - 1): | |
| cells.append(box(cols[i], rows[j], cols[i + 1], rows[j + 1])) | |
| cell_ids.append(cell_id) | |
| cell_id += 1 | |
| grid_gdf = gpd.GeoDataFrame({"cell_id": cell_ids, "geometry": cells}, crs=buildings_gdf.crs) | |
| buildings_in_grid = gpd.sjoin(buildings_gdf, grid_gdf, how="left", predicate="intersects") | |
| building_counts = buildings_in_grid.groupby("cell_id").size() | |
| grid_gdf["building_count"] = grid_gdf["cell_id"].map(building_counts).fillna(0).astype(int) | |
| cell_area_ha = (grid_size * grid_size) / 10000.0 # hectares | |
| grid_gdf["building_density"] = grid_gdf["building_count"] / cell_area_ha | |
| grid_gdf_active = grid_gdf[grid_gdf["building_count"] > 0].copy() | |
| # Clustering (usa centroides em WGS84 para estabilidade visual) | |
| b_wgs = buildings_gdf.to_crs(epsg=4326).copy() | |
| coords = np.column_stack([b_wgs.geometry.centroid.y.values, b_wgs.geometry.centroid.x.values]) | |
| scaler = StandardScaler() | |
| coords_scaled = scaler.fit_transform(coords) | |
| kmeans = KMeans(n_clusters=5, random_state=42, n_init=10) | |
| buildings_gdf["cluster_kmeans"] = kmeans.fit_predict(coords_scaled) | |
| dbscan = DBSCAN(eps=0.05, min_samples=10) | |
| buildings_gdf["cluster_dbscan"] = dbscan.fit_predict(coords_scaled) | |
| n_clusters_dbscan = len(set(buildings_gdf["cluster_dbscan"])) - (1 if -1 in buildings_gdf["cluster_dbscan"] else 0) | |
| n_noise = int((buildings_gdf["cluster_dbscan"] == -1).sum()) | |
| report = f""" | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ ANÁLISE ESPACIAL - GRID DE DENSIDADE ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| 📊 GRID (500m x 500m): | |
| Total de células: {len(grid_gdf)} | |
| Células com edifícios: {len(grid_gdf_active)} | |
| Cobertura: {(len(grid_gdf_active)/len(grid_gdf)*100):.1f}% | |
| 📈 DENSIDADE DE EDIFÍCIOS (edifícios/hectare): | |
| Média: {grid_gdf_active['building_density'].mean():.2f} | |
| Mediana: {grid_gdf_active['building_density'].median():.2f} | |
| Máxima: {grid_gdf_active['building_density'].max():.2f} | |
| Mínima: {grid_gdf_active['building_density'].min():.2f} | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ ANÁLISE ESPACIAL - CLUSTERING K-MEANS ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| 🎯 CLUSTERING K-MEANS (k=5): | |
| Total de edifícios: {len(buildings_gdf)} | |
| Clusters: {buildings_gdf['cluster_kmeans'].nunique()} | |
| Distribuição por cluster (K-Means): | |
| """ | |
| cluster_counts = buildings_gdf["cluster_kmeans"].value_counts().sort_index() | |
| total_buildings = len(buildings_gdf) | |
| for cluster, count in cluster_counts.items(): | |
| pct = (count / total_buildings) * 100 | |
| bar = "█" * int(pct / 2) | |
| # tenta usar área se existir | |
| area = 0.0 | |
| try: | |
| area = float(buildings_gdf.loc[buildings_gdf["cluster_kmeans"] == cluster, "area_m2"].sum()) | |
| except Exception: | |
| area = 0.0 | |
| report += f"\n Cluster {cluster}: {count:6d} edifícios ({pct:5.1f}%) {bar}" | |
| if area > 0: | |
| report += f" - {area:,.0f} m² de área" | |
| report += f""" | |
| ╔════════════════════════════════════════════════════════════════╗ | |
| ║ ANÁLISE ESPACIAL - CLUSTERING DBSCAN ║ | |
| ╚════════════════════════════════════════════════════════════════╝ | |
| 🎯 CLUSTERING DBSCAN: | |
| Clusters encontrados: {n_clusters_dbscan} | |
| Pontos de ruído: {n_noise} ({(n_noise/len(buildings_gdf))*100:.1f}%) | |
| """ | |
| # === Gráficos (para explicar os dados) === | |
| fig = plt.figure(figsize=(12, 8)) | |
| ax1 = fig.add_subplot(2, 2, 1) | |
| ax2 = fig.add_subplot(2, 2, 2) | |
| ax3 = fig.add_subplot(2, 2, 3) | |
| ax4 = fig.add_subplot(2, 2, 4) | |
| dens = grid_gdf_active["building_density"].replace([np.inf, -np.inf], np.nan).dropna() | |
| ax1.hist(dens, bins=30) | |
| ax1.set_title("Distribuição da densidade (edifícios/ha)") | |
| ax1.set_xlabel("edifícios/ha") | |
| ax1.set_ylabel("freq.") | |
| ax1.grid(True, alpha=0.3) | |
| top = grid_gdf_active.sort_values("building_density", ascending=False).head(10) | |
| ax2.bar(top["cell_id"].astype(str), top["building_density"]) | |
| ax2.set_title("Top 10 células por densidade") | |
| ax2.set_xlabel("cell_id") | |
| ax2.set_ylabel("edifícios/ha") | |
| ax2.tick_params(axis="x", rotation=45) | |
| ax2.grid(True, alpha=0.3) | |
| km_counts = buildings_gdf["cluster_kmeans"].value_counts().sort_index() | |
| ax3.bar(km_counts.index.astype(str), km_counts.values) | |
| ax3.set_title("K-Means: contagem por cluster") | |
| ax3.set_xlabel("cluster") | |
| ax3.set_ylabel("n edifícios") | |
| ax3.grid(True, alpha=0.3) | |
| db_counts = buildings_gdf["cluster_dbscan"].value_counts().sort_index() | |
| ax4.bar(db_counts.index.astype(str), db_counts.values) | |
| ax4.set_title("DBSCAN: contagem por rótulo (-1 = ruído)") | |
| ax4.set_xlabel("rótulo") | |
| ax4.set_ylabel("n edifícios") | |
| ax4.grid(True, alpha=0.3) | |
| fig.tight_layout() | |
| # Salva outputs adicionais | |
| _save_text("relatorio_espacial.txt", report) | |
| try: | |
| session_dir = _ensure_session_dir() | |
| fig_path = session_dir / "graficos_analise_espacial.png" | |
| fig.savefig(fig_path, dpi=160, bbox_inches="tight") | |
| except Exception: | |
| pass | |
| # Salva GPKG correspondentes (objetos que originam mapas) | |
| try: | |
| _save_gpkg("grid_500m.gpkg", grid_gdf, layer="grid") | |
| except Exception: | |
| pass | |
| try: | |
| _save_gpkg("edificios_com_clusters.gpkg", buildings_gdf, layer="buildings") | |
| except Exception: | |
| pass | |
| return report, fig | |
| except Exception as e: | |
| return f"❌ Erro na análise espacial: {str(e)}", None | |
| def create_heatmap(): | |
| """Cria mapa de calor (exibido via iframe + salvo)""" | |
| global buildings_gdf | |
| if buildings_gdf is None: | |
| return None, None, None | |
| try: | |
| buildings_wgs84 = buildings_gdf.to_crs(epsg=4326) | |
| center_lat = buildings_wgs84.geometry.centroid.y.mean() | |
| center_lon = buildings_wgs84.geometry.centroid.x.mean() | |
| # GPKG base: pontos (WGS84) + colunas de cluster (se existirem) | |
| kmeans_gdf = buildings_wgs84.copy() | |
| dbscan_gdf = buildings_wgs84.copy() | |
| if 'cluster_kmeans' in buildings_gdf.columns and 'cluster_kmeans' not in kmeans_gdf.columns: | |
| kmeans_gdf['cluster_kmeans'] = buildings_gdf['cluster_kmeans'].values | |
| if 'cluster_dbscan' in buildings_gdf.columns and 'cluster_dbscan' not in dbscan_gdf.columns: | |
| dbscan_gdf['cluster_dbscan'] = buildings_gdf['cluster_dbscan'].values | |
| m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap') | |
| heat_data = [] | |
| for _, row in buildings_wgs84.iterrows(): | |
| centroid = row.geometry.centroid | |
| heat_data.append([centroid.y, centroid.x]) | |
| plugins.HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m) | |
| # legenda | |
| _add_folium_legend( | |
| m, | |
| "Mapa de Calor (Edifícios)", | |
| [("Intensidade = concentração de pontos", "#ef4444")] | |
| ) | |
| # salvar html | |
| with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp: | |
| m.save(tmp.name) | |
| html_content = Path(tmp.name).read_text(encoding="utf-8") | |
| out_path = _copy_into_session(tmp.name, "mapa_heatmap.html") | |
| # Salva GPKG correspondente (pontos usados no heatmap) | |
| gpkg_path = None | |
| try: | |
| gpkg_path = _save_gpkg("heatmap_pontos.gpkg", buildings_wgs84, layer="heatmap_points") | |
| except Exception: | |
| gpkg_path = None | |
| return _iframe_srcdoc(html_content, height=650), out_path, gpkg_path | |
| except Exception as e: | |
| print(f"Erro: {e}") | |
| return None, None, None | |
| def create_clustering_maps(): | |
| """Cria mapas de clustering (K-Means e DBSCAN) com legenda. | |
| Observação: para performance, o mapa pode usar amostragem visual quando há muitos pontos. | |
| Os GPKGs salvos contêm TODOS os pontos (sem amostragem). | |
| """ | |
| global buildings_gdf | |
| if buildings_gdf is None or len(buildings_gdf) == 0: | |
| return None, None, None, None, None, None | |
| try: | |
| # Trabalha em WGS84 para mapas | |
| buildings_wgs84 = buildings_gdf.to_crs(epsg=4326).copy() | |
| # Centróides (uma vez) | |
| centroids = buildings_wgs84.geometry.centroid | |
| buildings_wgs84["_lat"] = centroids.y.values | |
| buildings_wgs84["_lon"] = centroids.x.values | |
| center_lat = float(buildings_wgs84["_lat"].mean()) | |
| center_lon = float(buildings_wgs84["_lon"].mean()) | |
| # Garante colunas de cluster (para o GPKG e para o mapa) | |
| coords = np.column_stack([buildings_wgs84["_lat"].values, buildings_wgs84["_lon"].values]) | |
| coords_scaled = StandardScaler().fit_transform(coords) | |
| if "cluster_kmeans" not in buildings_gdf.columns: | |
| kmeans = KMeans(n_clusters=5, random_state=42, n_init=10) | |
| buildings_gdf["cluster_kmeans"] = kmeans.fit_predict(coords_scaled) | |
| if "cluster_dbscan" not in buildings_gdf.columns: | |
| dbscan = DBSCAN(eps=0.05, min_samples=10) | |
| buildings_gdf["cluster_dbscan"] = dbscan.fit_predict(coords_scaled) | |
| # Copias em WGS84 (para salvar e para mapear) | |
| kmeans_gdf = buildings_wgs84.copy() | |
| dbscan_gdf = buildings_wgs84.copy() | |
| kmeans_gdf["cluster_kmeans"] = buildings_gdf["cluster_kmeans"].values | |
| dbscan_gdf["cluster_dbscan"] = buildings_gdf["cluster_dbscan"].values | |
| # --------- AMOSTRAGEM VISUAL (para o HTML não travar) ---------- | |
| MAX_POINTS_MAP = 15000 | |
| def _sample_for_map(gdf, label_col=None): | |
| if len(gdf) <= MAX_POINTS_MAP: | |
| return gdf | |
| if label_col and label_col in gdf.columns: | |
| # amostragem estratificada por rótulo | |
| parts = [] | |
| groups = gdf.groupby(label_col, dropna=False) | |
| # distribui a cota proporcionalmente | |
| for _, grp in groups: | |
| n = max(1, int(len(grp) / len(gdf) * MAX_POINTS_MAP)) | |
| parts.append(grp.sample(n=min(n, len(grp)), random_state=42)) | |
| out = pd.concat(parts, ignore_index=True) | |
| # se ainda excedeu, corta | |
| if len(out) > MAX_POINTS_MAP: | |
| out = out.sample(n=MAX_POINTS_MAP, random_state=42) | |
| return out | |
| # fallback: amostra simples | |
| return gdf.sample(n=MAX_POINTS_MAP, random_state=42) | |
| kmeans_map_gdf = _sample_for_map(kmeans_gdf, "cluster_kmeans") | |
| dbscan_map_gdf = _sample_for_map(dbscan_gdf, "cluster_dbscan") | |
| # ----------------- KMEANS MAP ----------------- | |
| m1 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="OpenStreetMap") | |
| colors = ["red", "blue", "green", "purple", "orange"] | |
| # usa loop apenas na amostra (mais leve) | |
| for _, row in kmeans_map_gdf.iterrows(): | |
| cluster = int(row.get("cluster_kmeans", 0)) | |
| color = colors[cluster % len(colors)] | |
| folium.CircleMarker( | |
| location=[float(row["_lat"]), float(row["_lon"])], | |
| radius=3, | |
| color=color, | |
| fill=True, | |
| fillColor=color, | |
| fillOpacity=0.6, | |
| weight=1, | |
| ).add_to(m1) | |
| _add_folium_legend( | |
| m1, | |
| "K-Means (clusters)", | |
| [(f"Cluster {i}", colors[i % len(colors)]) for i in range(5)], | |
| ) | |
| # ----------------- DBSCAN MAP ----------------- | |
| m2 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="OpenStreetMap") | |
| # nº clusters (ignorando ruído -1) | |
| labels = dbscan_gdf["cluster_dbscan"].values | |
| n_clusters_dbscan = len(set(labels)) - (1 if -1 in labels else 0) | |
| cmap = plt.cm.tab20(np.linspace(0, 1, max(n_clusters_dbscan, 1))) | |
| for _, row in dbscan_map_gdf.iterrows(): | |
| cluster = int(row.get("cluster_dbscan", -1)) | |
| if cluster == -1: | |
| color = "gray" | |
| opacity = 0.3 | |
| else: | |
| color = mcolors.rgb2hex(cmap[cluster % max(n_clusters_dbscan, 1)]) | |
| opacity = 0.7 | |
| folium.CircleMarker( | |
| location=[float(row["_lat"]), float(row["_lon"])], | |
| radius=3, | |
| color=color, | |
| fill=True, | |
| fillColor=color, | |
| fillOpacity=opacity, | |
| weight=1, | |
| ).add_to(m2) | |
| legend_items = [("Ruído (-1)", "gray")] | |
| # amostra (até 8 itens) para não explodir a legenda | |
| for i in range(min(n_clusters_dbscan, 8)): | |
| legend_items.append((f"Cluster {i}", mcolors.rgb2hex(cmap[i % max(n_clusters_dbscan, 1)]))) | |
| _add_folium_legend(m2, "DBSCAN (clusters)", legend_items) | |
| # salvar htmls (sempre copiando para a pasta da sessão) | |
| with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp1: | |
| m1.save(tmp1.name) | |
| kmeans_html = Path(tmp1.name).read_text(encoding="utf-8") | |
| kmeans_file = _copy_into_session(tmp1.name, "mapa_kmeans.html") | |
| with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp2: | |
| m2.save(tmp2.name) | |
| dbscan_html = Path(tmp2.name).read_text(encoding="utf-8") | |
| dbscan_file = _copy_into_session(tmp2.name, "mapa_dbscan.html") | |
| # Salva GPKG correspondentes (TODOS os pontos + rótulos) | |
| kmeans_gpkg = None | |
| dbscan_gpkg = None | |
| try: | |
| kmeans_gpkg = _save_gpkg("kmeans_pontos_clusters.gpkg", kmeans_gdf.drop(columns=["_lat","_lon"], errors="ignore"), layer="kmeans") | |
| except Exception: | |
| kmeans_gpkg = None | |
| try: | |
| dbscan_gpkg = _save_gpkg("dbscan_pontos_clusters.gpkg", dbscan_gdf.drop(columns=["_lat","_lon"], errors="ignore"), layer="dbscan") | |
| except Exception: | |
| dbscan_gpkg = None | |
| return kmeans_html, kmeans_file, dbscan_html, dbscan_file, kmeans_gpkg, dbscan_gpkg | |
| except Exception as e: | |
| print(f"Erro em create_clustering_maps: {e}") | |
| return None, None, None, None, None, None | |
| def create_grid_maps(): | |
| """Cria mapas com grid (exibidos via iframe + salvos)""" | |
| global buildings_gdf, highways_gdf, grid_gdf | |
| if buildings_gdf is None or grid_gdf is None or highways_gdf is None: | |
| return None, None, None, None, None, None | |
| try: | |
| grid_wgs84 = grid_gdf.to_crs(epsg=4326) | |
| buildings_wgs84 = buildings_gdf.to_crs(epsg=4326) | |
| highways_wgs84 = highways_gdf.to_crs(epsg=4326) | |
| center_lat = buildings_wgs84.geometry.centroid.y.mean() | |
| center_lon = buildings_wgs84.geometry.centroid.x.mean() | |
| # Mapa 1: Densidade de edifícios | |
| m1 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap') | |
| # bins e legenda (mantém suas cores, só adiciona legenda) | |
| legend_buildings = [ | |
| ("0", "white"), | |
| ("< 2", "#ffffcc"), | |
| ("2–4", "#ffeda0"), | |
| ("4–6", "#fed976"), | |
| ("6–8", "#feb24c"), | |
| ("8–10", "#fd8d3c"), | |
| (">= 10", "#e31a1c"), | |
| ] | |
| for idx, row in grid_wgs84.iterrows(): | |
| density = float(grid_gdf.loc[idx, 'building_density']) | |
| if density == 0: | |
| color = 'white' | |
| opacity = 0 | |
| elif density < 2: | |
| color = '#ffffcc' | |
| opacity = 0.3 | |
| elif density < 4: | |
| color = '#ffeda0' | |
| opacity = 0.4 | |
| elif density < 6: | |
| color = '#fed976' | |
| opacity = 0.5 | |
| elif density < 8: | |
| color = '#feb24c' | |
| opacity = 0.6 | |
| elif density < 10: | |
| color = '#fd8d3c' | |
| opacity = 0.7 | |
| else: | |
| color = '#e31a1c' | |
| opacity = 0.8 | |
| folium.GeoJson( | |
| data=row.geometry.__geo_interface__, | |
| style_function=lambda _, color=color, opacity=opacity: { | |
| 'fillColor': color, | |
| 'color': 'black', | |
| 'weight': 0.5, | |
| 'fillOpacity': opacity | |
| } | |
| ).add_to(m1) | |
| _add_folium_legend(m1, "Densidade de edifícios (por ha)", legend_buildings) | |
| # Mapa 2: Densidade de ruas | |
| m2 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap') | |
| roads_per_cell = [] | |
| for cell in grid_gdf.geometry: | |
| roads_in_cell = highways_gdf[highways_gdf.geometry.intersects(cell)] | |
| roads_per_cell.append(float(roads_in_cell['length_m'].sum()) if len(roads_in_cell) > 0 else 0.0) | |
| grid_gdf['road_density'] = np.array(roads_per_cell) / (500 ** 2) * 10000 | |
| max_road_density = float(grid_gdf['road_density'].max()) if float(grid_gdf['road_density'].max()) > 0 else 1.0 | |
| legend_roads = [ | |
| ("0", "white"), | |
| ("Baixa", "#f7fbff"), | |
| ("Média-baixa", "#deebf7"), | |
| ("Média", "#9ecae1"), | |
| ("Média-alta", "#3182bd"), | |
| ("Alta", "#08519c"), | |
| ] | |
| for idx, row in grid_wgs84.iterrows(): | |
| road_density = float(grid_gdf.loc[idx, 'road_density']) | |
| if road_density == 0: | |
| color = 'white' | |
| opacity = 0 | |
| else: | |
| intensity = road_density / max_road_density | |
| if intensity < 0.2: | |
| color = '#f7fbff' | |
| elif intensity < 0.4: | |
| color = '#deebf7' | |
| elif intensity < 0.6: | |
| color = '#9ecae1' | |
| elif intensity < 0.8: | |
| color = '#3182bd' | |
| else: | |
| color = '#08519c' | |
| opacity = 0.7 | |
| folium.GeoJson( | |
| data=row.geometry.__geo_interface__, | |
| style_function=lambda _, color=color, opacity=opacity: { | |
| 'fillColor': color, | |
| 'color': 'black', | |
| 'weight': 0.5, | |
| 'fillOpacity': opacity | |
| } | |
| ).add_to(m2) | |
| _add_folium_legend(m2, "Densidade de ruas (m/ha)", legend_roads) | |
| # salvar htmls + copiar p/ sessão | |
| with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp1: | |
| m1.save(tmp1.name) | |
| grid_html = Path(tmp1.name).read_text(encoding="utf-8") | |
| grid_file = _copy_into_session(tmp1.name, "grid_densidade_edificios.html") | |
| with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp2: | |
| m2.save(tmp2.name) | |
| roads_html = Path(tmp2.name).read_text(encoding="utf-8") | |
| roads_file = _copy_into_session(tmp2.name, "grid_densidade_ruas.html") | |
| # Salva GPKG correspondentes (grid + métricas) | |
| # NOTE: o usuário quer sempre o GPKG correspondente a cada mapa. | |
| # Aqui salvamos o grid com as métricas calculadas. | |
| grid_gpkg = None | |
| roads_gpkg = None | |
| try: | |
| grid_buildings = grid_gdf[["building_density", "geometry"]].copy() | |
| grid_gpkg = _save_gpkg("grid_densidade_edificios.gpkg", grid_buildings, layer="grid_buildings") | |
| except Exception: | |
| grid_gpkg = None | |
| try: | |
| grid_roads = grid_gdf[["road_density", "geometry"]].copy() | |
| roads_gpkg = _save_gpkg("grid_densidade_ruas.gpkg", grid_roads, layer="grid_roads") | |
| except Exception: | |
| roads_gpkg = None | |
| return grid_html, grid_file, roads_html, roads_file, grid_gpkg, roads_gpkg | |
| except Exception as e: | |
| print(f"Erro: {e}") | |
| return None, None, None, None, None, None | |
| def get_kmeans_data(): | |
| kmeans_html, kmeans_file, _, _, kmeans_gpkg, _ = create_clustering_maps() | |
| if kmeans_html is None: | |
| return None, None, None | |
| return _iframe_srcdoc(kmeans_html, height=650), kmeans_file, kmeans_gpkg | |
| def get_dbscan_data(): | |
| _, _, dbscan_html, dbscan_file, _, dbscan_gpkg = create_clustering_maps() | |
| if dbscan_html is None: | |
| return None, None, None | |
| return _iframe_srcdoc(dbscan_html, height=650), dbscan_file, dbscan_gpkg | |
| def get_grid_data(): | |
| grid_html, grid_file, roads_html, roads_file, grid_gpkg, roads_gpkg = create_grid_maps() | |
| if grid_html is None: | |
| return None, None, None, None, None, None | |
| return ( | |
| _iframe_srcdoc(grid_html, height=650), grid_file, grid_gpkg, | |
| _iframe_srcdoc(roads_html, height=650), roads_file, roads_gpkg | |
| ) | |
| def get_zip_download(): | |
| """Gera/atualiza o ZIP sob demanda.""" | |
| msg, zp = create_zip_for_download() | |
| return zp | |
| # ============================================================================ | |
| # INTERFACE GRADIO | |
| # ============================================================================ | |
| with gr.Blocks(title="Análise Geoespacial", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# 🗺️ Análise Geoespacial - Edifícios e Ruas") | |
| gr.Markdown("**Aplicação para análise exploratória de dados geoespaciais**") | |
| # Download do ZIP sempre visível | |
| with gr.Row(): | |
| zip_btn = gr.Button("📦 Gerar/Atualizar ZIP (tudo)", variant="primary") | |
| zip_file = gr.File(label="⬇️ Download ZIP (tudo)") | |
| zip_btn.click(fn=get_zip_download, outputs=zip_file) | |
| with gr.Tab("📁 Upload de Dados"): | |
| gr.Markdown("## Carregue seus arquivos GPKG") | |
| with gr.Row(): | |
| buildings_file = gr.File(label="📦 Arquivo de Edifícios (GPKG)", file_types=[".gpkg"]) | |
| highways_file = gr.File(label="📦 Arquivo de Ruas (GPKG)", file_types=[".gpkg"]) | |
| load_btn = gr.Button("🔄 Carregar Dados", variant="primary", size="lg") | |
| status_output = gr.Textbox(label="Status", lines=12, interactive=False) | |
| load_btn.click( | |
| fn=load_data, | |
| inputs=[buildings_file, highways_file], | |
| outputs=status_output | |
| ) | |
| with gr.Tab("📊 Análise Exploratória"): | |
| gr.Markdown("## Estatísticas Descritivas e Visualizações") | |
| with gr.Row(): | |
| eda_btn = gr.Button("📈 Gerar Análise Exploratória", variant="primary", size="lg") | |
| eda_output = gr.Textbox(label="Relatório EDA", lines=35, interactive=False) | |
| eda_btn.click(fn=exploratory_analysis, outputs=eda_output) | |
| gr.Markdown("## Visualizações") | |
| with gr.Row(): | |
| dist_btn = gr.Button("📊 Distribuições", size="lg") | |
| types_btn = gr.Button("📊 Tipos", size="lg") | |
| with gr.Row(): | |
| dist_output = gr.Image(label="Gráficos de Distribuição") | |
| types_output = gr.Image(label="Tipos de Edifícios e Ruas") | |
| # Atualiza também o ZIP a cada geração | |
| dist_btn.click(fn=create_distributions, outputs=dist_output) | |
| types_btn.click(fn=create_types_charts, outputs=types_output) | |
| with gr.Tab("🎯 Análise Espacial"): | |
| gr.Markdown("## Clustering e Densidade") | |
| spatial_btn = gr.Button("🔍 Análise Espacial", variant="primary", size="lg") | |
| spatial_output = gr.Textbox(label="Relatório Espacial", lines=35, interactive=False) | |
| spatial_plot = gr.Plot(label="📊 Gráficos - Análise Espacial") | |
| spatial_btn.click(fn=spatial_analysis, outputs=[spatial_output, spatial_plot]) | |
| with gr.Tab("🗺️ Mapas Interativos"): | |
| gr.Markdown("## Visualizações Geoespaciais") | |
| with gr.Row(): | |
| heatmap_btn = gr.Button("🔥 Mapa de Calor", size="lg") | |
| kmeans_btn = gr.Button("🎯 K-Means", size="lg") | |
| dbscan_btn = gr.Button("🎯 DBSCAN", size="lg") | |
| heatmap_output = gr.HTML(value="<div style='text-align:center; padding:20px; background:#f0f0f0; border-radius:10px;'><p>Clique no botão acima para gerar o mapa de calor</p></div>", label="Mapa de Calor") | |
| heatmap_download = gr.File(label="⬇️ Download Heatmap (HTML)") | |
| heatmap_gpkg = gr.File(label="⬇️ GPKG Heatmap (pontos)") | |
| kmeans_output = gr.HTML(value="<div style='text-align:center; padding:20px; background:#f0f0f0; border-radius:10px;'><p>Clique no botão acima para gerar o mapa K-Means</p></div>", label="K-Means") | |
| kmeans_download = gr.File(label="⬇️ Download K-Means (HTML)") | |
| kmeans_gpkg = gr.File(label="⬇️ GPKG K-Means (pontos+clusters)") | |
| dbscan_output = gr.HTML(value="<div style='text-align:center; padding:20px; background:#f0f0f0; border-radius:10px;'><p>Clique no botão acima para gerar o mapa DBSCAN</p></div>", label="DBSCAN") | |
| dbscan_download = gr.File(label="⬇️ Download DBSCAN (HTML)") | |
| dbscan_gpkg = gr.File(label="⬇️ GPKG DBSCAN (pontos+rótulos)") | |
| heatmap_btn.click(fn=create_heatmap, outputs=[heatmap_output, heatmap_download, heatmap_gpkg]) | |
| kmeans_btn.click(fn=get_kmeans_data, outputs=[kmeans_output, kmeans_download, kmeans_gpkg]) | |
| dbscan_btn.click(fn=get_dbscan_data, outputs=[dbscan_output, dbscan_download, dbscan_gpkg]) | |
| with gr.Tab("🔲 Mapas com Grid"): | |
| gr.Markdown("## Análise de Densidade com Grid (500m x 500m)") | |
| with gr.Row(): | |
| grid_btn = gr.Button("📊 Gerar Mapas com Grid", variant="primary", size="lg") | |
| # Grid 1: Edifícios | |
| grid_output = gr.HTML( | |
| value="<div style='text-align:center; padding:20px; background:#f0f0f0; border-radius:10px;'><p>Clique no botão acima para gerar o grid de edifícios</p></div>", | |
| label="Grid de Densidade - Edifícios" | |
| ) | |
| with gr.Row(): | |
| grid_download = gr.File(label="⬇️ Download Grid Edifícios (HTML)", interactive=False) | |
| grid_gpkg = gr.File(label="⬇️ Download GPKG Grid Edifícios", interactive=False) | |
| # Grid 2: Ruas | |
| roads_output = gr.HTML( | |
| value="<div style='text-align:center; padding:20px; background:#f0f0f0; border-radius:10px;'><p>Clique no botão acima para gerar o grid de ruas</p></div>", | |
| label="Grid de Densidade - Ruas" | |
| ) | |
| with gr.Row(): | |
| roads_download = gr.File(label="⬇️ Download Grid Ruas (HTML)", interactive=False) | |
| roads_gpkg = gr.File(label="⬇️ Download GPKG Grid Ruas", interactive=False) | |
| grid_btn.click( | |
| fn=get_grid_data, | |
| outputs=[grid_output, grid_download, grid_gpkg, roads_output, roads_download, roads_gpkg] | |
| ) | |
| with gr.Tab("ℹ️ Sobre"): | |
| gr.Markdown(""" | |
| ## 📖 Sobre esta Aplicação | |
| Aplicação para **análise geoespacial exploratória (EDA) completa** sobre dados de edifícios e ruas. | |
| ### ✨ Funcionalidades: | |
| - ✅ Upload de arquivos GPKG | |
| - ✅ Análise exploratória com estatísticas descritivas | |
| - ✅ Visualizações de distribuições **com legendas** | |
| - ✅ Análise espacial com clustering (K-Means e DBSCAN) | |
| - ✅ Mapas interativos **exibidos na interface** (heatmap, clustering) + legendas | |
| - ✅ Mapas com Grid de Densidade (500m x 500m) **exibidos na interface** + legendas | |
| - ✅ Download consolidado de **tudo** em um ZIP | |
| ### 🚀 Como Usar Localmente: | |
| ```bash | |
| python3.11 app_melhorado_v3.py | |
| ``` | |
| Depois acesse: http://localhost:7860 | |
| """) | |
| if __name__ == "__main__": | |
| print("=" * 80) | |
| print("🗺️ APLICAÇÃO GRADIO - ANÁLISE GEOESPACIAL (UI + ZIP)") | |
| print("=" * 80) | |
| print("\n✓ Iniciando aplicação...") | |
| print("✓ Acesse: http://localhost:7860") | |
| print("✓ Pressione CTRL+C para parar\n") | |
| # Em Hugging Face Spaces, a porta é fornecida via variável de ambiente PORT. | |
| server_port = int(os.environ.get('PORT', '7860')) | |
| demo.launch(server_name="0.0.0.0", server_port=server_port, share=False, allowed_paths=[str(OUTPUT_ROOT)]) |