Geocode / app.py
fschwartzer's picture
Update app.py
754eaac verified
# -*- coding: utf-8 -*-
import gradio as gr
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point # Importar Point para a conversão
import folium
from folium.plugins import MeasureControl
import html
from rapidfuzz import process
# --- CARREGAMENTO E PRÉ-PROCESSAMENTO DE DADOS ---
try:
# Carrega os eixos (ruas) e projeta para o sistema de coordenadas padrão (WGS84)
gdf_eixos = gpd.read_file("Eixos.shp", engine="fiona")
gdf_eixos_proj = gdf_eixos.to_crs("EPSG:4326")
print("Shapefile de Eixos carregado com sucesso.")
# 1. Criar o dicionário de mapeamento unificado: NOME -> CDLOG
mapa_nome_para_cdlog = {}
df_mapeamento = gdf_eixos_proj[['NMIDELOG', 'NMIDEABR', 'CDLOG']].dropna(subset=['CDLOG'])
for _, row in df_mapeamento.iterrows():
cdlog = row['CDLOG']
if pd.notna(row['NMIDELOG']):
mapa_nome_para_cdlog[row['NMIDELOG']] = cdlog
if pd.notna(row['NMIDEABR']):
mapa_nome_para_cdlog[row['NMIDEABR']] = cdlog
# 2. Criar a lista de busca unificada com todos os nomes únicos
lista_nomes_ruas_oficiais = list(mapa_nome_para_cdlog.keys())
print(f"{len(lista_nomes_ruas_oficiais)} nomes de ruas únicos (de NMIDELOG e NMIDEABR) pré-processados para busca.")
except Exception as e:
print(f"ERRO CRÍTICO: Não foi possível carregar ou processar o Shapefile 'Eixos.shp'.")
print(f"Verifique se o caminho de rede está correto e acessível.")
print(f"Erro original: {e}")
exit()
# --- FUNÇÕES DE PROCESSAMENTO (BACK-END) ---
def encontrar_melhor_correspondencia(nome_rua_usuario, score_minimo=85):
"""
Encontra a melhor correspondência para um nome de rua na lista oficial.
A comparação é feita em CAIXA ALTA para ser insensível ao caso.
"""
if not nome_rua_usuario or pd.isna(nome_rua_usuario):
return None, None, 0
nome_rua_upper = str(nome_rua_usuario).upper()
melhor_match = process.extractOne(nome_rua_upper, lista_nomes_ruas_oficiais)
if melhor_match and melhor_match[1] >= score_minimo:
nome_encontrado = melhor_match[0]
score = melhor_match[1]
cdlog_encontrado = mapa_nome_para_cdlog[nome_encontrado]
return int(cdlog_encontrado), nome_encontrado, score
return None, None, melhor_match[1] if melhor_match else 0
def geocodificar_lote_por_nome(df, coluna_nome_rua, coluna_num):
"""
Função principal que processa o DataFrame inteiro.
"""
if df is None:
return None, None, None
resultados_finais = []
falhas_geocodificacao = []
for index, row in df.iterrows():
nome_rua_original = row[coluna_nome_rua]
numero = pd.to_numeric(row[coluna_num], errors='coerce')
dados_linha = row.to_dict()
cdlog, nome_correspondido, score = encontrar_melhor_correspondencia(nome_rua_original)
dados_linha['CDLOG_ENCONTRADO'] = cdlog
dados_linha['NM_CORRESPONDIDO'] = nome_correspondido
dados_linha['SCORE_SIMILARIDADE'] = score
if not cdlog:
falhas_geocodificacao.append({**dados_linha, 'MOTIVO_FALHA': 'Nome da rua não encontrado com similaridade aceitável'})
resultados_finais.append({**dados_linha, 'x': None, 'y': None})
continue
if pd.isna(numero):
falhas_geocodificacao.append({**dados_linha, 'MOTIVO_FALHA': 'Número do imóvel inválido ou vazio'})
resultados_finais.append({**dados_linha, 'x': None, 'y': None})
continue
numero = int(numero)
segmentos = gdf_eixos_proj[gdf_eixos_proj['CDLOG'] == cdlog]
cond = (segmentos['NRPARINI'] <= numero) & (segmentos['NRPARFIN'] >= numero) if numero % 2 == 0 else (segmentos['NRIMPINI'] <= numero) & (segmentos['NRIMPFIN'] >= numero)
segmento_valido = segmentos[cond]
if segmento_valido.empty:
falhas_geocodificacao.append({**dados_linha, 'MOTIVO_FALHA': 'Número do imóvel fora do intervalo da rua encontrada'})
resultados_finais.append({**dados_linha, 'x': None, 'y': None})
continue
linha_eixo = segmento_valido.iloc[0]
geom = linha_eixo.geometry
ini, fim = (linha_eixo['NRPARINI'], linha_eixo['NRPARFIN']) if numero % 2 == 0 else (linha_eixo['NRIMPINI'], linha_eixo['NRIMPFIN'])
if pd.isna(ini) or pd.isna(fim) or fim == ini: frac = 0.5
else: frac = (numero - ini) / (fim - ini)
frac = max(0, min(1, frac))
ponto = geom.interpolate(geom.length * frac)
dados_linha['x'] = ponto.x
dados_linha['y'] = ponto.y
resultados_finais.append(dados_linha)
df_resultado = pd.DataFrame(resultados_finais)
df_falhas = pd.DataFrame(falhas_geocodificacao)
output_path = "dados_geocodificados.xlsx"
df_resultado.to_excel(output_path, index=False)
return df_resultado, df_falhas, output_path
# --- FUNÇÃO DO MAPA CORRIGIDA ---
def gerar_mapa_interpolado(df_interpolado):
"""Gera o mapa HTML a partir do DataFrame com coordenadas."""
if df_interpolado is None:
return "<div>Mapa não gerado. Processe os dados primeiro.</div>"
df_valido = df_interpolado.dropna(subset=['x', 'y']).copy()
if df_valido.empty:
return "<div>Nenhum ponto válido para exibir no mapa.</div>"
# --- INÍCIO DA CORREÇÃO ---
# 1. Converter o DataFrame do pandas para um GeoDataFrame do geopandas
gdf_valido = gpd.GeoDataFrame(
df_valido,
geometry=gpd.points_from_xy(df_valido.x, df_valido.y),
crs="EPSG:4326" # Definir o sistema de coordenadas (importante!)
)
# 2. Calcular o centro do mapa de forma eficiente
# Pega os limites geográficos de todos os pontos (min_x, min_y, max_x, max_y)
bounds = gdf_valido.total_bounds
center_y = (bounds[1] + bounds[3]) / 2
center_x = (bounds[0] + bounds[2]) / 2
m = folium.Map(location=[center_y, center_x], zoom_start=13, tiles='CartoDB positron')
# --- FIM DA CORREÇÃO ---
MeasureControl(primary_length_unit='meters', secondary_length_unit='kilometers').add_to(m)
# O resto da função permanece igual
for _, row in gdf_valido.iterrows():
conteudo_popup = "<br>".join([f"<b>{col}</b>: {row[col]}" for col in gdf_valido.columns if col != 'geometry'])
folium.CircleMarker(
location=[row['y'], row['x']],
radius=5, color='#007BFF', fill=True, fill_color='#007BFF',
fill_opacity=0.8, popup=folium.Popup(conteudo_popup, max_width=350)
).add_to(m)
mapa_html = m.get_root().render()
mapa_html_escapado = html.escape(mapa_html)
return f"<iframe srcdoc=\"{mapa_html_escapado}\" width='100%' height='1000px' style='border:none;'></iframe>"
# --- Funções de Interface (Gradio) ---
def carregar_abas(arquivo_excel):
if arquivo_excel is None: return gr.update(choices=[]), None
xls = pd.ExcelFile(arquivo_excel.name)
abas = xls.sheet_names
return gr.update(choices=abas, value=abas[0] if abas else None), abas[0] if abas else None
def listar_colunas(arquivo_excel, aba_selecionada):
if arquivo_excel is None or aba_selecionada is None:
return gr.update(choices=[]), gr.update(choices=[]), None, "", None
df = pd.read_excel(arquivo_excel.name, sheet_name=aba_selecionada)
colunas = df.columns.tolist()
return (gr.update(choices=colunas), gr.update(choices=colunas), df,
f"O DataFrame possui {df.shape[0]} linhas e {df.shape[1]} colunas.", df)
def reset_app():
return None, gr.update(choices=[], value=None), "", None, None, None, None, None, None, None
# --- INTERFACE GRÁFICA (GRADIO) ---
with gr.Blocks(theme=gr.themes.Soft(), title="Geocodificador Simplificado") as demo:
estado_df = gr.State()
gr.Markdown("# Geocodificador de Endereços")
gr.Markdown("Faça o upload de uma planilha Excel, selecione as colunas com **NOME da rua** e **NÚMERO**, e o aplicativo encontrará as coordenadas.")
with gr.Tabs():
with gr.TabItem("Processo de Geocodificação"):
with gr.Row():
with gr.Column(scale=1):
arquivo = gr.File(label="1. Selecione um arquivo Excel", file_types=[".xlsx"])
dropdown_abas = gr.Dropdown(label="2. Selecione uma aba da planilha", interactive=True)
dropdown_nome_rua = gr.Dropdown(label="3. Selecione a coluna com o NOME da rua", interactive=True)
dropdown_num = gr.Dropdown(label="4. Selecione a coluna com o NÚMERO do imóvel", interactive=True)
btn_processar = gr.Button("5. Geocodificar Endereços", variant="primary")
with gr.Column(scale=3):
linhas_colunas_output = gr.Textbox(label="Dimensões da Tabela", interactive=False)
tabela_output = gr.Dataframe(label="Pré-visualização dos Dados")
gr.Markdown("---")
gr.Markdown("### Resultados")
with gr.Row():
tabela_interpolada = gr.Dataframe(label="Dados com Coordenadas e Verificação de Endereço", interactive=False)
falhas_output = gr.Dataframe(label="Relatório de Endereços Não Encontrados", interactive=False)
arquivo_excel_output = gr.File(label="Baixar resultado completo como Excel", interactive=False)
with gr.TabItem("Mapa de Resultados"):
mapa_html = gr.HTML(label="Visualização Geográfica dos Dados")
btn_clear = gr.Button("Limpar Tudo e Recomeçar", variant="stop")
# --- LÓGICA DE EVENTOS DA INTERFACE ---
arquivo.change(fn=carregar_abas, inputs=arquivo, outputs=[dropdown_abas, dropdown_abas])
dropdown_abas.change(
fn=listar_colunas,
inputs=[arquivo, dropdown_abas],
outputs=[dropdown_nome_rua, dropdown_num, estado_df, linhas_colunas_output, tabela_output]
)
btn_processar.click(
fn=geocodificar_lote_por_nome,
inputs=[estado_df, dropdown_nome_rua, dropdown_num],
outputs=[tabela_interpolada, falhas_output, arquivo_excel_output]
).then(
fn=gerar_mapa_interpolado,
inputs=tabela_interpolada,
outputs=mapa_html
)
btn_clear.click(fn=reset_app, inputs=None, outputs=[
arquivo, dropdown_abas, linhas_colunas_output, dropdown_nome_rua, dropdown_num,
tabela_output, tabela_interpolada, falhas_output, arquivo_excel_output, mapa_html
])
# --- INICIAR A APLICAÇÃO ---
if __name__ == "__main__":
demo.launch(debug=True)