7. Conversão Programática para GeoJSON (Scripts e Automação)#
Este capítulo apresenta métodos programáticos para converter planilhas e outras fontes de dados em arquivos GeoJSON válidos, usando Python e ferramentas de linha de comando.
Público-alvo: Desenvolvedores, analistas com conhecimento de Python/SQL, profissionais que preferem automação.
7.1 Introdução#
Quando usar métodos programáticos:
- Você tem experiência com Python ou programação
- Precisa processar grandes volumes de dados
- Deseja automatizar o processo de conversão
- Tem dados em múltiplas fontes (banco de dados, APIs, shapefiles, etc.)
- Precisa integrar a conversão em pipelines de dados existentes
Requisitos técnicos:
- Python 3.8+ instalado
- Bibliotecas:
pandas,openpyxl,shapely,geopandas(conforme necessidade) - Para PostGIS:
psycopg2 - Para APIs:
requests
Instalação das bibliotecas básicas:
pip install pandas openpyxl shapely geopandas7.2 Conversão de Planilha Excel/CSV com Python#
O script em Python a seguir converte automaticamente a planilha Excel em GeoJSON, seguindo as regras do schema.
Passo 1: Instalação das bibliotecas necessárias
pip install pandas openpyxlPasso 2: Script de Conversão (Python 3.8+)
Nota: Este script converte a planilha Excel para GeoJSON, processando o campo
locale gerando IDs únicos automaticamente. A geometria deve ser adicionada separadamente (ver Seção 7.3).
import pandas as pd
import json
from datetime import datetime
import sys
def converter_planilha_para_geojson(
arquivo_planilha,
arquivo_geojson_saida,
tipo_template, # 'conservacao' ou 'obras'
lote, # Ex: 'L13'
aba_planilha="Dados"
):
"""
Converte uma planilha Excel/CSV para GeoJSON conforme schema R0.
- Processa campo 'local' (separado por ';') para array
- Gera campo 'id' único automaticamente
- Configura metadados e CRS corretamente
A geometria deve ser adicionada separadamente usando QGIS ou outro método.
"""
print(f"Lendo planilha: {arquivo_planilha}")
try:
if arquivo_planilha.endswith('.xlsx'):
# Ignora as 5 primeiras linhas do template (cabeçalho + instruções + exemplos)
df = pd.read_excel(arquivo_planilha, sheet_name=aba_planilha, skiprows=5)
elif arquivo_planilha.endswith('.csv'):
df = pd.read_csv(arquivo_planilha, skiprows=5)
else:
raise ValueError("Formato não suportado (use .xlsx ou .csv)")
except Exception as e:
print(f"Erro ao ler planilha: {e}")
return
# Remover linhas completamente vazias
df = df.dropna(how='all')
print(f"Encontradas {len(df)} linhas de dados.")
features = []
for idx, row in df.iterrows():
# Gerar ID único (formato: tipo-lote-sequencial)
id_feature = f"{tipo_template[:8]}-{lote}-{str(idx+1).zfill(3)}"
# Processar campo 'local' (separado por ';' no Excel → array no JSON)
local_value = row.get('local', '')
if pd.notna(local_value) and local_value:
# Dividir por ';' e remover espaços
local_array = [loc.strip() for loc in str(local_value).split(';') if loc.strip()]
else:
local_array = []
# Construir properties
properties = {
'id': id_feature,
'lote': row.get('lote'),
'rodovia': row.get('rodovia'),
'detalhamento_servico': row.get('detalhamento_servico'),
'unidade': row.get('unidade'),
'quantidade': row.get('quantidade'),
'km_inicial': row.get('km_inicial'),
'km_final': row.get('km_final'),
'local': local_array,
'data_inicial': row.get('data_inicial'),
'data_final': row.get('data_final'),
'observacoes_gerais': row.get('observacoes_gerais')
}
# Adicionar campos específicos por tipo
if tipo_template == 'conservacao':
properties['item'] = row.get('item')
elif tipo_template == 'obras':
properties['programa'] = row.get('programa')
properties['item'] = int(row.get('item')) if pd.notna(row.get('item')) else None
properties['subitem'] = int(row.get('subitem')) if pd.notna(row.get('subitem')) else None
# Converter datas para string no formato YYYY-MM-DD
for campo_data in ['data_inicial', 'data_final']:
if pd.notna(properties[campo_data]):
if isinstance(properties[campo_data], pd.Timestamp):
properties[campo_data] = properties[campo_data].strftime('%Y-%m-%d')
else:
# Se já for string, garantir formato correto
properties[campo_data] = str(properties[campo_data])
# Converter NaN/None para null JSON
properties_limpas = {}
for k, v in properties.items():
if pd.isna(v) or (isinstance(v, str) and v.strip() == ''):
properties_limpas[k] = None
else:
properties_limpas[k] = v
# Criar feature (geometria None - deve ser adicionada depois)
feature = {
"type": "Feature",
"geometry": None, # Geometria deve ser adicionada com QGIS ou outro método
"properties": properties_limpas
}
features.append(feature)
# Estrutura GeoJSON final
geojson_final = {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": { "name": "urn:ogc:def:crs:EPSG::4674" }
},
"metadata": {
"schema_version": "R0",
"data_geracao": datetime.now().strftime('%Y-%m-%dT%H:%M:%S-03:00')
},
"features": features
}
# Salvar arquivo
with open(arquivo_geojson_saida, 'w', encoding='utf-8') as f:
json.dump(geojson_final, f, ensure_ascii=False, indent=2)
print(f"✅ Arquivo '{arquivo_geojson_saida}' criado com {len(features)} features.")
print(f"⚠️ As geometrias estão NULL. Use uma das opções da Seção 7.3 para adicionar geometrias.")
print(f"⚠️ Valide o arquivo antes de enviar: python validar_geojson.py")
# --- Como usar ---
if __name__ == "__main__":
if len(sys.argv) != 5:
print("Uso: python converter.py <arquivo_excel> <conservacao|obras> <lote> <arquivo_saida>")
print("Exemplo: python converter.py template_lxx_conservacao_2026_r0.xlsx conservacao L13 L13_conservacao_2026_R0.geojson")
sys.exit(1)
arquivo_entrada = sys.argv[1]
tipo = sys.argv[2]
lote = sys.argv[3]
arquivo_saida = sys.argv[4]
if tipo not in ['conservacao', 'obras']:
print("Erro: O tipo deve ser 'conservacao' ou 'obras'")
sys.exit(1)
converter_planilha_para_geojson(
arquivo_planilha=arquivo_entrada,
arquivo_geojson_saida=arquivo_saida,
tipo_template=tipo,
lote=lote,
aba_planilha="Dados"
)Passo 3: Como executar o script
# Para arquivo de conservação do lote L13
python converter.py template_lxx_conservacao_2026_r0.xlsx conservacao L13 L13_conservacao_2026_R0.geojson
# Para arquivo de obras do lote L22
python converter.py template_lxx_obras_2026_r0.xlsx obras L22 L22_obras_2026_R0.geojsonO que o script faz:
- ✅ Lê a planilha Excel (pulando linhas de cabeçalho/exemplos)
- ✅ Gera IDs únicos automaticamente (formato:
conservacao-L13-001,obra-L22-001) - ✅ Converte campo
localde string delimitada para array - ✅ Formata datas para YYYY-MM-DD
- ✅ Configura CRS como
urn:ogc:def:crs:EPSG::4674 - ✅ Adiciona metadados corretos
- ⚠️ Cria geometrias NULL (devem ser adicionadas - ver Seção 7.3)
7.3 Extração de Geometrias de Múltiplas Fontes#
Esta seção apresenta métodos para adicionar geometrias ao GeoJSON a partir de diferentes fontes de dados.
7.3.1 Fonte: Planilha com Coordenadas (lat/lon)#
Se sua planilha tem colunas lat e lon, você pode criar geometrias Point automaticamente:
import pandas as pd
from shapely.geometry import Point, mapping
import json
def criar_point_de_planilha(df):
"""
Cria geometrias Point a partir de colunas lat/lon.
Parâmetros:
df: DataFrame pandas com colunas 'lat' e 'lon'
Retorna:
Lista de features GeoJSON com geometrias Point
"""
features = []
for idx, row in df.iterrows():
# Criar geometria Point
point = Point(row['lon'], row['lat'])
# Criar feature
feature = {
"type": "Feature",
"geometry": mapping(point), # Converte Shapely para GeoJSON
"properties": {
"id": row['id'],
"lote": row['lote'],
"rodovia": row['rodovia'],
"detalhamento_servico": row['detalhamento_servico'],
# ... adicionar outros campos conforme necessário
}
}
features.append(feature)
return features
# Exemplo de uso
df = pd.read_excel('dados_com_latlon.xlsx')
features = criar_point_de_planilha(df)7.3.2 Fonte: Planilha com WKT#
Se sua planilha tem uma coluna wkt com geometrias em formato WKT:
from shapely import wkt
from shapely.geometry import mapping
import pandas as pd
def criar_geometria_de_wkt(df):
"""
Cria geometrias a partir de coluna WKT.
Parâmetros:
df: DataFrame pandas com coluna 'wkt'
Retorna:
Lista de features GeoJSON
"""
features = []
for idx, row in df.iterrows():
# Parsear WKT para objeto Shapely
geom = wkt.loads(row['wkt'])
# Criar feature
feature = {
"type": "Feature",
"geometry": mapping(geom), # Converte Shapely para GeoJSON
"properties": {
"id": row['id'],
"lote": row['lote'],
"rodovia": row['rodovia'],
"detalhamento_servico": row['detalhamento_servico'],
# ... adicionar outros campos
}
}
features.append(feature)
return features
# Exemplo
df = pd.read_csv('dados_com_wkt.csv')
features = criar_geometria_de_wkt(df)7.3.3 Fonte: Banco de Dados PostGIS#
Para extrair geometrias diretamente de um banco de dados PostGIS:
flowchart LR
subgraph DB["🗄️ PostGIS Database"]
Table[(Tabela com<br/>geometrias)]
end
subgraph Python["🐍 Python Script"]
Conn[psycopg2.connect<br/>Conexão]
Query[SQL Query<br/>ST_AsGeoJSON]
Parse[json.loads<br/>Parse geometria]
Build[Construir<br/>Features]
end
subgraph Output["📄 Arquivo GeoJSON"]
JSON["FeatureCollection<br/>features: [...]"]
File[L13_conservacao_2026_R0.geojson]
end
Table -->|SELECT id, ST_AsGeoJSON| Conn
Conn --> Query
Query -->|rows| Parse
Parse --> Build
Build -->|features list| JSON
JSON -->|json.dump| File
style DB fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style Python fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
style Output fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style Table fill:#c8e6c9
style File fill:#ffe0b2import psycopg2
import json
def extrair_de_postgis(query, connection_params):
"""
Extrai geometrias de um banco PostGIS.
Parâmetros:
query: SQL com ST_AsGeoJSON(geom) AS geom_json
connection_params: dict com host, database, user, password
Retorna:
Lista de features GeoJSON
"""
# Conectar ao banco
conn = psycopg2.connect(**connection_params)
cursor = conn.cursor()
# Executar query
cursor.execute(query)
rows = cursor.fetchall()
features = []
for row in rows:
# Estrutura depende da query, este é um exemplo genérico
feature = {
"type": "Feature",
"geometry": json.loads(row[1]), # geom_json já está em formato GeoJSON
"properties": {
"id": row[0],
"lote": row[2],
"rodovia": row[3],
"detalhamento_servico": row[4],
"unidade": row[5],
"quantidade": row[6],
"km_inicial": row[7],
"km_final": row[8],
"local": row[9].split(';') if row[9] else [], # Converter string para array
"data_inicial": str(row[10]),
"data_final": str(row[11]),
"observacoes_gerais": row[12]
}
}
features.append(feature)
cursor.close()
conn.close()
return features
# Exemplo de uso
conn_params = {
'host': 'localhost',
'database': 'rodovias',
'user': 'usuario',
'password': 'senha'
}
query = """
SELECT
id,
ST_AsGeoJSON(ST_Transform(geom, 4674)) AS geom_json,
lote,
rodovia,
detalhamento_servico,
unidade,
quantidade,
km_inicial,
km_final,
local,
data_inicial,
data_final,
observacoes_gerais
FROM obras_conservacao
WHERE ano_programacao = 2026
AND lote = 'L13'
"""
features = extrair_de_postgis(query, conn_params)Dicas para PostGIS:
- Use
ST_AsGeoJSON(geom)para exportar geometria diretamente em formato GeoJSON - Use
ST_Transform(geom, 4674)se a geometria estiver em outro CRS - Filtre dados no SQL para melhor performance (WHERE, LIMIT)
- Para grandes volumes, use
LIMITeOFFSETpara processar em lotes
Instalação do psycopg2:
pip install psycopg2-binary7.3.4 Fonte: GeoPackage#
GeoPackage (.gpkg) é um formato geoespacial baseado em SQLite:
import geopandas as gpd
import json
def extrair_de_geopackage(gpkg_path, layer_name):
"""
Extrai geometrias de um arquivo GeoPackage.
Parâmetros:
gpkg_path: Caminho para o arquivo .gpkg
layer_name: Nome da camada a ser extraída
Retorna:
Lista de features GeoJSON
"""
# Ler camada do GeoPackage
gdf = gpd.read_file(gpkg_path, layer=layer_name)
# Reprojetar para EPSG:4674 se necessário
if gdf.crs.to_epsg() != 4674:
print(f"Reprojetando de {gdf.crs} para EPSG:4674")
gdf = gdf.to_crs(epsg=4674)
# Converter para GeoJSON features
features = []
for idx, row in gdf.iterrows():
feature = {
"type": "Feature",
"geometry": json.loads(gpd.GeoSeries([row.geometry]).to_json())['features'][0]['geometry'],
"properties": {
"id": row['id'],
"lote": row['lote'],
"rodovia": row['rodovia'],
"detalhamento_servico": row['detalhamento_servico'],
"unidade": row['unidade'],
"quantidade": row['quantidade'],
"km_inicial": row['km_inicial'],
"km_final": row['km_final'],
"local": row['local'].split(';') if isinstance(row['local'], str) else row['local'],
"data_inicial": str(row['data_inicial']),
"data_final": str(row['data_final']),
"observacoes_gerais": row['observacoes_gerais']
}
}
features.append(feature)
return features
# Exemplo de uso
features = extrair_de_geopackage('dados_rodovias.gpkg', 'conservacao_2026')Instalação do GeoPandas:
pip install geopandas7.3.5 Fonte: Shapefile#
Para extrair geometrias de um Shapefile:
import geopandas as gpd
import json
def extrair_de_shapefile(shp_path):
"""
Extrai geometrias de um Shapefile.
Parâmetros:
shp_path: Caminho para o arquivo .shp
Retorna:
Lista de features GeoJSON
"""
# Ler shapefile
gdf = gpd.read_file(shp_path)
# Reprojetar para EPSG:4674
if gdf.crs.to_epsg() != 4674:
print(f"Reprojetando de {gdf.crs} para EPSG:4674")
gdf = gdf.to_crs(epsg=4674)
# Converter para GeoJSON features
features = json.loads(gdf.to_json())['features']
# Processar properties para adequar ao schema
for feature in features:
# Processar campo 'local' se for string delimitada
if 'local' in feature['properties']:
local_str = feature['properties']['local']
if isinstance(local_str, str) and ';' in local_str:
feature['properties']['local'] = [
loc.strip() for loc in local_str.split(';') if loc.strip()
]
# Garantir que campos obrigatórios existam
# Adicionar validações conforme necessário
return features
# Exemplo
features = extrair_de_shapefile('conservacao_2026.shp')Nota: Shapef
iles têm limitações:
- Nomes de campos limitados a 10 caracteres
- Não suportam arrays (campo
localprecisará ser string delimitada) - Encoding pode ser problemático (usar
encoding='utf-8'ouencoding='latin1')
7.3.6 Fonte: API REST/Web Service#
Para extrair dados de APIs REST:
import requests
import json
def extrair_de_api(api_url, headers=None, params=None):
"""
Extrai dados geográficos de uma API REST.
Parâmetros:
api_url: URL da API
headers: Headers HTTP (autenticação, etc)
params: Parâmetros da query string
Retorna:
Lista de features GeoJSON
"""
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status() # Levanta exceção se status != 200
data = response.json()
# Processar resposta conforme formato da API
# Este exemplo assume que a API retorna GeoJSON ou estrutura similar
features = []
for item in data.get('results', []): # Ajustar conforme estrutura da API
feature = {
"type": "Feature",
"geometry": item.get('geometry'), # Ajustar conforme API
"properties": {
"id": item.get('id'),
"lote": item.get('lote'),
"rodovia": item.get('rodovia'),
"detalhamento_servico": item.get('descricao'), # Mapear campos
"unidade": item.get('unidade'),
"quantidade": item.get('quantidade'),
"km_inicial": item.get('km_ini'),
"km_final": item.get('km_fim'),
"local": item.get('local', []),
"data_inicial": item.get('data_inicio'),
"data_final": item.get('data_termino'),
"observacoes_gerais": item.get('observacoes')
}
}
features.append(feature)
return features
# Exemplo de uso
api_url = "https://api.exemplo.com/obras/2026"
headers = {"Authorization": "Bearer TOKEN_AQUI"}
params = {"lote": "L13", "tipo": "conservacao"}
features = extrair_de_api(api_url, headers=headers, params=params)Instalação do requests:
pip install requests7.4 Mesclagem de Dados (Atributos + Geometrias)#
Quando você tem atributos em uma planilha Excel e geometrias em outra fonte (shapefile, GeoPackage, banco de dados), você precisa mesclá-los.
flowchart TD
subgraph Source["📊 Fontes de Dados"]
Excel["📗 Excel/CSV<br/>Atributos<br/>id | lote | rodovia | ..."]
Geom["🗺️ Geometrias<br/>Shapefile/GeoPackage/PostGIS<br/>id | geometry"]
end
subgraph Process["⚙️ Processo de Mesclagem"]
Read1[pandas.read_excel<br/>Ler atributos]
Read2[Extrair geometrias<br/>função específica]
Dict[Criar dicionário<br/>geom_dict por ID]
Merge[Mesclar por campo 'id'<br/>JOIN]
Validate{ID encontrado<br/>em ambas fontes?}
end
subgraph Output["✅ Resultado"]
Success[Feature completa<br/>geometry + properties]
Error[⚠️ Registro sem geometria<br/>Log de erro]
Final[GeoJSON Final<br/>FeatureCollection]
end
Excel --> Read1
Geom --> Read2
Read1 --> Merge
Read2 --> Dict
Dict --> Merge
Merge --> Validate
Validate -->|Sim| Success
Validate -->|Não| Error
Success --> Final
Error --> Final
style Excel fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style Geom fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style Process fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style Success fill:#c8e6c9,stroke:#4caf50,stroke-width:2px
style Error fill:#ffccbc,stroke:#ff5722,stroke-width:2px
style Final fill:#fff9c4,stroke:#fbc02d,stroke-width:3px7.4.1 Combinar Planilha Excel com Geometrias de Outra Fonte#
import pandas as pd
import geopandas as gpd
import json
def mesclar_excel_com_geometrias(excel_path, geometrias_features, campo_chave='id'):
"""
Combina atributos de planilha Excel com geometrias de outra fonte.
Parâmetros:
excel_path: Caminho para planilha Excel com atributos
geometrias_features: Lista de features GeoJSON com geometrias
campo_chave: Campo usado para fazer o join (padrão: 'id')
Retorna:
Lista de features GeoJSON mescladas
"""
# Ler atributos da planilha
df_atributos = pd.read_excel(excel_path, skiprows=5) # Ajustar skiprows conforme template
df_atributos = df_atributos.dropna(how='all')
# Criar dicionário de geometrias por ID
geom_dict = {}
for feature in geometrias_features:
key = feature['properties'].get(campo_chave)
if key:
geom_dict[key] = feature['geometry']
# Mesclar
features_mescladas = []
erros = []
for idx, row in df_atributos.iterrows():
id_item = row.get(campo_chave)
if id_item in geom_dict:
# Processar campo 'local'
local_value = row.get('local', '')
if pd.notna(local_value) and local_value:
local_array = [loc.strip() for loc in str(local_value).split(';') if loc.strip()]
else:
local_array = []
# Criar feature mesclada
feature = {
"type": "Feature",
"geometry": geom_dict[id_item],
"properties": {
"id": id_item,
"lote": row.get('lote'),
"rodovia": row.get('rodovia'),
"detalhamento_servico": row.get('detalhamento_servico'),
"unidade": row.get('unidade'),
"quantidade": row.get('quantidade'),
"km_inicial": row.get('km_inicial'),
"km_final": row.get('km_final'),
"local": local_array,
"data_inicial": str(row.get('data_inicial')),
"data_final": str(row.get('data_final')),
"observacoes_gerais": row.get('observacoes_gerais')
}
}
features_mescladas.append(feature)
else:
erros.append(f"⚠️ Aviso: ID '{id_item}' não encontrado nas geometrias")
# Relatar erros
if erros:
print("\nRegistros sem geometria:")
for erro in erros:
print(f" {erro}")
print(f"\n✅ Mescladas {len(features_mescladas)} features")
print(f"⚠️ {len(erros)} registros sem geometria")
return features_mescladas
# Exemplo: Mesclar Excel com Shapefile
excel_file = 'atributos_conservacao.xlsx'
geometrias_shp = extrair_de_shapefile('geometrias_conservacao.shp')
features_final = mesclar_excel_com_geometrias(excel_file, geometrias_shp, campo_chave='id')7.5 Processamento do Campo local (Automático)#
Função utilitária para garantir que o campo local esteja sempre no formato de array:
def processar_campo_local(features):
"""
Converte campo 'local' de string delimitada para array.
Modifica as features in-place.
Parâmetros:
features: Lista de features GeoJSON
Retorna:
features modificadas (mesmo objeto de entrada)
"""
for feature in features:
local_value = feature['properties'].get('local')
if isinstance(local_value, str):
if local_value.strip():
# Converter string delimitada em array
feature['properties']['local'] = [
loc.strip() for loc in local_value.split(';') if loc.strip()
]
else:
feature['properties']['local'] = []
elif local_value is None:
feature['properties']['local'] = []
# Se já for lista, manter como está
return features
# Usar sempre antes de gerar o GeoJSON final
features = processar_campo_local(features)7.6 Geração do Arquivo GeoJSON Final#
Função para gerar o arquivo GeoJSON final com todos os metadados corretos:
from datetime import datetime
import json
def gerar_geojson_final(features, tipo, lote, output_path):
"""
Gera arquivo GeoJSON final conforme especificação ARTESP R0.
Parâmetros:
features: Lista de features GeoJSON
tipo: 'conservacao' ou 'obras'
lote: Ex: 'L13'
output_path: Caminho do arquivo de saída
Retorna:
None (salva arquivo em disco)
"""
geojson_final = {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::4674"
}
},
"metadata": {
"schema_version": "R0",
"data_geracao": datetime.now().strftime('%Y-%m-%dT%H:%M:%S-03:00')
},
"features": features
}
# Salvar arquivo
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(geojson_final, f, ensure_ascii=False, indent=2)
print(f"\n{'='*60}")
print(f"✅ Arquivo GeoJSON gerado com sucesso!")
print(f"{'='*60}")
print(f"📁 Arquivo: {output_path}")
print(f"📊 Total de features: {len(features)}")
print(f"📋 Tipo: {tipo}")
print(f"🏷️ Lote: {lote}")
print(f"{'='*60}\n")
print("⚠️ Próximos passos:")
print(" 1. Validar o arquivo com: python validar_geojson.py")
print(" 2. Verificar visualmente as geometrias no QGIS")
print(" 3. Enviar para a ARTESP conforme instruções")
# Exemplo de uso
gerar_geojson_final(
features=features_final,
tipo='conservacao',
lote='L13',
output_path='L13_conservacao_2026_R0.geojson'
)7.7 Script Completo de Exemplo#
Script unificado que combina todas as funcionalidades:
#!/usr/bin/env python3
"""
Script completo para conversão de múltiplas fontes para GeoJSON ARTESP R0.
Suporta:
- Excel/CSV com lat/lon
- Excel/CSV com WKT
- PostGIS
- GeoPackage
- Shapefile
- API REST
- Mesclagem de atributos + geometrias
Uso:
python converter_completo.py --fonte excel --arquivo dados.xlsx --tipo conservacao --lote L13
python converter_completo.py --fonte postgis --config db_config.json --tipo obras --lote L22
python converter_completo.py --fonte mesclar --atributos dados.xlsx --geometrias geom.shp --tipo conservacao --lote L13
"""
import argparse
import json
import pandas as pd
import geopandas as gpd
from datetime import datetime
import sys
# Importar todas as funções definidas anteriormente
# (Nota: Em produção, organize em módulos separados)
def main():
parser = argparse.ArgumentParser(
description='Conversor para GeoJSON ARTESP',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('--fonte', required=True,
choices=['excel_latlon', 'excel_wkt', 'postgis', 'geopackage', 'shapefile', 'api', 'mesclar'],
help='Tipo de fonte de dados')
parser.add_argument('--arquivo', help='Caminho do arquivo fonte')
parser.add_argument('--atributos', help='Arquivo Excel com atributos (para --fonte mesclar)')
parser.add_argument('--geometrias', help='Arquivo com geometrias (para --fonte mesclar)')
parser.add_argument('--config', help='Arquivo JSON com configuração (para PostGIS ou API)')
parser.add_argument('--tipo', required=True, choices=['conservacao', 'obras'])
parser.add_argument('--lote', required=True, help='Ex: L13')
parser.add_argument('--output', help='Arquivo de saída (opcional)')
args = parser.parse_args()
# Determinar arquivo de saída
output_path = args.output or f"{args.lote}_{args.tipo}_2026_R0.geojson"
# Extrair features conforme fonte
features = []
if args.fonte == 'excel_latlon':
df = pd.read_excel(args.arquivo, skiprows=5)
features = criar_point_de_planilha(df)
elif args.fonte == 'excel_wkt':
df = pd.read_csv(args.arquivo) if args.arquivo.endswith('.csv') else pd.read_excel(args.arquivo, skiprows=5)
features = criar_geometria_de_wkt(df)
elif args.fonte == 'postgis':
with open(args.config, 'r') as f:
config = json.load(f)
features = extrair_de_postgis(config['query'], config['connection'])
elif args.fonte == 'geopackage':
layer = input("Nome da camada (ou pressione Enter para 'default'): ") or 'default'
features = extrair_de_geopackage(args.arquivo, layer)
elif args.fonte == 'shapefile':
features = extrair_de_shapefile(args.arquivo)
elif args.fonte == 'api':
with open(args.config, 'r') as f:
config = json.load(f)
features = extrair_de_api(config['url'], config.get('headers'), config.get('params'))
elif args.fonte == 'mesclar':
geom_features = extrair_de_shapefile(args.geometrias) # Ou outra fonte
features = mesclar_excel_com_geometrias(args.atributos, geom_features)
# Processar campo 'local'
features = processar_campo_local(features)
# Gerar arquivo final
gerar_geojson_final(features, args.tipo, args.lote, output_path)
print(f"\n✅ Processo concluído!")
print(f"📁 Arquivo salvo em: {output_path}")
if __name__ == "__main__":
main()Exemplos de uso:
# Excel com lat/lon
python converter_completo.py --fonte excel_latlon --arquivo dados.xlsx --tipo conservacao --lote L13
# Shapefile
python converter_completo.py --fonte shapefile --arquivo dados.shp --tipo obras --lote L22
# Mesclar Excel + Shapefile
python converter_completo.py --fonte mesclar --atributos atrib.xlsx --geometrias geom.shp --tipo conservacao --lote L13
# PostGIS (com arquivo de configuração)
python converter_completo.py --fonte postgis --config db_config.json --tipo obras --lote L07Exemplo de arquivo de configuração para PostGIS (db_config.json):
{
"connection": {
"host": "localhost",
"database": "rodovias",
"user": "usuario",
"password": "senha"
},
"query": "SELECT id, ST_AsGeoJSON(ST_Transform(geom, 4674)) AS geom_json, lote, rodovia, detalhamento_servico, unidade, quantidade, km_inicial, km_final, local, data_inicial, data_final, observacoes_gerais FROM obras WHERE lote = 'L07'"
}7.8 Mesclagem de Arquivos GeoJSON (Múltiplas Geometrias)#
Contexto e Necessidade#
Quando você trabalha com QGIS e precisa criar geometrias de tipos diferentes (Point, LineString, Polygon) para representar diferentes serviços, você encontra uma limitação técnica:
Problema:
- Cada camada GeoPackage/Shapefile no QGIS aceita apenas UM tipo de geometria
- Se você tem serviços com Point + LineString + Polygon, precisa criar 3 camadas separadas
- Ao exportar cada camada, você obtém 3 arquivos GeoJSON separados
Exigência do Schema:
- O schema ARTESP R0 exige UM ÚNICO arquivo GeoJSON final
- Este arquivo deve conter TODAS as features, independente do tipo de geometria
- O schema permite geometrias mistas no mesmo FeatureCollection
Solução:
- Script Python
mesclar_geojson.pyque combina múltiplos arquivos GeoJSON em um único
7.8.1 Funcionalidades do Script#
O script mesclar_geojson.py realiza as seguintes operações:
- ✅ Carrega múltiplos arquivos GeoJSON (2 ou mais)
- ✅ Valida estrutura de cada arquivo (FeatureCollection, features, geometria, properties)
- ✅ Extrai todas as features de todos os arquivos
- ✅ Verifica IDs únicos - detecta e reporta IDs duplicados
- ✅ Combina em único FeatureCollection preservando todos os atributos
- ✅ Adiciona CRS (EPSG:4674 - SIRGAS 2000)
- ✅ Adiciona metadata (schema_version: R0, data_geracao com timestamp)
- ✅ Gera relatório detalhado com estatísticas de tipos de geometria
- ✅ Salva arquivo final pronto para validação
7.8.2 Código-Fonte Completo#
O código completo está disponível em /scripts/mesclar_geojson.py do projeto.
Estrutura do script:
#!/usr/bin/env python3
"""
Mescla múltiplos arquivos GeoJSON em um único FeatureCollection.
"""
import json
import sys
import os
import argparse
from datetime import datetime, timezone
from collections import Counter
# Funções principais:
# - validar_geojson_entrada(arquivo) → valida estrutura JSON/GeoJSON
# - extrair_features(data, arquivo) → extrai e valida features
# - validar_ids_unicos(features) → verifica duplicatas de IDs
# - mesclar_geojson_files(entrada, saida) → função principal de mesclagem
# - main() → CLI com argparsePrincipais funções:
validar_geojson_entrada(arquivo)#
Valida se um arquivo é um GeoJSON válido antes de processar:
def validar_geojson_entrada(arquivo):
"""Valida se o arquivo é um GeoJSON válido."""
try:
with open(arquivo, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print(f"❌ ERRO: Arquivo não encontrado: '{arquivo}'")
return None
except json.JSONDecodeError as e:
print(f"❌ ERRO: Arquivo '{arquivo}' não é um JSON válido: {e}")
return None
# Verifica estrutura básica de GeoJSON
if data.get('type') != 'FeatureCollection':
print(f"⚠️ AVISO: '{arquivo}' não é um FeatureCollection")
if 'features' not in data:
print(f"⚠️ AVISO: '{arquivo}' não contém campo 'features'")
return dataextrair_features(data, arquivo)#
Extrai features válidas de um GeoJSON:
def extrair_features(data, arquivo):
"""Extrai as features de um GeoJSON."""
features = data.get('features', [])
if not features:
print(f"⚠️ AVISO: '{arquivo}' não contém features")
return []
features_validas = []
for idx, feature in enumerate(features):
# Valida estrutura mínima
if 'geometry' not in feature:
print(f"⚠️ Feature {idx} em '{arquivo}' sem geometria, ignorando")
continue
if 'properties' not in feature:
print(f"⚠️ Feature {idx} em '{arquivo}' sem properties, ignorando")
continue
features_validas.append(feature)
return features_validasvalidar_ids_unicos(features)#
Verifica se os IDs são únicos em todas as features:
def validar_ids_unicos(features, emitir_aviso=True):
"""Valida se todos os IDs são únicos e reporta duplicatas."""
ids = []
for feature in features:
feature_id = feature.get('properties', {}).get('id')
if feature_id is not None:
ids.append(feature_id)
# Contar ocorrências de cada ID
id_counts = Counter(ids)
ids_duplicados = {id_val: count
for id_val, count in id_counts.items()
if count > 1}
if emitir_aviso and ids_duplicados:
print(f"\n⚠️ AVISO: {len(ids_duplicados)} IDs duplicados:")
for dup_id, count in sorted(ids_duplicados.items()):
print(f" ID '{dup_id}' aparece {count} vezes")
return len(set(ids)), ids_duplicadosmesclar_geojson_files(arquivos_entrada, arquivo_saida)#
Função principal que realiza a mesclagem:
def mesclar_geojson_files(arquivos_entrada, arquivo_saida, incluir_metadata=True):
"""Mescla múltiplos arquivos GeoJSON em um único FeatureCollection."""
todas_features = []
# Processar cada arquivo de entrada
for arquivo in arquivos_entrada:
data = validar_geojson_entrada(arquivo)
if data is None:
continue
features = extrair_features(data, arquivo)
if not features:
continue
# Reportar tipos de geometria
geom_types = Counter(f.get('geometry', {}).get('type')
for f in features)
print(f"✅ {len(features)} features extraídas:")
for geom_type, count in sorted(geom_types.items()):
print(f" - {geom_type}: {count}")
todas_features.extend(features)
# Validar IDs únicos
ids_unicos, ids_duplicados = validar_ids_unicos(todas_features)
# Construir GeoJSON de saída
geojson_saida = {
"type": "FeatureCollection",
"features": todas_features
}
if incluir_metadata:
geojson_saida["crs"] = {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::4674"
}
}
# Data/hora com timezone
agora = datetime.now(timezone.utc).astimezone()
data_geracao = agora.strftime('%Y-%m-%dT%H:%M:%S%z')
data_geracao = data_geracao[:-2] + ':' + data_geracao[-2:]
geojson_saida["metadata"] = {
"schema_version": "R0",
"data_geracao": data_geracao
}
# Salvar arquivo
with open(arquivo_saida, 'w', encoding='utf-8') as f:
json.dump(geojson_saida, f, ensure_ascii=False, indent=2)
print(f"\n✅ Arquivo mesclado salvo: {arquivo_saida}")
return True7.8.3 Exemplos de Uso#
Exemplo 1: Mesclar 2 arquivos (Points + LineStrings)#
python scripts/mesclar_geojson.py \
L13_conservacao_points.geojson \
L13_conservacao_lines.geojson \
-o L13_conservacao_2026_R0.geojsonSaída esperada:
======================================================================
Mesclagem de Arquivos GeoJSON
======================================================================
📂 Arquivos de entrada: 2
[1/2] Processando: L13_conservacao_points.geojson
------------------------------------------------------------
✅ 12 features extraídas:
- Point: 12
[2/2] Processando: L13_conservacao_lines.geojson
------------------------------------------------------------
✅ 5 features extraídas:
- LineString: 5
======================================================================
📊 Resumo da Mesclagem
======================================================================
Total de features: 17
Tipos de geometria:
- LineString: 5
- Point: 12
Verificando unicidade dos IDs...
IDs únicos: 17
✅ Arquivo mesclado salvo com sucesso!
📁 Arquivo de saída: L13_conservacao_2026_R0.geojson
📏 Tamanho: 28.4 KB
======================================================================
✅ Próximo passo: Validar com validar_geojson.py
======================================================================Exemplo 2: Mesclar 3 arquivos (Points + LineStrings + Polygons)#
python scripts/mesclar_geojson.py \
L22_obras_points_TEMP.geojson \
L22_obras_lines_TEMP.geojson \
L22_obras_polygons_TEMP.geojson \
-o L22_obras_2026_R0.geojsonExemplo 3: Mesclar sem adicionar metadata#
Útil se você quiser adicionar metadata manualmente depois:
python scripts/mesclar_geojson.py \
arquivo1.geojson \
arquivo2.geojson \
-o saida.geojson \
--no-metadataExemplo 4: Mesclar 4 ou mais arquivos#
O script aceita quantos arquivos você precisar:
python scripts/mesclar_geojson.py \
L07_points.geojson \
L07_lines.geojson \
L07_polygons.geojson \
L07_multipoints.geojson \
-o L07_conservacao_2026_R0.geojson7.8.4 Opções da Linha de Comando#
python mesclar_geojson.py [arquivos...] -o SAIDA [opções]Argumentos posicionais:
arquivos- Lista de arquivos GeoJSON para mesclar (mínimo 2)
Argumentos obrigatórios:
-o,--output- Arquivo de saída (GeoJSON mesclado)
Argumentos opcionais:
--no-metadata- Não adicionar campos CRS e metadata ao arquivo de saída-h,--help- Exibir ajuda
Exemplos:
# Ajuda
python mesclar_geojson.py --help
# Uso mínimo
python mesclar_geojson.py arq1.geojson arq2.geojson -o saida.geojson
# Com múltiplos arquivos
python mesclar_geojson.py *.geojson -o combinado.geojson
# Sem metadata
python mesclar_geojson.py a.geojson b.geojson -o saida.geojson --no-metadata7.8.5 Validações Realizadas#
O script realiza as seguintes validações durante o processamento:
| Validação | Descrição | Comportamento em caso de erro |
|---|---|---|
| Arquivo existe | Verifica se arquivo de entrada existe | ❌ Reporta erro e pula arquivo |
| JSON válido | Valida sintaxe JSON | ❌ Reporta erro e pula arquivo |
| FeatureCollection | Verifica se type = “FeatureCollection” | ⚠️ Emite aviso, continua processando |
| Campo features | Verifica se contém campo “features” | ⚠️ Emite aviso, tenta extrair |
| Feature tem geometry | Cada feature deve ter geometria | ⚠️ Pula feature inválida |
| Feature tem properties | Cada feature deve ter properties | ⚠️ Pula feature inválida |
| IDs únicos | Verifica duplicatas entre TODOS os IDs | ⚠️ Emite aviso, continua (não bloqueia) |
| Arquivo saída existe | Pergunta se deseja sobrescrever | ⚠️ Solicita confirmação do usuário |
Nota importante sobre IDs duplicados:
- O script detecta e reporta IDs duplicados, mas não bloqueia a mesclagem
- Isso permite que você veja o problema e corrija depois
- IDs duplicados falharão na validação com
validar_geojson.py - Corrija os IDs antes de submeter o arquivo
7.8.6 Estrutura do GeoJSON de Saída#
O arquivo mesclado terá a seguinte estrutura:
{
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::4674"
}
},
"metadata": {
"schema_version": "R0",
"data_geracao": "2026-01-15T14:30:00-03:00"
},
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-46.633, -23.550]
},
"properties": {
"id": "conserva-001",
"lote": "L13",
...
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-46.633, -23.550], [-46.634, -23.551]]
},
"properties": {
"id": "conserva-002",
...
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[...]]
},
"properties": {
"id": "conserva-003",
...
}
}
]
}Campos adicionados automaticamente:
crs- Sistema de coordenadas (EPSG:4674)metadata.schema_version- Versão do schema (R0)metadata.data_geracao- Timestamp ISO8601 com timezone
7.8.7 Tratamento de Erros Comuns#
Erro: “File not found”#
❌ ERRO: Arquivo não encontrado: 'arquivo.geojson'Solução:
- Verifique o caminho do arquivo
- Use caminho absoluto ou relativo correto
- No Windows, use
\ou/nas pastas
Erro: “Arquivo não é um JSON válido”#
❌ ERRO: Arquivo 'dados.geojson' não é um JSON válido:
Expecting ',' delimiter: line 15 column 5Solução:
- Abra o arquivo em um editor JSON (VS Code)
- Corrija erros de sintaxe (vírgulas, aspas, chaves)
- Valide com um validador JSON online
Aviso: “IDs duplicados encontrados”#
⚠️ AVISO: 2 IDs duplicados encontrados:
ID 'conserva-005' aparece 2 vezes
ID 'conserva-012' aparece 3 vezesSolução:
- Abra os arquivos de entrada no QGIS
- Localize as features com IDs duplicados (tabela de atributos)
- Corrija os IDs para serem únicos
- Re-exporte as camadas
- Execute o script novamente
Aviso: “Feature sem geometria”#
⚠️ Feature 5 em 'arquivo.geojson' sem geometria, ignorandoSolução:
- A feature será ignorada (pulada)
- Verifique no QGIS se todas features têm geometria válida
- Re-exporte a camada se necessário
7.8.8 Integração com Workflow QGIS#
O script se integra perfeitamente no workflow QGIS:
flowchart LR
QGIS["QGIS:<br/>Criar múltiplas camadas"]
Export["Exportar<br/>cada camada"]
Files["Múltiplos<br/>GeoJSON"]
Script["mesclar_geojson.py"]
Final["GeoJSON<br/>único"]
Validate["validar_geojson.py"]
QGIS --> Export
Export --> Files
Files --> Script
Script --> Final
Final --> Validate
style Script fill:#fff4e1
style Final fill:#e7f9e7Workflow completo:
- QGIS: Criar camadas (points.gpkg, lines.gpkg, polygons.gpkg)
- QGIS: Exportar cada camada como GeoJSON
- Script:
python mesclar_geojson.py *.geojson -o final.geojson - Validação:
python validar_geojson.py schema.json final.geojson - Submissão: Upload no portal ARTESP
7.8.9 Download e Disponibilidade#
Onde encontrar o script:
- Pasta do projeto:
/scripts/mesclar_geojson.py - Portal de Dados Abertos ARTESP:
- Código-fonte completo nesta documentação (Seção 7.8.2)
Requisitos:
- Python 3.6 ou superior
- Bibliotecas padrão (json, sys, os, argparse, datetime, collections)
- Nenhuma dependência externa necessária
Instalação:
# Nenhuma instalação necessária - use o script diretamente
python scripts/mesclar_geojson.py --help7.8.10 Referências Cruzadas#
- Uso prático no QGIS: Seção 6.5.A - Múltiplas Camadas
- Aviso na criação de camadas: Seção 6.3 Passo D
- Validação após mesclagem: Capítulo 9 - Validação
- Script de validação: Seção 9.1.3
🎉 Parabéns! Agora você tem um conjunto completo de ferramentas programáticas para converter dados de múltiplas fontes para GeoJSON conforme o schema ARTESP R0.
Próximos passos:
- Adapte os scripts conforme suas necessidades específicas
- Teste com dados de desenvolvimento primeiro
- Valide sempre com
python validar_geojson.py - Consulte o Capítulo 9 para validação detalhada
- Consulte o Capítulo 10 para solução de erros comuns