Posted in

Transforme Shapefiles em arquivos KMZ Rotulado com Python (Passo a Passo)

Rotular arquivos KMZ pode ser uma tarefa desafiadora, especialmente porque o Google Earth impõe algumas limitações — principalmente no que diz respeito à exibição de rótulos para linhas e polígonos. Normalmente, apenas pontos são rotulados automaticamente. Já no ArcGIS Earth, esse tipo de visualização funciona sem maiores problemas.

Mas e se você quiser converter uma pasta cheia de shapefiles para o formato KMZ e rotular os polígonos com base em um dos campos da tabela de atributos? E mais: incluir essa tabela como um popup clicável no KMZ?

A boa notícia é que isso é totalmente possível com um simples script em Python.

O que você vai precisar

Antes de rodar o script, certifique-se de que o Python está instalado em seu computador. Em seguida, abra o Prompt de Comando (no Windows) e execute o seguinte comando para instalar as bibliotecas necessárias:

pip install geopandas pandas pathlib

Depois disso, siga estes passos:

  1. Crie um novo arquivo de texto;
  2. Cole o código Python que fornecemos abaixo;
  3. Salve o arquivo com a extensão .py (por exemplo, converter_para_kmz.py);
  4. Clique com o botão direito no arquivo e escolha “Abrir com” > python.exe (ou execute via terminal).

Pronto! O script deve rodar automaticamente, convertendo seus shapefiles em arquivos KMZ rotulados e com popups interativos.

import geopandas as gpd
import pandas as pd
from pathlib import Path
import zipfile
import math

NAME_FIELD = "TALHAO"  # mude para o campo que desejar rotular

def xml_escape(text):
    s = "" if text is None else str(text)
    return (s.replace("&", "&")
             .replace("<", "&lt;")
             .replace(">", "&gt;")
             .replace('"', "&quot;")
             .replace("'", "&apos;"))

def is_null(v):
    try:
        return pd.isna(v) or (isinstance(v, float) and math.isnan(v))
    except Exception:
        return v is None

def coords_str(ring):
    # lon,lat,alt(0)
    return " ".join(f"{c[0]},{c[1]},0" for c in ring.coords)

def polygon_xml(poly):
    outer = f"""
    <outerBoundaryIs>
      <LinearRing>
        <coordinates>{coords_str(poly.exterior)}</coordinates>
      </LinearRing>
    </outerBoundaryIs>"""
    inner_xml = ""
    if poly.interiors:
        inner_xml = "\n".join(
            f"""    <innerBoundaryIs>
      <LinearRing>
        <coordinates>{coords_str(r)}</coordinates>
      </LinearRing>
    </innerBoundaryIs>""" for r in poly.interiors
        )
    return f"<Polygon>{outer}{inner_xml}\n</Polygon>"

def geometry_with_label_xml(geom):
    # MultiGeometry: all polygon parts + one representative point for on-map label
    if geom.geom_type == "Polygon":
        polys = [geom]
    elif geom.geom_type == "MultiPolygon":
        polys = [p for p in geom.geoms if not p.is_empty]
    else:
        return ""  # ignore non-polygons

    poly_parts = "\n".join(polygon_xml(p) for p in polys)
    rp = geom.representative_point()  # label point inside polygon
    pt = f"<Point><coordinates>{rp.x},{rp.y},0</coordinates></Point>"
    return f"<MultiGeometry>\n{poly_parts}\n{pt}\n</MultiGeometry>"

def extended_data_xml(row, columns):
    items = []
    for col in columns:
        val = "" if is_null(row[col]) else xml_escape(row[col])
        items.append(f'<Data name="{xml_escape(col)}"><value>{val}</value></Data>')
    return "<ExtendedData>" + "".join(items) + "</ExtendedData>"

def pick_name(row, columns):
    if NAME_FIELD in columns and not is_null(row[NAME_FIELD]):
        return str(row[NAME_FIELD])
    for col in columns:
        v = row[col]
        if not is_null(v):
            return str(v)
    return str(row.name)

# ---- Runtime inputs ----
print ("BEM VINDO AO CONVERSOR EM MASSA DE SHP EM UMA PASTA PARA KMZ ROTULADO.")
src = Path(input("Entre o caminho da pasta com os shps: ").strip('"'))
dst = Path(input("Entre a pasta de destino dos kmz: ").strip('"'))
exclude_text = input("Pule arquivos que contenham isto no nome do arquivo. Deixe em branco para nenhum: ").strip()
label_scale_in = input("Tamanho do Label (e.g., 0.8 pequeno, 1 normal, 1.5 grande) [padrão 0.8]: ").strip()
try:
    label_scale = float(label_scale_in) if label_scale_in else 0.8
except ValueError:
    print("Escala inválida. Usando 0.8.")
    label_scale = 0.8

dst.mkdir(parents=True, exist_ok=True)

# Build KML header with chosen label size
KML_HEADER = f"""<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="polyWithLabel">
  <LineStyle>
    <color>ff00ffff</color>  <!-- Yellow (aabbggrr) -->
    <width>1</width>
  </LineStyle>
  <PolyStyle>
    <fill>0</fill>           <!-- No fill -->
    <outline>1</outline>
  </PolyStyle>
  <IconStyle>
    <scale>0</scale>         <!-- hide icon; keeps only text label -->
  </IconStyle>
  <LabelStyle>
    <color>ffffffff</color>  <!-- white label text -->
    <scale>{label_scale}</scale>
  </LabelStyle>
</Style>
"""

KML_FOOTER = "</Document></kml>"

for shp in src.glob("*.shp"):
    # Skip by filename substring if requested
    if exclude_text and exclude_text.lower() in shp.name.lower():
        print(f"[SKIP] {shp.name} (devido a texto excluidor entrado: '{exclude_text}')")
        continue

    print(f"Processando {shp.name}...")
    gdf = gpd.read_file(shp)

    # Reproject to WGS84 (KML requirement)
    if gdf.crs is not None and gdf.crs.to_string() != "EPSG:4326":
        gdf = gdf.to_crs("EPSG:4326")
    elif gdf.crs is None:
        print(f"  [WARN] {shp.name}: projecao faltando; Usando WGS84.")

    attr_cols = [c for c in gdf.columns if c != gdf.geometry.name]

    parts = [KML_HEADER, f"<Folder><name>{xml_escape(shp.stem)}</name>"]

    for _, row in gdf.iterrows():
        geom = row.geometry
        if geom is None or geom.is_empty:
            continue
        if not geom.is_valid and geom.geom_type in ("Polygon", "MultiPolygon"):
            geom = geom.buffer(0)

        geom_xml = geometry_with_label_xml(geom)
        if not geom_xml:
            continue

        name_text = xml_escape(pick_name(row, attr_cols))
        data_xml = extended_data_xml(row, attr_cols)

        parts.append(f"""
<Placemark>
  <name>{name_text}</name>
  <styleUrl>#polyWithLabel</styleUrl>
  {data_xml}
  {geom_xml}
</Placemark>""")

    parts.append("</Folder>")
    parts.append(KML_FOOTER)
    kml_content = "\n".join(parts)

    # Write KMZ (doc.kml inside)
    kmz_path = dst / f"{shp.stem}.kmz"
    with zipfile.ZipFile(kmz_path, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("doc.kml", kml_content)

print("Concluido.")

Se encontrar qualquer problema ou tiver dúvidas, fique à vontade para comentar abaixo — vou tentar ajudar!

Aqui está nosso conversor, usando um código parecido com o acima, confira, use e compartilhe

Gostou? Compartilhe

Um comentário em “Transforme Shapefiles em arquivos KMZ Rotulado com Python (Passo a Passo)

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *