brunoapj commited on
Commit
628e4b2
·
verified ·
1 Parent(s): cdd494d

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +1085 -0
  2. requirements.txt +12 -0
app.py ADDED
@@ -0,0 +1,1085 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aplicação Gradio para Análise Geoespacial - VERSÃO MELHORADA (UI + ZIP)
4
+ Melhorias solicitadas:
5
+ - Legendas nos gráficos (Matplotlib) e mapas (Folium)
6
+ - Mapas interativos e mapas com grid passam a ser exibidos corretamente na interface
7
+ (via iframe com srcdoc)
8
+ - Todo output gerado é salvo e pode ser baixado em um único ZIP
9
+
10
+ EXECUTAR: python3.11 app_melhorado_v3.py (ou python app_melhorado_v3.py)
11
+ ACESSAR: http://localhost:7860
12
+ """
13
+
14
+ import gradio as gr
15
+ import geopandas as gpd
16
+ import pandas as pd
17
+ import numpy as np
18
+ import matplotlib.pyplot as plt
19
+ import seaborn as sns
20
+ import folium
21
+ from folium import plugins
22
+ import matplotlib.colors as mcolors
23
+ from sklearn.cluster import KMeans, DBSCAN
24
+ from sklearn.preprocessing import StandardScaler
25
+ import warnings
26
+ from datetime import datetime
27
+ import tempfile
28
+ import os
29
+ from pathlib import Path
30
+ import zipfile
31
+ import html as _html
32
+
33
+ warnings.filterwarnings('ignore')
34
+
35
+ # Configurar estilo (mantido)
36
+ sns.set_style("whitegrid")
37
+ plt.rcParams['figure.figsize'] = (12, 8)
38
+ plt.rcParams['font.size'] = 10
39
+
40
+ # ============================================================================
41
+ # VARIÁVEIS GLOBAIS
42
+ # ============================================================================
43
+ buildings_gdf = None
44
+ highways_gdf = None
45
+ grid_gdf = None
46
+
47
+ # Sessão atual de outputs (para ZIP)
48
+ CURRENT_SESSION_DIR = None
49
+
50
+ # Pasta de outputs (DEVE ficar dentro do diretório de trabalho para o Gradio aceitar downloads)
51
+ # Pasta de outputs
52
+ # - Em Hugging Face Spaces, é seguro escrever em /tmp (recomendado) e também no diretório do app.
53
+ # - Para evitar erros de permissões/paths, usamos /tmp por padrão quando disponível.
54
+ OUTPUT_ROOT = Path(os.environ.get('GEO_OUTPUT_ROOT', Path(tempfile.gettempdir()) / 'geospatial_downloads'))
55
+ OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
56
+
57
+ # ============================================================================
58
+ # HELPERS (ZIP + EMBED)
59
+ # ============================================================================
60
+
61
+ def _ensure_session_dir():
62
+ """Cria (uma vez) a pasta de outputs da sessão atual."""
63
+ global CURRENT_SESSION_DIR
64
+ if CURRENT_SESSION_DIR is None:
65
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
66
+ CURRENT_SESSION_DIR = OUTPUT_ROOT / f"session_{ts}"
67
+ CURRENT_SESSION_DIR.mkdir(parents=True, exist_ok=True)
68
+ return CURRENT_SESSION_DIR
69
+
70
+ def _save_text(name: str, content: str) -> str:
71
+ """Salva texto na sessão e devolve o caminho."""
72
+ session_dir = _ensure_session_dir()
73
+ fp = session_dir / name
74
+ fp.write_text(content, encoding="utf-8")
75
+ return str(fp)
76
+
77
+
78
+ def _save_gpkg(name: str, gdf: gpd.GeoDataFrame, layer: str = "data") -> str:
79
+ """Salva um GeoDataFrame em GPKG na sessão e devolve o caminho."""
80
+ session_dir = _ensure_session_dir()
81
+ fp = session_dir / name
82
+ # garante extensão .gpkg
83
+ if fp.suffix.lower() != ".gpkg":
84
+ fp = fp.with_suffix(".gpkg")
85
+ # escreve (sempre sobrescreve)
86
+ gdf.to_file(fp, layer=layer, driver="GPKG")
87
+ return str(fp)
88
+
89
+ def _copy_into_session(src_path: str, name: str | None = None) -> str:
90
+ """Copia um arquivo gerado (tmp) para a pasta da sessão e devolve o novo caminho."""
91
+ session_dir = _ensure_session_dir()
92
+ src = Path(src_path)
93
+ dst = session_dir / (name or src.name)
94
+ # leitura/escrita binária para evitar problemas cross-platform
95
+ dst.write_bytes(src.read_bytes())
96
+ return str(dst)
97
+
98
+ def _make_zip() -> str | None:
99
+ """Empacota TODO o conteúdo da sessão em um ZIP e devolve o caminho."""
100
+ session_dir = _ensure_session_dir()
101
+ # se ainda não há nada, retorna None
102
+ if not any(session_dir.iterdir()):
103
+ return None
104
+ zip_path = session_dir.with_suffix(".zip")
105
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as z:
106
+ for p in session_dir.rglob("*"):
107
+ if p.is_file():
108
+ z.write(p, arcname=str(p.relative_to(session_dir)))
109
+ return str(zip_path)
110
+
111
+ def _iframe_srcdoc(html_content: str, height: int = 650) -> str:
112
+ """
113
+ Garante exibição consistente de mapas Folium no Gradio.
114
+ Usa iframe com srcdoc e HTML escapado.
115
+ """
116
+ escaped = _html.escape(html_content, quote=True)
117
+ return f"""
118
+ <div style="width:100%; height:{height}px; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
119
+ <iframe
120
+ style="width:100%; height:100%; border:0;"
121
+ srcdoc="{escaped}">
122
+ </iframe>
123
+ </div>
124
+ """
125
+
126
+ def _add_folium_legend(m: folium.Map, title: str, items: list[tuple[str, str]]):
127
+ """
128
+ Adiciona legenda simples no canto do mapa (HTML overlay).
129
+ items: [(label, color), ...]
130
+ """
131
+ rows = "\n".join(
132
+ [f"<div style='display:flex; align-items:center; gap:8px; margin:4px 0;'>"
133
+ 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>"
134
+ f"<span style='font-size:12px; color:#111827;'>{_html.escape(str(l))}</span>"
135
+ f"</div>" for l, c in items]
136
+ )
137
+ legend_html = f"""
138
+ <div style="
139
+ position: fixed;
140
+ bottom: 20px;
141
+ left: 20px;
142
+ z-index: 9999;
143
+ background: rgba(255,255,255,0.92);
144
+ padding: 10px 12px;
145
+ border-radius: 12px;
146
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
147
+ border: 1px solid rgba(0,0,0,0.08);
148
+ max-width: 260px;">
149
+ <div style="font-weight:700; font-size:13px; margin-bottom:6px; color:#111827;">
150
+ {_html.escape(title)}
151
+ </div>
152
+ {rows}
153
+ </div>
154
+ """
155
+ m.get_root().html.add_child(folium.Element(legend_html))
156
+
157
+
158
+
159
+ def create_zip_for_download():
160
+ """Gera/atualiza o ZIP com TODOS os outputs da sessão e devolve o caminho."""
161
+ try:
162
+ zp = _make_zip()
163
+ if zp is None:
164
+ return '⚠️ Ainda não há outputs nesta sessão para empacotar.', None
165
+ from pathlib import Path as _P
166
+ return f'📦 ZIP gerado: {_P(zp).name}', zp
167
+ except Exception as e:
168
+ return f'❌ Erro ao gerar ZIP: {e}', None
169
+ # ============================================================================
170
+ # FUNÇÕES DE PROCESSAMENTO
171
+ # ============================================================================
172
+
173
+ def load_data(buildings_file, highways_file):
174
+ """Carrega os arquivos GPKG"""
175
+ global buildings_gdf, highways_gdf, grid_gdf, CURRENT_SESSION_DIR
176
+
177
+ try:
178
+ if buildings_file is None or highways_file is None:
179
+ return "❌ Por favor, carregue ambos os arquivos (edifícios e ruas)", None
180
+
181
+ # reset de sessão a cada novo carregamento (para zip limpo)
182
+ CURRENT_SESSION_DIR = None
183
+ grid_gdf = None
184
+
185
+ buildings_gdf = gpd.read_file(buildings_file)
186
+ highways_gdf = gpd.read_file(highways_file)
187
+
188
+ # Reprojetar para UTM (mantido)
189
+ buildings_gdf = buildings_gdf.to_crs(epsg=32632)
190
+ highways_gdf = highways_gdf.to_crs(epsg=32632)
191
+
192
+ # Calcular métricas básicas (mantido)
193
+ buildings_gdf['area_m2'] = buildings_gdf.geometry.area
194
+ buildings_gdf['perimeter_m'] = buildings_gdf.geometry.length
195
+ highways_gdf['length_m'] = highways_gdf.geometry.length
196
+
197
+ msg = f"""✓ Dados carregados com sucesso!
198
+
199
+ 📊 EDIFÍCIOS: {len(buildings_gdf):,} registros
200
+ • Área total: {buildings_gdf['area_m2'].sum():,.0f} m²
201
+ • Área média: {buildings_gdf['area_m2'].mean():.0f} m²
202
+
203
+ 📊 RUAS: {len(highways_gdf):,} registros
204
+ • Comprimento total: {highways_gdf['length_m'].sum():,.0f} m ({highways_gdf['length_m'].sum()/1000:,.1f} km)
205
+ • Comprimento médio: {highways_gdf['length_m'].mean():.0f} m
206
+
207
+ 📍 Sistema de Coordenadas: {buildings_gdf.crs}
208
+ ⏰ Data de Carregamento: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
209
+ """
210
+
211
+ # salvar outputs base
212
+ _save_text("status_carregamento.txt", msg)
213
+ return msg
214
+ except Exception as e:
215
+ return f"❌ Erro ao carregar dados: {str(e)}"
216
+
217
+ def exploratory_analysis():
218
+ """Análise exploratória básica"""
219
+ global buildings_gdf, highways_gdf
220
+
221
+ if buildings_gdf is None or highways_gdf is None:
222
+ return "❌ Carregue os dados primeiro"
223
+
224
+ try:
225
+ report = f"""
226
+ ╔════════════════════════════════════════════════════════════════╗
227
+ ║ ANÁLISE EXPLORATÓRIA - EDIFÍCIOS ║
228
+ ╚════════════════════════════════════════════════════════════════╝
229
+
230
+ 📐 ÁREA DOS EDIFÍCIOS (m²):
231
+ Total: {buildings_gdf['area_m2'].sum():,.0f} m²
232
+ Média: {buildings_gdf['area_m2'].mean():,.0f} m²
233
+ Mediana: {buildings_gdf['area_m2'].median():,.0f} m²
234
+ Mínima: {buildings_gdf['area_m2'].min():,.0f} m²
235
+ Máxima: {buildings_gdf['area_m2'].max():,.0f} m²
236
+ Desvio Padrão: {buildings_gdf['area_m2'].std():,.0f} m²
237
+ Q1 (25%): {buildings_gdf['area_m2'].quantile(0.25):,.0f} m²
238
+ Q3 (75%): {buildings_gdf['area_m2'].quantile(0.75):,.0f} m²
239
+
240
+ 📏 PERÍMETRO DOS EDIFÍCIOS (m):
241
+ Média: {buildings_gdf['perimeter_m'].mean():,.0f} m
242
+ Mediana: {buildings_gdf['perimeter_m'].median():,.0f} m
243
+ Máxima: {buildings_gdf['perimeter_m'].max():,.0f} m
244
+
245
+ ╔════════════════════════════════════════════════════════════════╗
246
+ ║ ANÁLISE EXPLORATÓRIA - RUAS ║
247
+ ╚════════════════════════════════════════════════════════════════╝
248
+
249
+ 🛣️ COMPRIMENTO DAS RUAS (m):
250
+ Total: {highways_gdf['length_m'].sum():,.0f} m ({highways_gdf['length_m'].sum()/1000:,.1f} km)
251
+ Média: {highways_gdf['length_m'].mean():,.0f} m
252
+ Mediana: {highways_gdf['length_m'].median():,.0f} m
253
+ Mínima: {highways_gdf['length_m'].min():,.0f} m
254
+ Máxima: {highways_gdf['length_m'].max():,.0f} m
255
+ Desvio Padrão: {highways_gdf['length_m'].std():,.0f} m
256
+ Q1 (25%): {highways_gdf['length_m'].quantile(0.25):,.0f} m
257
+ Q3 (75%): {highways_gdf['length_m'].quantile(0.75):,.0f} m
258
+
259
+ ╔════════════════════════════════════════════════════════════════╗
260
+ ║ TIPOS DE EDIFÍCIOS (TOP 15) ║
261
+ ╚════════════════════════════════════════════════════════════════╝
262
+ """
263
+
264
+ if 'building' in buildings_gdf.columns:
265
+ building_types = buildings_gdf['building'].value_counts().head(15)
266
+ for i, (btype, count) in enumerate(building_types.items(), 1):
267
+ pct = (count / len(buildings_gdf)) * 100
268
+ bar = "█" * int(pct / 2)
269
+ report += f"\n{i:2d}. {str(btype):20s}: {count:6d} ({pct:5.1f}%) {bar}"
270
+
271
+ report += f"""
272
+
273
+ ╔════════════════════════════════════════════════════════════════╗
274
+ ║ TIPOS DE RUAS (TOP 15) ║
275
+ ╚════════════════════════════════════════════════════════════════╝
276
+ """
277
+
278
+ if 'highway' in highways_gdf.columns:
279
+ highway_types = highways_gdf['highway'].value_counts().head(15)
280
+ for i, (htype, count) in enumerate(highway_types.items(), 1):
281
+ pct = (count / len(highways_gdf)) * 100
282
+ bar = "█" * int(pct / 2)
283
+ report += f"\n{i:2d}. {str(htype):30s}: {count:6d} ({pct:5.1f}%) {bar}"
284
+
285
+ # salvar relatório
286
+ _save_text("relatorio_eda.txt", report)
287
+ return report
288
+ except Exception as e:
289
+ return f"❌ Erro na análise: {str(e)}"
290
+
291
+ def create_distributions():
292
+ """Cria gráficos de distribuição (com legendas)"""
293
+ global buildings_gdf, highways_gdf
294
+
295
+ if buildings_gdf is None or highways_gdf is None:
296
+ return None
297
+
298
+ try:
299
+ fig, axes = plt.subplots(2, 2, figsize=(14, 10))
300
+ fig.suptitle('Distribuições - Edifícios e Ruas', fontsize=16, fontweight='bold')
301
+
302
+ # Histograma área
303
+ axes[0, 0].hist(buildings_gdf['area_m2'], bins=50, edgecolor='black', alpha=0.7, color='steelblue', label='Edifícios (área)')
304
+ axes[0, 0].set_xlabel('Área (m²)')
305
+ axes[0, 0].set_ylabel('Frequência')
306
+ axes[0, 0].set_title('Distribuição de Áreas de Edifícios')
307
+ axes[0, 0].set_yscale('log')
308
+ axes[0, 0].grid(True, alpha=0.3)
309
+ axes[0, 0].legend(loc='upper right')
310
+
311
+ # Boxplot área
312
+ buildings_gdf.boxplot(column='area_m2', ax=axes[0, 1])
313
+ axes[0, 1].set_ylabel('Área (m²)')
314
+ axes[0, 1].set_title('Box Plot - Áreas de Edifícios')
315
+ axes[0, 1].set_yscale('log')
316
+ axes[0, 1].grid(True, alpha=0.3)
317
+ # legenda simples
318
+ axes[0, 1].plot([], [], label='Edifícios (área)', color='black')
319
+ axes[0, 1].legend(loc='upper right')
320
+
321
+ # Histograma ruas
322
+ axes[1, 0].hist(highways_gdf['length_m'], bins=50, edgecolor='black', alpha=0.7, color='coral', label='Ruas (comprimento)')
323
+ axes[1, 0].set_xlabel('Comprimento (m)')
324
+ axes[1, 0].set_ylabel('Frequência')
325
+ axes[1, 0].set_title('Distribuição de Comprimentos de Ruas')
326
+ axes[1, 0].set_yscale('log')
327
+ axes[1, 0].grid(True, alpha=0.3)
328
+ axes[1, 0].legend(loc='upper right')
329
+
330
+ # Boxplot ruas
331
+ highways_gdf.boxplot(column='length_m', ax=axes[1, 1])
332
+ axes[1, 1].set_ylabel('Comprimento (m)')
333
+ axes[1, 1].set_title('Box Plot - Comprimentos de Ruas')
334
+ axes[1, 1].set_yscale('log')
335
+ axes[1, 1].grid(True, alpha=0.3)
336
+ axes[1, 1].plot([], [], label='Ruas (comprimento)', color='black')
337
+ axes[1, 1].legend(loc='upper right')
338
+
339
+ plt.tight_layout()
340
+
341
+ # Salvar em arquivo temporário e copiar para sessão
342
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
343
+ plt.savefig(tmp.name, format='png', dpi=120, bbox_inches='tight')
344
+ plt.close()
345
+ out_path = _copy_into_session(tmp.name, "graficos_distribuicoes.png")
346
+
347
+ return out_path
348
+ except Exception as e:
349
+ print(f"Erro: {e}")
350
+ return None
351
+
352
+ def create_types_charts():
353
+ """Cria gráficos de tipos (com legendas)"""
354
+ global buildings_gdf, highways_gdf
355
+
356
+ if buildings_gdf is None or highways_gdf is None:
357
+ return None
358
+
359
+ try:
360
+ fig, axes = plt.subplots(1, 2, figsize=(16, 6))
361
+ fig.suptitle('Tipos de Edifícios e Ruas', fontsize=16, fontweight='bold')
362
+
363
+ if 'building' in buildings_gdf.columns:
364
+ building_types = buildings_gdf['building'].value_counts().head(15)
365
+ building_types.plot(kind='barh', ax=axes[0], color='steelblue', label='Quantidade')
366
+ axes[0].set_xlabel('Quantidade')
367
+ axes[0].set_title('Top 15 Tipos de Edifícios')
368
+ axes[0].grid(True, alpha=0.3, axis='x')
369
+ axes[0].legend(loc='lower right')
370
+
371
+ if 'highway' in highways_gdf.columns:
372
+ highway_types = highways_gdf['highway'].value_counts().head(15)
373
+ highway_types.plot(kind='barh', ax=axes[1], color='coral', label='Quantidade')
374
+ axes[1].set_xlabel('Quantidade')
375
+ axes[1].set_title('Top 15 Tipos de Ruas')
376
+ axes[1].grid(True, alpha=0.3, axis='x')
377
+ axes[1].legend(loc='lower right')
378
+
379
+ plt.tight_layout()
380
+
381
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
382
+ plt.savefig(tmp.name, format='png', dpi=120, bbox_inches='tight')
383
+ plt.close()
384
+ out_path = _copy_into_session(tmp.name, "graficos_tipos.png")
385
+
386
+ return out_path
387
+ except Exception as e:
388
+ print(f"Erro: {e}")
389
+ return None
390
+
391
+
392
+ def spatial_analysis():
393
+ """Análise espacial com clustering + gráficos explicativos"""
394
+ global buildings_gdf, highways_gdf, grid_gdf
395
+
396
+ if buildings_gdf is None or highways_gdf is None:
397
+ return "❌ Carregue os dados primeiro"
398
+
399
+ try:
400
+ # Grid
401
+ grid_size = 500
402
+ minx, miny, maxx, maxy = buildings_gdf.total_bounds
403
+ cols = np.arange(minx, maxx + grid_size, grid_size)
404
+ rows = np.arange(miny, maxy + grid_size, grid_size)
405
+
406
+ cells = []
407
+ cell_ids = []
408
+ cell_id = 0
409
+ from shapely.geometry import box
410
+ for i in range(len(cols) - 1):
411
+ for j in range(len(rows) - 1):
412
+ cells.append(box(cols[i], rows[j], cols[i + 1], rows[j + 1]))
413
+ cell_ids.append(cell_id)
414
+ cell_id += 1
415
+
416
+ grid_gdf = gpd.GeoDataFrame({"cell_id": cell_ids, "geometry": cells}, crs=buildings_gdf.crs)
417
+ buildings_in_grid = gpd.sjoin(buildings_gdf, grid_gdf, how="left", predicate="intersects")
418
+
419
+ building_counts = buildings_in_grid.groupby("cell_id").size()
420
+ grid_gdf["building_count"] = grid_gdf["cell_id"].map(building_counts).fillna(0).astype(int)
421
+
422
+ cell_area_ha = (grid_size * grid_size) / 10000.0 # hectares
423
+ grid_gdf["building_density"] = grid_gdf["building_count"] / cell_area_ha
424
+
425
+ grid_gdf_active = grid_gdf[grid_gdf["building_count"] > 0].copy()
426
+
427
+ # Clustering (usa centroides em WGS84 para estabilidade visual)
428
+ b_wgs = buildings_gdf.to_crs(epsg=4326).copy()
429
+ coords = np.column_stack([b_wgs.geometry.centroid.y.values, b_wgs.geometry.centroid.x.values])
430
+
431
+ scaler = StandardScaler()
432
+ coords_scaled = scaler.fit_transform(coords)
433
+
434
+ kmeans = KMeans(n_clusters=5, random_state=42, n_init=10)
435
+ buildings_gdf["cluster_kmeans"] = kmeans.fit_predict(coords_scaled)
436
+
437
+ dbscan = DBSCAN(eps=0.05, min_samples=10)
438
+ buildings_gdf["cluster_dbscan"] = dbscan.fit_predict(coords_scaled)
439
+
440
+ n_clusters_dbscan = len(set(buildings_gdf["cluster_dbscan"])) - (1 if -1 in buildings_gdf["cluster_dbscan"] else 0)
441
+ n_noise = int((buildings_gdf["cluster_dbscan"] == -1).sum())
442
+
443
+ report = f"""
444
+ ╔════════════════════════════════════════════════════════════════╗
445
+ ║ ANÁLISE ESPACIAL - GRID DE DENSIDADE ║
446
+ ╚════════════════════════════════════════════════════════════════╝
447
+
448
+ 📊 GRID (500m x 500m):
449
+ Total de células: {len(grid_gdf)}
450
+ Células com edifícios: {len(grid_gdf_active)}
451
+ Cobertura: {(len(grid_gdf_active)/len(grid_gdf)*100):.1f}%
452
+
453
+ 📈 DENSIDADE DE EDIFÍCIOS (edifícios/hectare):
454
+ Média: {grid_gdf_active['building_density'].mean():.2f}
455
+ Mediana: {grid_gdf_active['building_density'].median():.2f}
456
+ Máxima: {grid_gdf_active['building_density'].max():.2f}
457
+ Mínima: {grid_gdf_active['building_density'].min():.2f}
458
+
459
+ ╔════════════════════════════════════════════════════════════════╗
460
+ ║ ANÁLISE ESPACIAL - CLUSTERING K-MEANS ║
461
+ ╚════════════════════════════════════════════════════════════��═══╝
462
+
463
+ 🎯 CLUSTERING K-MEANS (k=5):
464
+ Total de edifícios: {len(buildings_gdf)}
465
+ Clusters: {buildings_gdf['cluster_kmeans'].nunique()}
466
+
467
+ Distribuição por cluster (K-Means):
468
+ """
469
+
470
+ cluster_counts = buildings_gdf["cluster_kmeans"].value_counts().sort_index()
471
+ total_buildings = len(buildings_gdf)
472
+
473
+ for cluster, count in cluster_counts.items():
474
+ pct = (count / total_buildings) * 100
475
+ bar = "█" * int(pct / 2)
476
+ # tenta usar área se existir
477
+ area = 0.0
478
+ try:
479
+ area = float(buildings_gdf.loc[buildings_gdf["cluster_kmeans"] == cluster, "area_m2"].sum())
480
+ except Exception:
481
+ area = 0.0
482
+ report += f"\n Cluster {cluster}: {count:6d} edifícios ({pct:5.1f}%) {bar}"
483
+ if area > 0:
484
+ report += f" - {area:,.0f} m² de área"
485
+
486
+ report += f"""
487
+
488
+ ╔════════════════════════════════════════════════════════════════╗
489
+ ║ ANÁLISE ESPACIAL - CLUSTERING DBSCAN ║
490
+ ╚════════════════════════════════════════════════════════════════╝
491
+
492
+ 🎯 CLUSTERING DBSCAN:
493
+ Clusters encontrados: {n_clusters_dbscan}
494
+ Pontos de ruído: {n_noise} ({(n_noise/len(buildings_gdf))*100:.1f}%)
495
+ """
496
+
497
+ # === Gráficos (para explicar os dados) ===
498
+ fig = plt.figure(figsize=(12, 8))
499
+ ax1 = fig.add_subplot(2, 2, 1)
500
+ ax2 = fig.add_subplot(2, 2, 2)
501
+ ax3 = fig.add_subplot(2, 2, 3)
502
+ ax4 = fig.add_subplot(2, 2, 4)
503
+
504
+ dens = grid_gdf_active["building_density"].replace([np.inf, -np.inf], np.nan).dropna()
505
+ ax1.hist(dens, bins=30)
506
+ ax1.set_title("Distribuição da densidade (edifícios/ha)")
507
+ ax1.set_xlabel("edifícios/ha")
508
+ ax1.set_ylabel("freq.")
509
+ ax1.grid(True, alpha=0.3)
510
+
511
+ top = grid_gdf_active.sort_values("building_density", ascending=False).head(10)
512
+ ax2.bar(top["cell_id"].astype(str), top["building_density"])
513
+ ax2.set_title("Top 10 células por densidade")
514
+ ax2.set_xlabel("cell_id")
515
+ ax2.set_ylabel("edifícios/ha")
516
+ ax2.tick_params(axis="x", rotation=45)
517
+ ax2.grid(True, alpha=0.3)
518
+
519
+ km_counts = buildings_gdf["cluster_kmeans"].value_counts().sort_index()
520
+ ax3.bar(km_counts.index.astype(str), km_counts.values)
521
+ ax3.set_title("K-Means: contagem por cluster")
522
+ ax3.set_xlabel("cluster")
523
+ ax3.set_ylabel("n edifícios")
524
+ ax3.grid(True, alpha=0.3)
525
+
526
+ db_counts = buildings_gdf["cluster_dbscan"].value_counts().sort_index()
527
+ ax4.bar(db_counts.index.astype(str), db_counts.values)
528
+ ax4.set_title("DBSCAN: contagem por rótulo (-1 = ruído)")
529
+ ax4.set_xlabel("rótulo")
530
+ ax4.set_ylabel("n edifícios")
531
+ ax4.grid(True, alpha=0.3)
532
+
533
+ fig.tight_layout()
534
+
535
+ # Salva outputs adicionais
536
+ _save_text("relatorio_espacial.txt", report)
537
+ try:
538
+ session_dir = _ensure_session_dir()
539
+ fig_path = session_dir / "graficos_analise_espacial.png"
540
+ fig.savefig(fig_path, dpi=160, bbox_inches="tight")
541
+ except Exception:
542
+ pass
543
+
544
+ # Salva GPKG correspondentes (objetos que originam mapas)
545
+ try:
546
+ _save_gpkg("grid_500m.gpkg", grid_gdf, layer="grid")
547
+ except Exception:
548
+ pass
549
+ try:
550
+ _save_gpkg("edificios_com_clusters.gpkg", buildings_gdf, layer="buildings")
551
+ except Exception:
552
+ pass
553
+
554
+ return report, fig
555
+ except Exception as e:
556
+ return f"❌ Erro na análise espacial: {str(e)}", None
557
+
558
+
559
+ def create_heatmap():
560
+ """Cria mapa de calor (exibido via iframe + salvo)"""
561
+ global buildings_gdf
562
+
563
+ if buildings_gdf is None:
564
+ return None, None, None
565
+
566
+ try:
567
+ buildings_wgs84 = buildings_gdf.to_crs(epsg=4326)
568
+ center_lat = buildings_wgs84.geometry.centroid.y.mean()
569
+ center_lon = buildings_wgs84.geometry.centroid.x.mean()
570
+
571
+ # GPKG base: pontos (WGS84) + colunas de cluster (se existirem)
572
+ kmeans_gdf = buildings_wgs84.copy()
573
+ dbscan_gdf = buildings_wgs84.copy()
574
+ if 'cluster_kmeans' in buildings_gdf.columns and 'cluster_kmeans' not in kmeans_gdf.columns:
575
+ kmeans_gdf['cluster_kmeans'] = buildings_gdf['cluster_kmeans'].values
576
+ if 'cluster_dbscan' in buildings_gdf.columns and 'cluster_dbscan' not in dbscan_gdf.columns:
577
+ dbscan_gdf['cluster_dbscan'] = buildings_gdf['cluster_dbscan'].values
578
+
579
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap')
580
+
581
+ heat_data = []
582
+ for _, row in buildings_wgs84.iterrows():
583
+ centroid = row.geometry.centroid
584
+ heat_data.append([centroid.y, centroid.x])
585
+
586
+ plugins.HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
587
+
588
+ # legenda
589
+ _add_folium_legend(
590
+ m,
591
+ "Mapa de Calor (Edifícios)",
592
+ [("Intensidade = concentração de pontos", "#ef4444")]
593
+ )
594
+
595
+ # salvar html
596
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp:
597
+ m.save(tmp.name)
598
+ html_content = Path(tmp.name).read_text(encoding="utf-8")
599
+ out_path = _copy_into_session(tmp.name, "mapa_heatmap.html")
600
+ # Salva GPKG correspondente (pontos usados no heatmap)
601
+ gpkg_path = None
602
+ try:
603
+ gpkg_path = _save_gpkg("heatmap_pontos.gpkg", buildings_wgs84, layer="heatmap_points")
604
+ except Exception:
605
+ gpkg_path = None
606
+
607
+ return _iframe_srcdoc(html_content, height=650), out_path, gpkg_path
608
+ except Exception as e:
609
+ print(f"Erro: {e}")
610
+ return None, None, None
611
+
612
+ def create_clustering_maps():
613
+ """Cria mapas de clustering (K-Means e DBSCAN) com legenda.
614
+ Observação: para performance, o mapa pode usar amostragem visual quando há muitos pontos.
615
+ Os GPKGs salvos contêm TODOS os pontos (sem amostragem).
616
+ """
617
+ global buildings_gdf
618
+
619
+ if buildings_gdf is None or len(buildings_gdf) == 0:
620
+ return None, None, None, None, None, None
621
+
622
+ try:
623
+ # Trabalha em WGS84 para mapas
624
+ buildings_wgs84 = buildings_gdf.to_crs(epsg=4326).copy()
625
+ # Centróides (uma vez)
626
+ centroids = buildings_wgs84.geometry.centroid
627
+ buildings_wgs84["_lat"] = centroids.y.values
628
+ buildings_wgs84["_lon"] = centroids.x.values
629
+
630
+ center_lat = float(buildings_wgs84["_lat"].mean())
631
+ center_lon = float(buildings_wgs84["_lon"].mean())
632
+
633
+ # Garante colunas de cluster (para o GPKG e para o mapa)
634
+ coords = np.column_stack([buildings_wgs84["_lat"].values, buildings_wgs84["_lon"].values])
635
+ coords_scaled = StandardScaler().fit_transform(coords)
636
+
637
+ if "cluster_kmeans" not in buildings_gdf.columns:
638
+ kmeans = KMeans(n_clusters=5, random_state=42, n_init=10)
639
+ buildings_gdf["cluster_kmeans"] = kmeans.fit_predict(coords_scaled)
640
+ if "cluster_dbscan" not in buildings_gdf.columns:
641
+ dbscan = DBSCAN(eps=0.05, min_samples=10)
642
+ buildings_gdf["cluster_dbscan"] = dbscan.fit_predict(coords_scaled)
643
+
644
+ # Copias em WGS84 (para salvar e para mapear)
645
+ kmeans_gdf = buildings_wgs84.copy()
646
+ dbscan_gdf = buildings_wgs84.copy()
647
+
648
+ kmeans_gdf["cluster_kmeans"] = buildings_gdf["cluster_kmeans"].values
649
+ dbscan_gdf["cluster_dbscan"] = buildings_gdf["cluster_dbscan"].values
650
+
651
+ # --------- AMOSTRAGEM VISUAL (para o HTML não travar) ----------
652
+ MAX_POINTS_MAP = 15000
653
+ def _sample_for_map(gdf, label_col=None):
654
+ if len(gdf) <= MAX_POINTS_MAP:
655
+ return gdf
656
+ if label_col and label_col in gdf.columns:
657
+ # amostragem estratificada por rótulo
658
+ parts = []
659
+ groups = gdf.groupby(label_col, dropna=False)
660
+ # distribui a cota proporcionalmente
661
+ for _, grp in groups:
662
+ n = max(1, int(len(grp) / len(gdf) * MAX_POINTS_MAP))
663
+ parts.append(grp.sample(n=min(n, len(grp)), random_state=42))
664
+ out = pd.concat(parts, ignore_index=True)
665
+ # se ainda excedeu, corta
666
+ if len(out) > MAX_POINTS_MAP:
667
+ out = out.sample(n=MAX_POINTS_MAP, random_state=42)
668
+ return out
669
+ # fallback: amostra simples
670
+ return gdf.sample(n=MAX_POINTS_MAP, random_state=42)
671
+
672
+ kmeans_map_gdf = _sample_for_map(kmeans_gdf, "cluster_kmeans")
673
+ dbscan_map_gdf = _sample_for_map(dbscan_gdf, "cluster_dbscan")
674
+
675
+ # ----------------- KMEANS MAP -----------------
676
+ m1 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="OpenStreetMap")
677
+ colors = ["red", "blue", "green", "purple", "orange"]
678
+
679
+ # usa loop apenas na amostra (mais leve)
680
+ for _, row in kmeans_map_gdf.iterrows():
681
+ cluster = int(row.get("cluster_kmeans", 0))
682
+ color = colors[cluster % len(colors)]
683
+ folium.CircleMarker(
684
+ location=[float(row["_lat"]), float(row["_lon"])],
685
+ radius=3,
686
+ color=color,
687
+ fill=True,
688
+ fillColor=color,
689
+ fillOpacity=0.6,
690
+ weight=1,
691
+ ).add_to(m1)
692
+
693
+ _add_folium_legend(
694
+ m1,
695
+ "K-Means (clusters)",
696
+ [(f"Cluster {i}", colors[i % len(colors)]) for i in range(5)],
697
+ )
698
+
699
+ # ----------------- DBSCAN MAP -----------------
700
+ m2 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="OpenStreetMap")
701
+
702
+ # nº clusters (ignorando ruído -1)
703
+ labels = dbscan_gdf["cluster_dbscan"].values
704
+ n_clusters_dbscan = len(set(labels)) - (1 if -1 in labels else 0)
705
+ cmap = plt.cm.tab20(np.linspace(0, 1, max(n_clusters_dbscan, 1)))
706
+
707
+ for _, row in dbscan_map_gdf.iterrows():
708
+ cluster = int(row.get("cluster_dbscan", -1))
709
+ if cluster == -1:
710
+ color = "gray"
711
+ opacity = 0.3
712
+ else:
713
+ color = mcolors.rgb2hex(cmap[cluster % max(n_clusters_dbscan, 1)])
714
+ opacity = 0.7
715
+
716
+ folium.CircleMarker(
717
+ location=[float(row["_lat"]), float(row["_lon"])],
718
+ radius=3,
719
+ color=color,
720
+ fill=True,
721
+ fillColor=color,
722
+ fillOpacity=opacity,
723
+ weight=1,
724
+ ).add_to(m2)
725
+
726
+ legend_items = [("Ruído (-1)", "gray")]
727
+ # amostra (até 8 itens) para não explodir a legenda
728
+ for i in range(min(n_clusters_dbscan, 8)):
729
+ legend_items.append((f"Cluster {i}", mcolors.rgb2hex(cmap[i % max(n_clusters_dbscan, 1)])))
730
+ _add_folium_legend(m2, "DBSCAN (clusters)", legend_items)
731
+
732
+ # salvar htmls (sempre copiando para a pasta da sessão)
733
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp1:
734
+ m1.save(tmp1.name)
735
+ kmeans_html = Path(tmp1.name).read_text(encoding="utf-8")
736
+ kmeans_file = _copy_into_session(tmp1.name, "mapa_kmeans.html")
737
+
738
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp2:
739
+ m2.save(tmp2.name)
740
+ dbscan_html = Path(tmp2.name).read_text(encoding="utf-8")
741
+ dbscan_file = _copy_into_session(tmp2.name, "mapa_dbscan.html")
742
+
743
+ # Salva GPKG correspondentes (TODOS os pontos + rótulos)
744
+ kmeans_gpkg = None
745
+ dbscan_gpkg = None
746
+ try:
747
+ kmeans_gpkg = _save_gpkg("kmeans_pontos_clusters.gpkg", kmeans_gdf.drop(columns=["_lat","_lon"], errors="ignore"), layer="kmeans")
748
+ except Exception:
749
+ kmeans_gpkg = None
750
+ try:
751
+ dbscan_gpkg = _save_gpkg("dbscan_pontos_clusters.gpkg", dbscan_gdf.drop(columns=["_lat","_lon"], errors="ignore"), layer="dbscan")
752
+ except Exception:
753
+ dbscan_gpkg = None
754
+
755
+ return kmeans_html, kmeans_file, dbscan_html, dbscan_file, kmeans_gpkg, dbscan_gpkg
756
+
757
+ except Exception as e:
758
+ print(f"Erro em create_clustering_maps: {e}")
759
+ return None, None, None, None, None, None
760
+ def create_grid_maps():
761
+ """Cria mapas com grid (exibidos via iframe + salvos)"""
762
+ global buildings_gdf, highways_gdf, grid_gdf
763
+
764
+ if buildings_gdf is None or grid_gdf is None or highways_gdf is None:
765
+ return None, None, None, None, None, None
766
+
767
+ try:
768
+ grid_wgs84 = grid_gdf.to_crs(epsg=4326)
769
+ buildings_wgs84 = buildings_gdf.to_crs(epsg=4326)
770
+ highways_wgs84 = highways_gdf.to_crs(epsg=4326)
771
+
772
+ center_lat = buildings_wgs84.geometry.centroid.y.mean()
773
+ center_lon = buildings_wgs84.geometry.centroid.x.mean()
774
+
775
+ # Mapa 1: Densidade de edifícios
776
+ m1 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap')
777
+
778
+ # bins e legenda (mantém suas cores, só adiciona legenda)
779
+ legend_buildings = [
780
+ ("0", "white"),
781
+ ("< 2", "#ffffcc"),
782
+ ("2–4", "#ffeda0"),
783
+ ("4–6", "#fed976"),
784
+ ("6–8", "#feb24c"),
785
+ ("8–10", "#fd8d3c"),
786
+ (">= 10", "#e31a1c"),
787
+ ]
788
+
789
+ for idx, row in grid_wgs84.iterrows():
790
+ density = float(grid_gdf.loc[idx, 'building_density'])
791
+
792
+ if density == 0:
793
+ color = 'white'
794
+ opacity = 0
795
+ elif density < 2:
796
+ color = '#ffffcc'
797
+ opacity = 0.3
798
+ elif density < 4:
799
+ color = '#ffeda0'
800
+ opacity = 0.4
801
+ elif density < 6:
802
+ color = '#fed976'
803
+ opacity = 0.5
804
+ elif density < 8:
805
+ color = '#feb24c'
806
+ opacity = 0.6
807
+ elif density < 10:
808
+ color = '#fd8d3c'
809
+ opacity = 0.7
810
+ else:
811
+ color = '#e31a1c'
812
+ opacity = 0.8
813
+
814
+ folium.GeoJson(
815
+ data=row.geometry.__geo_interface__,
816
+ style_function=lambda _, color=color, opacity=opacity: {
817
+ 'fillColor': color,
818
+ 'color': 'black',
819
+ 'weight': 0.5,
820
+ 'fillOpacity': opacity
821
+ }
822
+ ).add_to(m1)
823
+
824
+ _add_folium_legend(m1, "Densidade de edifícios (por ha)", legend_buildings)
825
+
826
+ # Mapa 2: Densidade de ruas
827
+ m2 = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='OpenStreetMap')
828
+
829
+ roads_per_cell = []
830
+ for cell in grid_gdf.geometry:
831
+ roads_in_cell = highways_gdf[highways_gdf.geometry.intersects(cell)]
832
+ roads_per_cell.append(float(roads_in_cell['length_m'].sum()) if len(roads_in_cell) > 0 else 0.0)
833
+
834
+ grid_gdf['road_density'] = np.array(roads_per_cell) / (500 ** 2) * 10000
835
+ max_road_density = float(grid_gdf['road_density'].max()) if float(grid_gdf['road_density'].max()) > 0 else 1.0
836
+
837
+ legend_roads = [
838
+ ("0", "white"),
839
+ ("Baixa", "#f7fbff"),
840
+ ("Média-baixa", "#deebf7"),
841
+ ("Média", "#9ecae1"),
842
+ ("Média-alta", "#3182bd"),
843
+ ("Alta", "#08519c"),
844
+ ]
845
+
846
+ for idx, row in grid_wgs84.iterrows():
847
+ road_density = float(grid_gdf.loc[idx, 'road_density'])
848
+
849
+ if road_density == 0:
850
+ color = 'white'
851
+ opacity = 0
852
+ else:
853
+ intensity = road_density / max_road_density
854
+ if intensity < 0.2:
855
+ color = '#f7fbff'
856
+ elif intensity < 0.4:
857
+ color = '#deebf7'
858
+ elif intensity < 0.6:
859
+ color = '#9ecae1'
860
+ elif intensity < 0.8:
861
+ color = '#3182bd'
862
+ else:
863
+ color = '#08519c'
864
+ opacity = 0.7
865
+
866
+ folium.GeoJson(
867
+ data=row.geometry.__geo_interface__,
868
+ style_function=lambda _, color=color, opacity=opacity: {
869
+ 'fillColor': color,
870
+ 'color': 'black',
871
+ 'weight': 0.5,
872
+ 'fillOpacity': opacity
873
+ }
874
+ ).add_to(m2)
875
+
876
+ _add_folium_legend(m2, "Densidade de ruas (m/ha)", legend_roads)
877
+
878
+ # salvar htmls + copiar p/ sessão
879
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp1:
880
+ m1.save(tmp1.name)
881
+ grid_html = Path(tmp1.name).read_text(encoding="utf-8")
882
+ grid_file = _copy_into_session(tmp1.name, "grid_densidade_edificios.html")
883
+
884
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp2:
885
+ m2.save(tmp2.name)
886
+ roads_html = Path(tmp2.name).read_text(encoding="utf-8")
887
+ roads_file = _copy_into_session(tmp2.name, "grid_densidade_ruas.html")
888
+
889
+ # Salva GPKG correspondentes (grid + métricas)
890
+ # NOTE: o usuário quer sempre o GPKG correspondente a cada mapa.
891
+ # Aqui salvamos o grid com as métricas calculadas.
892
+ grid_gpkg = None
893
+ roads_gpkg = None
894
+ try:
895
+ grid_buildings = grid_gdf[["building_density", "geometry"]].copy()
896
+ grid_gpkg = _save_gpkg("grid_densidade_edificios.gpkg", grid_buildings, layer="grid_buildings")
897
+ except Exception:
898
+ grid_gpkg = None
899
+ try:
900
+ grid_roads = grid_gdf[["road_density", "geometry"]].copy()
901
+ roads_gpkg = _save_gpkg("grid_densidade_ruas.gpkg", grid_roads, layer="grid_roads")
902
+ except Exception:
903
+ roads_gpkg = None
904
+
905
+ return grid_html, grid_file, roads_html, roads_file, grid_gpkg, roads_gpkg
906
+ except Exception as e:
907
+ print(f"Erro: {e}")
908
+ return None, None, None, None, None, None
909
+
910
+ def get_kmeans_data():
911
+ kmeans_html, kmeans_file, _, _, kmeans_gpkg, _ = create_clustering_maps()
912
+ if kmeans_html is None:
913
+ return None, None, None
914
+ return _iframe_srcdoc(kmeans_html, height=650), kmeans_file, kmeans_gpkg
915
+
916
+ def get_dbscan_data():
917
+ _, _, dbscan_html, dbscan_file, _, dbscan_gpkg = create_clustering_maps()
918
+ if dbscan_html is None:
919
+ return None, None, None
920
+ return _iframe_srcdoc(dbscan_html, height=650), dbscan_file, dbscan_gpkg
921
+
922
+ def get_grid_data():
923
+ grid_html, grid_file, roads_html, roads_file, grid_gpkg, roads_gpkg = create_grid_maps()
924
+ if grid_html is None:
925
+ return None, None, None, None, None, None
926
+ return (
927
+ _iframe_srcdoc(grid_html, height=650), grid_file, grid_gpkg,
928
+ _iframe_srcdoc(roads_html, height=650), roads_file, roads_gpkg
929
+ )
930
+
931
+ def get_zip_download():
932
+ """Gera/atualiza o ZIP sob demanda."""
933
+ msg, zp = create_zip_for_download()
934
+ return zp
935
+
936
+ # ============================================================================
937
+ # INTERFACE GRADIO
938
+ # ============================================================================
939
+
940
+ with gr.Blocks(title="Análise Geoespacial", theme=gr.themes.Soft()) as demo:
941
+ gr.Markdown("# 🗺️ Análise Geoespacial - Edifícios e Ruas")
942
+ gr.Markdown("**Aplicação para análise exploratória de dados geoespaciais**")
943
+
944
+ # Download do ZIP sempre visível
945
+ with gr.Row():
946
+ zip_btn = gr.Button("📦 Gerar/Atualizar ZIP (tudo)", variant="primary")
947
+ zip_file = gr.File(label="⬇️ Download ZIP (tudo)")
948
+
949
+ zip_btn.click(fn=get_zip_download, outputs=zip_file)
950
+
951
+ with gr.Tab("📁 Upload de Dados"):
952
+ gr.Markdown("## Carregue seus arquivos GPKG")
953
+
954
+ with gr.Row():
955
+ buildings_file = gr.File(label="📦 Arquivo de Edifícios (GPKG)", file_types=[".gpkg"])
956
+ highways_file = gr.File(label="📦 Arquivo de Ruas (GPKG)", file_types=[".gpkg"])
957
+
958
+ load_btn = gr.Button("🔄 Carregar Dados", variant="primary", size="lg")
959
+ status_output = gr.Textbox(label="Status", lines=12, interactive=False)
960
+
961
+ load_btn.click(
962
+ fn=load_data,
963
+ inputs=[buildings_file, highways_file],
964
+ outputs=status_output
965
+ )
966
+
967
+ with gr.Tab("📊 Análise Exploratória"):
968
+ gr.Markdown("## Estatísticas Descritivas e Visualizações")
969
+
970
+ with gr.Row():
971
+ eda_btn = gr.Button("📈 Gerar Análise Exploratória", variant="primary", size="lg")
972
+
973
+ eda_output = gr.Textbox(label="Relatório EDA", lines=35, interactive=False)
974
+
975
+ eda_btn.click(fn=exploratory_analysis, outputs=eda_output)
976
+
977
+ gr.Markdown("## Visualizações")
978
+
979
+ with gr.Row():
980
+ dist_btn = gr.Button("📊 Distribuições", size="lg")
981
+ types_btn = gr.Button("📊 Tipos", size="lg")
982
+
983
+ with gr.Row():
984
+ dist_output = gr.Image(label="Gráficos de Distribuição")
985
+ types_output = gr.Image(label="Tipos de Edifícios e Ruas")
986
+
987
+ # Atualiza também o ZIP a cada geração
988
+ dist_btn.click(fn=create_distributions, outputs=dist_output)
989
+ types_btn.click(fn=create_types_charts, outputs=types_output)
990
+
991
+ with gr.Tab("🎯 Análise Espacial"):
992
+ gr.Markdown("## Clustering e Densidade")
993
+
994
+ spatial_btn = gr.Button("🔍 Análise Espacial", variant="primary", size="lg")
995
+ spatial_output = gr.Textbox(label="Relatório Espacial", lines=35, interactive=False)
996
+ spatial_plot = gr.Plot(label="📊 Gráficos - Análise Espacial")
997
+
998
+ spatial_btn.click(fn=spatial_analysis, outputs=[spatial_output, spatial_plot])
999
+
1000
+ with gr.Tab("🗺️ Mapas Interativos"):
1001
+ gr.Markdown("## Visualizações Geoespaciais")
1002
+
1003
+ with gr.Row():
1004
+ heatmap_btn = gr.Button("🔥 Mapa de Calor", size="lg")
1005
+ kmeans_btn = gr.Button("🎯 K-Means", size="lg")
1006
+ dbscan_btn = gr.Button("🎯 DBSCAN", size="lg")
1007
+
1008
+ 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")
1009
+ heatmap_download = gr.File(label="⬇️ Download Heatmap (HTML)")
1010
+ heatmap_gpkg = gr.File(label="⬇️ GPKG Heatmap (pontos)")
1011
+
1012
+ 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")
1013
+ kmeans_download = gr.File(label="⬇️ Download K-Means (HTML)")
1014
+ kmeans_gpkg = gr.File(label="⬇️ GPKG K-Means (pontos+clusters)")
1015
+
1016
+ 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")
1017
+ dbscan_download = gr.File(label="⬇️ Download DBSCAN (HTML)")
1018
+ dbscan_gpkg = gr.File(label="⬇️ GPKG DBSCAN (pontos+rótulos)")
1019
+
1020
+ heatmap_btn.click(fn=create_heatmap, outputs=[heatmap_output, heatmap_download, heatmap_gpkg])
1021
+ kmeans_btn.click(fn=get_kmeans_data, outputs=[kmeans_output, kmeans_download, kmeans_gpkg])
1022
+ dbscan_btn.click(fn=get_dbscan_data, outputs=[dbscan_output, dbscan_download, dbscan_gpkg])
1023
+
1024
+ with gr.Tab("🔲 Mapas com Grid"):
1025
+ gr.Markdown("## Análise de Densidade com Grid (500m x 500m)")
1026
+
1027
+ with gr.Row():
1028
+ grid_btn = gr.Button("📊 Gerar Mapas com Grid", variant="primary", size="lg")
1029
+
1030
+ # Grid 1: Edifícios
1031
+ grid_output = gr.HTML(
1032
+ 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>",
1033
+ label="Grid de Densidade - Edifícios"
1034
+ )
1035
+ with gr.Row():
1036
+ grid_download = gr.File(label="⬇️ Download Grid Edifícios (HTML)", interactive=False)
1037
+ grid_gpkg = gr.File(label="⬇️ Download GPKG Grid Edifícios", interactive=False)
1038
+
1039
+ # Grid 2: Ruas
1040
+ roads_output = gr.HTML(
1041
+ 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>",
1042
+ label="Grid de Densidade - Ruas"
1043
+ )
1044
+ with gr.Row():
1045
+ roads_download = gr.File(label="⬇️ Download Grid Ruas (HTML)", interactive=False)
1046
+ roads_gpkg = gr.File(label="⬇️ Download GPKG Grid Ruas", interactive=False)
1047
+
1048
+ grid_btn.click(
1049
+ fn=get_grid_data,
1050
+ outputs=[grid_output, grid_download, grid_gpkg, roads_output, roads_download, roads_gpkg]
1051
+ )
1052
+
1053
+ with gr.Tab("ℹ️ Sobre"):
1054
+ gr.Markdown("""
1055
+ ## 📖 Sobre esta Aplicação
1056
+
1057
+ Aplicação para **análise geoespacial exploratória (EDA) completa** sobre dados de edifícios e ruas.
1058
+
1059
+ ### ✨ Funcionalidades:
1060
+ - ✅ Upload de arquivos GPKG
1061
+ - ✅ Análise exploratória com estatísticas descritivas
1062
+ - ✅ Visualizações de distribuições **com legendas**
1063
+ - ✅ Análise espacial com clustering (K-Means e DBSCAN)
1064
+ - ✅ Mapas interativos **exibidos na interface** (heatmap, clustering) + legendas
1065
+ - ✅ Mapas com Grid de Densidade (500m x 500m) **exibidos na interface** + legendas
1066
+ - ✅ Download consolidado de **tudo** em um ZIP
1067
+
1068
+ ### 🚀 Como Usar Localmente:
1069
+ ```bash
1070
+ python3.11 app_melhorado_v3.py
1071
+ ```
1072
+
1073
+ Depois acesse: http://localhost:7860
1074
+ """)
1075
+
1076
+ if __name__ == "__main__":
1077
+ print("=" * 80)
1078
+ print("🗺️ APLICAÇÃO GRADIO - ANÁLISE GEOESPACIAL (UI + ZIP)")
1079
+ print("=" * 80)
1080
+ print("\n✓ Iniciando aplicação...")
1081
+ print("✓ Acesse: http://localhost:7860")
1082
+ print("✓ Pressione CTRL+C para parar\n")
1083
+ # Em Hugging Face Spaces, a porta é fornecida via variável de ambiente PORT.
1084
+ server_port = int(os.environ.get('PORT', '7860'))
1085
+ demo.launch(server_name="0.0.0.0", server_port=server_port, share=False, allowed_paths=[str(OUTPUT_ROOT)])
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ geopandas
3
+ pandas
4
+ numpy
5
+ matplotlib
6
+ seaborn
7
+ folium
8
+ scikit-learn
9
+ shapely
10
+ pyproj
11
+ fiona
12
+ rtree